在昨天的博客(自定義ViewGroup(2))中,咱們解決了多個手指交替滑動帶來的頁面的跳動問題。但同時也還遺留了兩個問題。 java
相對來說,第一個問題稍稍容易處理一點,這裏咱們先說一下第二個問題。 app
onInterceptTouchEvent()是用來給ViewGroup本身一個攔截事件的機會,當ViewGroup意識到某個Touch事件應該由本身處理,那麼就能夠經過此方法來阻止事件被分發到子View中。 ide
按照google官方文檔的說明: 動畫
可是,爲何咱們在onInterceptTouchEvent方法中返回false以後,卻收不到後續的事件呢??經過實驗以及stackoverflow上面的一些問答得知,當咱們在onInterceptTouchEvent()方法中返回false,且子View的onTouchEvent返回true的狀況下,onInterceptTouchEvent方法纔會收到後續的事件。 ui
雖然這個結果與官方文檔的說法有點不一樣,但實驗說明是正確的。仔細想一想這樣的邏輯也確實很是合理:由於onInterceptTouchEvent方法是用來攔截觸摸事件,防止被子View捕獲。那麼如今子View在onTouchEvent中返回false,明確聲明本身不會處理這個觸摸事件,那麼這個時候還須要攔截嗎?固然就不須要了,所以onInterceptTouchEvent不須要攔截這個事件,那也就沒有必要將後續事件再傳給它了。 google
還有就是onInterceptTouchEvent()被調用的前提是它的子View沒有調用requestDisallowInterceptTouchEvent(true)方法(這個方法用於阻止ViewGroup攔截事件)。 spa
畫了一個簡單的圖,以下: .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(); } }
其實根據上面所說的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方法。
這裏咱們是在onTouchEvent中對於ACTION_UP多作了一些處理:
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;
這裏只列出了對於ACTION_UP事件的處理(其他部分和上一片博客中的相同),如今咱們應該能夠爲我們的ViewGroup設置click事件了吧:)