序言
MotionEvent 的分发机制
流程图
- dispatchTouchEvent()
- onInterceptTouchEvent()
- onTouchEvent()
序言
View 的分发机制是比较复杂的一块机制, 在日常开发中也遇到很多与 view 分发机制有关的问题. 所以抽空总结下 view 的分发机制.
image.png
MotionEvent 的分发机制
用户的触摸和点击事件对应的对象类型就是 MotionEvent,view 的事件分发过程就是对 MotionEvent 的分发和消费过程, 而在这个过程中主要涉及到了三个方法, 分别是 dispatchTouchEvent().onInterceptTouchEvent(),onTouchEvent().
- dispatchTouchEvent()
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (mInputEventConsistencyVerifier != null) {
- mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
- }
- // If the event targets the accessibility focused view and this is it, start
- // normal event dispatch. Maybe a descendant is what will handle the click.
- if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
- ev.setTargetAccessibilityFocus(false);
- }
- boolean handled = false;
- if (onFilterTouchEventForSecurity(ev)) {
- final int action = ev.getAction();
- final int actionMasked = action & MotionEvent.ACTION_MASK;
- // Handle an initial down.
- if (actionMasked == MotionEvent.ACTION_DOWN) {
- // Throw away all previous state when starting a new touch gesture.
- // The framework may have dropped the up or cancel event for the previous gesture
- // due to an app switch, ANR, or some other state change.
- cancelAndClearTouchTargets(ev);
- resetTouchState();
- }
- // Check for interception.
- 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); // restore action in case it was changed
- } else {
- intercepted = false;
- }
- } else {
- // There are no touch targets and this action is not an initial down
- // so this view group continues to intercept touches.
- intercepted = true;
- }
- ......
dispatchTouchEvent()是整个 view 分发流程的入口. MotionEvent 在 dispatchTouchEvent()中的流程为如下:
首先会由 Activity 的 dispatchTouchEvent() 方法来处理. 这个方法比较简短也好理解, 会调用 Activity 所属的 Window 对象的 dispatchTouchEvent()方法.
- public boolean dispatchTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- onUserInteraction();
- }
- if (getWindow().superDispatchTouchEvent(ev)) {
- return true;
- }
- return onTouchEvent(ev);
- }
而在 PhoneWindow 这个 Window 的具体实现类中, 调用了 mDecor 的 dispatchTouchEvent(event)方法, mDecor 是 Activity.setContentView()设置的顶层 view, 也就是从这个地方开始 motionEvent 开始了在 view 树中的分发过程.
- public boolean superDispatchTouchEvent(MotionEvent event){
- return mDecor.superDispatchTouchEvent(event);
- }
ViewGroup 的 dispatchToucheEvent()过程可以分为两部分:
判断是否拦截 motionEvent 以及调用 oninterceptTouchEvent()处理事件
决定究竟哪个子 view 来接收没有被父 view 拦截的 MotionEvent()事件.
在拦截 MotionEvent 的时候, 这里有一个逻辑需要注意: 如果子 view 声明了 disallowIntercept 标志位 (顾名思义就是不允许父 view 拦截 motionEvent), 父 view 将不会再调用 interceptTouchEvent() 处理 ACTION_UP 和 ACTION_MOVE, 而是直接交给子 view 处理但如果是 ACTION_DOWN 则不会这样处理, 因为当 ACTION_DOWN 事件到来时这个标志位总是会被重置还有一种情况就是如果 mFirstTouchTarget 不为空的时候, 而 mFirstTouchTarget 代表的就是处理上一个触摸事件的子 view.
- // Check for interception.
- 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); // restore action in case it was changed
- } else {
- intercepted = false;
- }
- } else {
- // There are no touch targets and this action is not an initial down
- // so this view group continues to intercept touches.
- intercepted = true;
- }
在处理完 intercept 标志位之后, 接着会遍历子 view 来寻找处理触摸事件的 view.
这里会将子 view 整理为一个队列, 而给子 view 排序的依据是子 view 加入到父 view 中的顺序, 即先加入的子 view 会排在队列前边. 然后会遍历这个子 view 集合来找出符合分发条件的子 view: 这个条件就是 view 可见或 view.getAnimation()不为空并且触摸事件的落点正好在子 view 中. 如果找到了这样的一个子 view, 那么触摸事件就会交给它处理, 如果没有找到则会将触摸事件交给上一个子 view, 如果没有上一个处理触摸事件的子 view, 那么触摸事件则会交给父 view 来处理.
- {
- final float x = ev.getX(actionIndex);
- final float y = ev.getY(actionIndex);
- // Find a child that can receive the event.
- // Scan children from front to back.
- final ArrayList<View> preorderedList = buildTouchDispatchChildList();
- final boolean customOrder = preorderedList == null
- && isChildrenDrawingOrderEnabled();
- final View[] children = mChildren;
- for (int i = childrenCount - 1; i >= 0; i--) {
- final int childIndex = getAndVerifyPreorderedIndex(
- childrenCount, i, customOrder);
- final View child = getAndVerifyPreorderedView(
- preorderedList, children, childIndex);
- // If there is a view that has accessibility focus we want it
- // to get the event first and if not handled we will perform a
- // normal dispatch. We may do a double iteration but this is
- // safer given the timeframe.
- if (childWithAccessibilityFocus != null) {
- if (childWithAccessibilityFocus != child) {
- continue;
- }
- childWithAccessibilityFocus = null;
- i = childrenCount - 1;
- }
- if (!canViewReceivePointerEvents(child)
- || !isTransformedTouchPointInView(x, y, child, null)) {
- ev.setTargetAccessibilityFocus(false);
- continue;
- }
- newTouchTarget = getTouchTarget(child);
- if (newTouchTarget != null) {
- // Child is already receiving touch within its bounds.
- // Give it the new pointer in addition to the ones it is handling.
- newTouchTarget.pointerIdBits |= idBitsToAssign;
- break;
- }
- resetCancelNextUpFlag(child);
- if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
- // Child wants to receive touch within its bounds.
- mLastTouchDownTime = ev.getDownTime();
- if (preorderedList != null) {
- // childIndex points into presorted list, find original index
- for (int j = 0; j < childrenCount; j++) {
- if (children[childIndex] == mChildren[j]) {
- mLastTouchDownIndex = j;
- break;
- }
- }
- } else {
- mLastTouchDownIndex = childIndex;
- }
- mLastTouchDownX = ev.getX();
- mLastTouchDownY = ev.getY();
- newTouchTarget = addTouchTarget(child, idBitsToAssign);
- alreadyDispatchedToNewTouchTarget = true;
- break;
- }
- // The accessibility focus didn't handle the event, so clear
- // the flag and do a normal dispatch to all children.
- ev.setTargetAccessibilityFocus(false);
- }
- if (preorderedList != null) preorderedList.clear();
- }
- View.onDispatchTouchEvent()
在经历了父 View 的 onDispatchTouchEvent()后, 会进入到选定的子 View 的 (这里一般为 View) 的 onDispatchTouchEvent()中.
这里首先会调用 view 的 onTouchListener, 如果 onTouch()返回 true, 那么 onTouchEvent()方法则不会被调用.
- if (onFilterTouchEventForSecurity(event)) {
- if ((mViewFlags & ENABLED_MASK) == ENAB*LED && handleScrollBarDragging(event)) {
- result = true;
- }
- //noinspection SimplifiableIfStatement
- ListenerInfo li = mListenerInfo;
- if (li != null && li.mOnTouchListener != null
- && (mViewFlags & ENABLED_MASK) == ENABLED
- && li.mOnTouchListener.onTouch(this, event)) {
- result = true;
- }
- if (!result && onTouchEvent(event)) {
- result = true;
- }
- }
至此, onDispatchTouchEvent()的流程就结束了.
总结几个结论:
MotionEvent 是必定会经过 onDispatchTouchEvent()方法的, 但不保证会经过 onInterceptTouchEvent()和 onTouchEvent();
Activity 是 MotionEvent 的分发的起点和终点, 所有的事件会首先交给 Activity 处理, 如果最终没有 view 处理的话, 事件会重新交还给 Activity 来处理.
调用 requestDisAllowIntercept()方法可以禁止父 view 拦截点击事件, 但除了 ACTION_DOWN 事件.
- onInterceptTouchEvent()
- /**
- * Implement this method to intercept all touch screen motion events. This
- * allows you to watch events as they are dispatched to your children, and
- * take ownership of the current gesture at any point.
- *
- * <p>Using this function takes some care, as it has a fairly complicated
- * interaction with {@link View#onTouchEvent(MotionEvent)
- * View.onTouchEvent(MotionEvent)}, and using it requires implementing
- * that method as well as this one in the correct way. Events will be
- * received in the following order:
- *
- * <ol>
- * <li> You will receive the down event here.
- * <li> The down event will be handled either by a child of this view
- * group, or given to your own onTouchEvent() method to handle; this means
- * you should implement onTouchEvent() to return true, so you will
- * continue to see the rest of the gesture (instead of looking for
- * a parent view to handle it). Also, by returning true from
- * onTouchEvent(), you will not receive any following
- * events in onInterceptTouchEvent() and all touch processing must
- * happen in onTouchEvent() like normal.
- * <li> For as long as you return false from this function, each following
- * event (up to and including the final up) will be delivered first here
- * and then to the target's onTouchEvent().
- * <li> If you return true from here, you will not receive any
- * following events: the target view will receive the same event but
- * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
- * events will be delivered to your onTouchEvent() method and no longer
- * appear here.
- * </ol>
- *
- * @param ev The motion event being dispatched down the hierarchy.
- * @return Return true to steal motion events from the children and have
- * them dispatched to this ViewGroup through onTouchEvent().
- * The current target will receive an ACTION_CANCEL event, and no further
- * messages will be delivered here.
- */
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
- && ev.getAction() == MotionEvent.ACTION_DOWN
- && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
- && isOnScrollbarThumb(ev.getX(), ev.getY())) {
- return true;
- }
- return false;
- }
ViewGroup 的 onInterceptTouchEvent()默认返回 false, 也就是默认不拦截任何点击事件, 通过 javadoc 注释可以知道一旦拦截后, 当前的 view 对象会收到 ACTION_CANCEL 事件, 接着之后所有的 MotionEvent 都会被交给当前 view 的 onTouchEvent()处理, 而不会再经过 interceptTouchEvent().
安卓开发艺术探索中是这样形容这个过程的, 我觉得非常形象: View 体系中的事件分发就好像领导把一个开发任务向下指派给开发人员的过程一样.
如果这个任务的开始部分被指派给了某个开发人员, 那么后续的开发任务都会被指派给他
相反如果这个任务如果底层的开发人员无法胜任, 那么自然而然地它就会被向上传递给上一层的其它同事, 这样层层上传直到回到开始分发任务的领导(没人能解决就只能领导自己背锅了).
onTouchEvent()
onTouchEvent()回调方法会具体地处理点击事件, 代码如下
在 enable 状态下, 仍然会处理和消费事件, 只是不会再执行 onTouchEvent()方法后面的逻辑, onTouchListener 中的回调也无法得到执行.
- if ((viewFlags & ENABLED_MASK) == DISABLED) {
- if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
- setPressed(false);
- }
- mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
- // A disabled view that is clickable still consumes the touch
- // events, it just doesn't respond to them.
- return clickable;
- }
下面的这段逻辑用来处理开发者设置的 onClickListener 中的点击事件. 主要有两点逻辑:
只有处于 pressed 状态的才能执行这段代码
只有当 view 同时满足 isFocusable(),isFocusableInTouchMode(), 且 isFocused()为 false, 才会去执行获取焦点的代码; 如果没有获取到焦点, 则不会调用 performClick()执行 onClick()事件.
- if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
- // take focus if we don't have it already and we should in
- // touch mode.
- boolean focusTaken = false;
- if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
- focusTaken = requestFocus();
- }
- . . .
- // Only perform take click actions if we were in the pressed state
- if (!focusTaken) {
- // Use a Runnable and post this rather than calling
- // performClick directly. This lets other visual state
- // of the view update before click actions start.
- if (mPerformClick == null) {
- mPerformClick = new PerformClick();
- }
- if (!post(mPerformClick)) {
- performClick();
- }
- }
以上.
来源: http://www.jianshu.com/p/c9e20e8ccd82