自定義ViewGroup (3) 與子View之間 Touch Event的攔截與處理

在昨天的博客(自定義ViewGroup(2))中,咱們解決了多個手指交替滑動帶來的頁面的跳動問題。但同時也還遺留了兩個問題。 java

  1. 咱們自定義的這個ViewGroup自己還不支持onClick, onLongClick事件。
  2. 當咱們給子View設置click事件後,咱們的ViewGroup竟然不能滑動了。

相對來說,第一個問題稍稍容易處理一點,這裏咱們先說一下第二個問題。 app

onInterceptTouchEvent()的做用以及什麼時候會被調用

onInterceptTouchEvent()是用來給ViewGroup本身一個攔截事件的機會,當ViewGroup意識到某個Touch事件應該由本身處理,那麼就能夠經過此方法來阻止事件被分發到子View中。 ide

爲何onInterceptTouchEvent()方法只接收到來ACTION_DOWN事件??須要處理ACTION_MOVE,ACTION_UP等等事件嗎??

按照google官方文檔的說明: 動畫

  1. 若是onInterceptTouchEvent方法返回true,那麼它將不會收到後續事件,事件將會直接傳遞給目標的onTouchEvent方法(其實會先傳給目標的onTouch方法)
  2. 若是onInterceptTouchEvent方法返回false,那麼全部的後續事件都會先傳給onInterceptTouchEvent,而後再傳給目標的onTouchEvent方法。

可是,爲何咱們在onInterceptTouchEvent方法中返回false以後,卻收不到後續的事件呢??經過實驗以及stackoverflow上面的一些問答得知,當咱們在onInterceptTouchEvent()方法中返回false,子View的onTouchEvent返回true的狀況下,onInterceptTouchEvent方法纔會收到後續的事件。 ui

雖然這個結果與官方文檔的說法有點不一樣,但實驗說明是正確的。仔細想一想這樣的邏輯也確實很是合理:由於onInterceptTouchEvent方法是用來攔截觸摸事件,防止被子View捕獲。那麼如今子View在onTouchEvent中返回false,明確聲明本身不會處理這個觸摸事件,那麼這個時候還須要攔截嗎?固然就不須要了,所以onInterceptTouchEvent不須要攔截這個事件,那也就沒有必要將後續事件再傳給它了。 google

還有就是onInterceptTouchEvent()被調用的前提是它的子View沒有調用requestDisallowInterceptTouchEvent(true)方法(這個方法用於阻止ViewGroup攔截事件)。 spa

ViewGroup的onInterceptTouchEvent方法,onTouchEvent方法以及View的onTouchEvent方法之間的事件傳遞流程

畫了一個簡單的圖,以下: .net

其中:Intercept指的是onInterceptTouchEvent()方法,Touch指的是onTouchEvent()方法。 code

好了,如今咱們能夠解決博客開頭列出的第二個問題了,之因此爲子View設置click以後,咱們的ViewGroup方法沒法滑動,是由於,子View在接受到ACTION_DOWN事件後返回true,而且ViewGroup的onInterceptTouchEvent()方法的默認實現是返回false(就是徹底不攔截),因此後續的ACTION_MOVE,ACTION_UP事件都傳遞給了子View,所以咱們的ViewGroup天然就沒法滑動了。 orm

解決方法就是重寫onInterceptTouchEvent方法:

/**
	 * onInterceptTouchEvent()用來詢問是否要攔截處理。 onTouchEvent()是用來進行處理。
	 * 
	 * 例如:parentLayout----childLayout----childView 事件的分發流程:
	 * parentLayout::onInterceptTouchEvent()---false?--->
	 * childLayout::onInterceptTouchEvent()---false?--->
	 * childView::onTouchEvent()---false?--->
	 * childLayout::onTouchEvent()---false?---> parentLayout::onTouchEvent()
	 * 
	 * 
	 * 
	 * 若是onInterceptTouchEvent()返回false,且分發的子View的onTouchEvent()中返回true,
	 * 那麼onInterceptTouchEvent()將收到全部的後續事件。
	 * 
	 * 若是onInterceptTouchEvent()返回true,本來的target將收到ACTION_CANCEL,該事件
	 * 將會發送給咱們本身的onTouchEvent()。
	 */
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		final int action = ev.getActionMasked();
		if (BuildConfig.DEBUG)
			Log.d("onInterceptTouchEvent", "action: " + action);

		if (action == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
			// 該事件可能不是咱們的
			return false;
		}

		boolean isIntercept = false;
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			// 若是動畫還未結束,則將此事件交給onTouchEvet()處理,
			// 不然,先分發給子View
			isIntercept = !mScroller.isFinished();
			// 若是此時不攔截ACTION_DOWN時間,應該記錄下觸摸地址及手指id,當咱們決定攔截ACTION_MOVE的event時,
			// 將會須要這些初始信息(由於咱們的onTouchEvent將可能接收不到ACTION_DOWN事件)
			mPointerId = ev.getPointerId(0);
//			if (!isIntercept) {
			downX = x = ev.getX();
			downY = y = ev.getY();
