在阅读Android触摸事件处理的源码的时候看到了这么一个方法:event.getPointerIdBits()
。这个方法是不对外暴露的。但是我们可以看到他的源码:
1 2 3 4 5 6 7 8
| public final int getPointerIdBits() { int idBits = 0; final int pointerCount = nativeGetPointerCount(mNativePtr); for (int i = 0; i < pointerCount; i++) { idBits |= 1 << nativeGetPointerId(mNativePtr, i); } return idBits; }
|
可以看到他先是调用了nativeGetPointerCount()
这个C方法获取到当前手指头触摸在屏幕上的总数。想获取手指头触摸在屏幕的总数我们可以通过方法:event.getPointerCount()
这是官方暴露给我们的方法,他的源码就是调用了方法nativeGetPointerCount()
1 2 3
| public final int getPointerCount() { return nativeGetPointerCount(mNativePtr); }
|
继续回到方法getPointerIdBits
,可以看到里面调用了方法nativeGetPointerId()
。这个方法就是获取到指定位置的手指id。这个方法官方也提供了一个公开方法让我们调用event.getPointerId(i)
。
1 2 3
| public final int getPointerId(int pointerIndex) { return nativeGetPointerId(mNativePtr, pointerIndex); }
|
所以这么一拆分,我们可以将方法getPointerIdBits
拆成这么写:
1 2 3 4 5 6 7 8
| public final int getPointerIdBits() { int idBits = 0; final int pointerCount = getPointerCount(); for (int i = 0; i < pointerCount; i++) { idBits |= 1 << getPointerId(i); } return idBits; }
|
这么一看我们似乎明白了getPointerIdBits()
是干嘛的了。我们可以通过这个方法获取到是有哪些手指头放在屏幕上了。
这个最好我们自己试验下。getPointerId(index)他是获取当前这个index对应的手指id。比如我先大拇指按在屏幕上,这个时候他的getPointerId的值就是0。然后保持大拇指按着,在按下无名指这个时候这个无名指的getPointerId就是1。如果这个时候你抬起大拇指那么调用getPointerId得到的值是1。这个方法就是用来获取在屏幕上手指的id的。默认这个id从0开始。
一个触摸事件在ViewRootImpl产生,调用mView.dispatchPointerEvent(event)
这个方法将事件分发给视图层。
1 2 3 4 5 6 7 8
| public final boolean dispatchPointerEvent(MotionEvent event) { if (event.isTouchEvent()) { return dispatchTouchEvent(event); } else { return dispatchGenericMotionEvent(event); } }
|
首先映入眼帘的是方法dispatchTouchEvent
,这里我直接先看ViewGroup的这个方法:
1 2 3 4 5 6 7 8 9 10
| public boolean dispatchTouchEvent(MotionEvent ev) { boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
|
再看ViewGroup的dispatchTouchEvent方法的时候,首先所有的核心方法都被包括在了一个判断里面onFilterTouchEventForSecurity
。这个方法意思就是执行一个安全策略,当不符合条件的时候,你可以屏蔽你的应用控件处理这个事件。这是一个公开方法,在自定义view的时候可以重写这个方法。默认这个方法是在当应用不可见的时候才返回false。
这个onInterceptTouchEvent
方法的触发条件:
1 2 3 4 5 6 7 8 9 10 11 12
|
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); } else { intercepted = false; } }
|
当onInterceptTouchEvent
方法返回true的时候,也就是拦截这个事件,交给自己来处理。
1 2 3 4 5 6 7
|
if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, cancel=canceled, child=null, desiredPointerIdBits=TouchTarget.ALL_POINTER_IDS); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) { return false; }
final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); }
if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); }
handled = child.dispatchTouchEvent(transformedEvent); }
transformedEvent.recycle(); return handled; }
|
这里会进入到view的dispatchTouchEvent方法。然后会触发onTouchEvent
方法。
哎整个android应用就是一个递归调用,递归啊,我最害怕的递归啊,整个触摸事件的传递就是一个递归调用的过程。当触发disPatchTouchEvent
就是咱们递归的开始。这里我给你演示下三成view的情况。
首先1触发dispatchTouchEvent方法,当这次是一个按下的事件或者找到了事件传递链。那么就进入自己的事件拦截方法判断是不是要拦截事件。
如果不拦截事件,则遍历全部孩子,这里1的孩子只有2。然后调用孩子2的dispatchTouchEvent方法如果孩子2这个方法返回true。那么就代表找到了这个事件传递链。
整个事件传递其实只需要看两个方法dispatchTouchEvent
和dispatchTransformedTouchEvent
。
dispatchTouchEvent
他的返回值代表这次事件传递是否找到了消费者。找到了消费者就可以为我们的mFirstTouchTarget
设置上值了。最终去处理这个事件的视图的视图他的mFirstTouchTarget的值是null。
现在在回来看下ViewGroup的dispatchTouchEvent:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| public boolean dispatchTouchEvent(MotionEvent ev) { boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); } else { intercepted = false; } } else { intercepted = true; } boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { if (newTouchTarget == null && childrenCount != 0) { for (int i = childrenCount - 1; i >= 0; i--) { if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } } } if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } return handled; }
|
嘿嘿被我这么一删,就可以很清晰的看到整个执行流程了。
首选判断如果是按下事件或者mFirstTouchTarget!=null 那么就执行onInterceptTouchEvent方法判断是否拦截事件。
然后不拦截的话,然后这个事件是一个按下事件,就去遍历所有的孩子执行方法dispatchTransformedTouchEvent里面去调用了孩子的dispatchTouchEvent方法。直到某个dispatchTouchevent方法返回了true。返回true才代表找到了消费这个事件的人。然后我们的mFirstTouchTarget就有值了。然后接着是滑动事件。最先收到事件的当然也是最外层的父容易。然后由于他的mFirstTouchTarget值不为空所以还是会执行他自己的onInterceptTouchEvent判断是否拦截事件。由于这次不是按压事件了。所以直接按着mFirstTouchTarget的调用链执行dispatchTransformedTouchEvent。
我们再来回顾下父视图什么时候会执行onInterceptTouchEvent
方法:
1 2 3 4
| if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { intercepted = onInterceptTouchEvent(ev); }
|
我们可以看到在点击事件的时候,是一定会走拦截方法的。然后还有一个情况就是当mFirstTouchTarget不等于null的时候。那什么时候mFirstTouchTarget不等于null呢?答:在触发点击事件的时候,这个事件在某个子视图的onTouchEvent的down事件返回true或者自己的onTouchEvent在处理down事件的时候返回了true。这样子我们的整整一个事件调用链的mFirstTouchTarget的值就不会为null。既然不会为null了,那么接下来的滑动事件抬起事件,在onInterceptTouchevent里面就能接收到了。为什么我会产生对这个的深入理解,那是因为我在看NestedScrollView
的源码的时候,发现NestedScrollView
只重写了onInterceptTouchEvent
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && mIsBeingDragged) { return true; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { break; }
final int y = (int) ev.getY(pointerIndex); final int yDiff = Math.abs(y - mLastMotionY); if (yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { mIsBeingDragged = true; mLastMotionY = y; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); mNestedYOffset = 0; final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; }
case MotionEvent.ACTION_DOWN: { final int y = (int) ev.getY(); if (!inChild((int) ev.getX(), y)) { mIsBeingDragged = false; recycleVelocityTracker(); break; } mLastMotionY = y; mActivePointerId = ev.getPointerId(0);
initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); mScroller.computeScrollOffset(); mIsBeingDragged = !mScroller.isFinished(); startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH); break; }
case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; recycleVelocityTracker(); if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange())) { ViewCompat.postInvalidateOnAnimation(this); } stopNestedScroll(ViewCompat.TYPE_TOUCH); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; }
return mIsBeingDragged; }
|
可以看到在onInterceptTouchEvent
里面只在拖动事件的时候返回true。那问题来了。如果NestedScrollView的所有孩子都没有对down事件进行返回true的处理的话,那么在NestedScrollView的onInterceptTouchEvent不是只能接收到down事件了,关于move up事件都不会到这里来了。这方面NestedScrollView当然考虑到了,我们可以看NestedScrollView的onTouchEvent事件,他直接返回了一个true。不管孩子他们对事件处不处理,反正到了我这里我全部是要处理的。
1 2 3 4 5 6 7 8 9 10
| public boolean onTouchEvent(MotionEvent ev) { switch (actionMasked) { case MotionEvent.ACTION_DOWN: { if (getChildCount() == 0) { return false; } } } return true }
|
他这么一处理,NestedScrollview就再也不怕他的onInterceptTouchEvent不会执行了。
那现在来分析下NestedScrollView是如何实现滑动的,并且解决嵌套滑动的问题的呢。
关于嵌套滑动,谷歌官方提供了一组类供我们使用。
NestedScrollingChildHelper
NestedScrollingParentHelper
然后还有一组接口:
- NestedScrollingParent
- NestedScrollingParent2
- NestedScrollingParent3
- NestedScrollingChild
- NestedScrollingChild2
- NestedScrollingChild3
他的实现原理就是,父类继承接口NestedScrollingParent。子类继承接口NestedScrollingChild。在滑动的时候,子类主动告诉父类子类想要消耗多少距离,消耗不了的距离告诉父类,从而实现嵌套滑动的功能。使用规则就是在down时间的时候调用ChildHelper.startNestedScroll(axes,type)。然后再滑动的时候调用ChildHelper.dispatchNestedPreScrollView()。然后在UP事件调用ChildHelper.stopNestedScroll()