//			}
			break;
		case MotionEvent.ACTION_MOVE:
			int pointerIndex = ev.findPointerIndex(mPointerId);
			if (BuildConfig.DEBUG)
				Log.d("onInterceptTouchEvent", "pointerIndex: " + pointerIndex
						+ ", pointerId: " + mPointerId);
			float mx = ev.getX(pointerIndex);
			float my = ev.getY(pointerIndex);

			if (BuildConfig.DEBUG)
				Log.d("onInterceptTouchEvent", "action_move [touchSlop: "
						+ mTouchSlop + ", deltaX: " + (x - mx) + ", deltaY: "
						+ (y - my) + "]");

			// 根據方向進行攔截,(其實這樣,若是咱們的方向是水平的,裏面有一個ScrollView,那麼咱們是支持嵌套的)
			if (orientation == Orientation.HORIZONTAL) {
				if (Math.abs(x - mx) >= mTouchSlop) {
					// we get a move event for ourself
					isIntercept = true;
				}
			} else {
				if (Math.abs(y - my) >= mTouchSlop) {
					isIntercept = true;
				}
			}

			//若是不攔截的話,咱們不會更新位置,這樣能夠經過累積小的移動距離來判斷是否達到能夠認爲是Move的閾值。
			//這裏當產生攔截的話,會更新位置(這樣至關於損失了mTouchSlop的移動距離,若是不更新,可能會有一點點跳的感受)
			if (isIntercept) {
				x = mx;
				y = my;
			}
			break;
		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP:
			// 這是觸摸的最後一個事件,不管如何都不會攔截
			if (velocityTracker != null) {
				velocityTracker.recycle();
				velocityTracker = null;
			}
			break;
		case MotionEvent.ACTION_POINTER_UP:
			solvePointerUp(ev);
			break;
		}
		return isIntercept;
	}

private void solvePointerUp(MotionEvent event) {
		// 獲取離開屏幕的手指的索引
		int pointerIndexLeave = event.getActionIndex();
		int pointerIdLeave = event.getPointerId(pointerIndexLeave);
		if (mPointerId == pointerIdLeave) {
			// 離開屏幕的正是目前的有效手指,此處須要從新調整,而且須要重置VelocityTracker
			int reIndex = pointerIndexLeave == 0 ? 1 : 0;
			mPointerId = event.getPointerId(reIndex);
			// 調整觸摸位置,防止出現跳動
			x = event.getX(reIndex);
			y = event.getY(reIndex);
			if (velocityTracker != null)
				velocityTracker.clear();
		}
	}



如今再運行app,問題應該解決了。

onTouchEvent收到ACTION_DOWN,是否必定能收到ACTION_MOVE,ACTION_UP...???     收到了ACTION_MOVE,可否說明它已經收到過ACTION_DOWN???

其實根據上面所說的onInterceptTouchEvent方法與onTouchEvent方法之間事件傳遞的過程,咱們知道這兩個問題的答案都是否認的。

對於第一個,收到ACTION_DOWN事件後,ACTION_MOVE事件可能會被攔截,那麼它將只可以再收到一個ACTION_CANCEL事件。

對於第二個,是基於上面的這一個狀況,ACTION_DOWN傳遞給了子View,而onInterceptTouchEvent攔截了ACTION_MOVE事件,因此咱們的onTouchEvent方法將會收到ACTION_MOVE,而不會收到ACTION_DOWN。(這也是爲何我在onInterceptTouchEvent方法的ACTION_DOWN中記錄下位置信息的緣由)

還有一個問題就是,若是咱們單純的在onTouchEvent中: 對於ACTION_DOWN返回true,在接收到ACTION_MOVE事件後返回false,那麼這個時候事件會從新尋找能處理它的View嗎?不會,全部的後續事件依然會發給這個onTouchEvent方法。


讓ViewGroup支持click事件

這裏咱們是在onTouchEvent中對於ACTION_UP多作了一些處理:

  1. 判斷從按下時的位置到如今的移動距離是否小於可被識別爲Move的閾值。
  2. 根據ACTION_DOWN和ACTION_UP之間的時間差,判斷是CLICK,仍是LONG CLICK(這裏當沒有設置long click的話,咱們也可將其認爲是click)
    case MotionEvent.ACTION_UP:
    			//先判斷是不是點擊事件
    			final int pi = event.findPointerIndex(mPointerId);
    			
    			if((isClickable() || isLongClickable()) 
    					&& ((event.getX(pi) - downX) < mTouchSlop || (event.getY(pi) - downY) < mTouchSlop)) {
    				//這裏咱們獲得了一個點擊事件
    				if(isFocusable() && isFocusableInTouchMode() && !isFocused())
    					requestFocus();
    				if(event.getEventTime() - event.getDownTime() >= ViewConfiguration.getLongPressTimeout() && isLongClickable()) {
    					//是一個長按事件
    					performLongClick();
    				} else {
    					performClick();
    				}
    			} else {
    				velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);
    				float velocityX = velocityTracker.getXVelocity(mPointerId);
    				float velocityY = velocityTracker.getYVelocity(mPointerId);
    	
    				completeMove(-velocityX, -velocityY);
    				if (velocityTracker != null) {
    					velocityTracker.recycle();
    					velocityTracker = null;
    				}
    			}
    			break;



  3. 這裏只列出了對於ACTION_UP事件的處理(其他部分和上一片博客中的相同),如今咱們應該能夠爲我們的ViewGroup設置click事件了吧:)

相關文章
相關標籤/搜索