大领导又给小明安排任务——Android触摸事件

这是Android触摸事件系列的第二篇,系列文章目录如下:

  1. 大领导给小明安排任务——Android触摸事件
  2. 大领导又给小明安排任务——Android触摸事件

把上一篇中领导分配任务的故事,延展一下:

大领导安排任务会经历一个“递”的过程:大领导先把任务告诉小领导,小领导再把任务告诉小明。也可能会经历一个“归”的过程:小明告诉小领导做不了,小领导告诉大领导任务完不成。然后,就没有然后了。。。。但如果这次完成了任务,大领导还会继续将后序任务分配给小明。

故事的延展部分和今天要讲的ACTION_DONW后序事件很类似,先来回答上一篇中遗留的另一个问题“拦截事件”:

拦截事件

ViewGroup在遍历孩子分发触摸事件前还有一段拦截逻辑:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            ...
            // Check for interception.
            //检查ViewGroup是否要拦截触摸事件的下发
            final boolean intercepted;
            //第一个条件表示拦截ACTION_DOWN事件
            //第二个条件表示拦截ACTION_DOWN事件已经分发给孩子,现在拦截后序事件
            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;
            }
            ...
            //当事件没有被拦截的时候,将其分发给孩子
            if (!canceled && !intercepted) {
                //遍历孩子并将事件分发给它们
                //如果有孩子声称要消费事件,则将其添加到触摸链上
                //这段逻辑在上一篇中分析过,这里就省略了
            }
        }
        
        //将触摸事件分发给触摸链
        if (mFirstTouchTarget == null) { //没有触摸链 
            //如果事件被ViewGroup拦截,则触摸链为空,ViewGroup自己消费事件
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        } else {
            ...
        }
    }
    
    //返回true表示拦截事件,默认返回false
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
        ...
        if (child == null) {
            //ViewGroup孩子都不愿意接收触摸事件或者触摸事件被拦截 则其将自己当成View处理(调用View.dispatchTouchEvent())
            handled = super.dispatchTouchEvent(transformedEvent);
        }
        ...
    }
}
复制代码

当允许拦截时,onInterceptTouchEvent()会被调用,如果重载这个方法并且返回true,表示ViewGroup要对事件进行拦截,此时不再将事件分发给孩子而是自己消费(通过调用View.dispatchTouchEvent()最终走到ViewGroup.onTouchEvent())。

用一张图总结一下:

图1

  • 图中黑色的箭头表示触摸事件传递的路径,灰色的箭头表示触摸事件消费的回溯路径。onInterceptTouchEvent()返回true,导致onTouchEvent()被调用,因为onTouchEvent()返回true,导致dispatchTouchEvent()返回true
  • 准确的说,拦截触摸事件的受益者是所有上层的ViewGroup(包括自己),因为触摸事件不再会向下层的View传递。

ACTION_MOVE和ACTION_UP

上一篇在阅读源码的时候,埋下了一个伏笔,现在将其补全:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    //触摸链头结点
    private TouchTarget mFirstTouchTarget;
    ...
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!canceled && !intercepted) {
            ...
            //当ACTION_DOWN的时候才遍历寻找消费触摸事件的孩子,若找到则将其加入到触摸链
            if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //遍历孩子
                for (int i = childrenCount - 1; i >= 0; i--) {
                    ...
                    //转换触摸坐标并分发给孩子(child参数不为null)
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                          ...
                          //有孩子愿意消费触摸事件,将其插入“触摸链”
                          newTouchTarget = addTouchTarget(child, idBitsToAssign);
                          //表示已经将触摸事件分发给新的触摸目标
                          alreadyDispatchedToNewTouchTarget = true;
                          break;
                    }
                     ...
                }
            }
        }
    
        if (mFirstTouchTarget == null) {
                //如果没有孩子愿意消费触摸事件,则自己消费(child参数为null)
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        } 
        //触摸链不为null,表示有孩子消费了ACTION_DOWN
        else {
                //将伏笔补全
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                //遍历触摸链将ACTION_DOWN的后序事件分发给孩子
                while (target != null) {
                    final TouchTarget next = target.next;
                    //上一篇分析了,ACTION_DOWN会走这里
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        //如果已经将触摸事件分发给新的触摸目标,则返回true
                        handled = true;
                    } 
                    //ACTION_DONW的后序事件走这里
                    else {
                        ...
                        //将触摸事件分发给触摸链上的触摸目标
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        ...
                    }
                    predecessor = target;
                    target = next;
                }
        }
        ...
        if (canceled
            || actionMasked == MotionEvent.ACTION_UP
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //如果是ACTION_UP事件,则将触摸链清空
                resetTouchState();
        }

        return handled;
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        ...
        // Perform any necessary transformations and dispatch.
        //进行必要的坐标转换然后分发触摸事件
        if (child == null) {
            //ViewGroup孩子都不愿意消费触摸事件 则其将自己当成View处理(调用View.dispatchTouchEvent())
            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);
        }
        ...
        return handled;
    }
    
    /**
     * Resets all touch state in preparation for a new cycle.
     * 重置Touch标志
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    
    /**
     * Clears all touch targets.
     * 清空触摸链
     */
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }
}
复制代码

触摸事件是一个序列,序列总是以ACTION_DOWN开始,紧接着有ACTION_MOVEACTION_UPACTION_DOWN发生时,ViewGroup.dispatchTouchEvent()会将愿意消费触摸事件的孩子存储在触摸链中,当后序事件会分发给触摸链上的对象。

用一种图总结一下:

图2

  • 图中黑色箭头表示ACTION_DOWN事件的传递路径,灰色箭头表示ACTION_MOVEACTION_UP事件的传递路径。即只要有视图声称消费ACTION_DOWN,则其后序事件也传递给它,不管它是否声称消费ACTION_MOVEACTION_UP,如果它不消费,则后序事件会像上一篇分析的ACTION_DOWN一样向上回溯给上层消费。

图3

  • 图中黑色箭头表示ACTION_DOWN事件的传递路径,灰色箭头表示ACTION_MOVEACTION_UP事件的传递路径。即所有视图都不消费ACTION_DOWN,则其后序事件只会传递给Activity.onTouchEvent()

ACTION_CANCEL

把领导布置任务的故事继续延展一下:大领导给小领导布置了任务1,小领导把他传递给小明,小明完成了。紧接着大领导给小领导布置了任务2,小领导决定自己处理任务2,于是他和小明说后序任务我来接手,你可以忙别的事情。

故事对应的触摸事件传递场景是:ActivityACTION_DOWN传递给ViewGroupViewGroup将其传递给ViewView声称消费ACTION_DOWNActivity继续将ACTION_MOVE传递给ViewGroup,但ViewGroup对其做了拦截,此时ViewGroup会发送ACTION_CANCEL事件给View

看下源码:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //检查ViewGroup是否要拦截触摸事件的下发
        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;
                }
        }
        ...
        //如果孩子消费ACTION_DOWN事件,则会在这里将其添加到触摸链中
        if (!canceled && !intercepted) {
            ...
        }
        //将触摸事件分发给触摸链
        if (mFirstTouchTarget == null) { //没有触摸链 表示当前ViewGroup中没有孩子愿意接收触摸事件
            //将触摸事件分发给自己
        } else {
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            //遍历触摸链分发触摸事件给所有想接收的孩子
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    //如果事件被拦截则cancelChild为true
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    //将ACTION_CANCEL事件传递给孩子
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    //如果发送了ACTION_CANCEL事件,将孩子从触摸链上摘除
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
        ...
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don‘t need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                //将ACTION_CANCEL事件传递给孩子
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
    }
    ...
}
复制代码

当孩子消费了ACTION_DOWN事件,它的引用被会保存在父亲的触摸链中。当父亲拦截后序事件时,父亲会向触摸链上的孩子发送ACTION_CANCEL事件,并将孩子从触摸链上摘除。后序事件就传递到父亲为止。

总结

经过两篇文章的分析,对Android触摸事件的分发有了初步的了解,得出了以下结论:

  • Activity接收到触摸事件后,会传递给PhoneWindow,再传递给DecorView,由DecorView调用ViewGroup.dispatchTouchEvent()自顶向下分发ACTION_DOWN触摸事件。
  • ACTION_DOWN事件通过ViewGroup.dispatchTouchEvent()DecorView经过若干个ViewGroup层层传递下去,最终到达View
  • 每个层次都可以通过在onTouchEvent()OnTouchListener.onTouch()返回true,来告诉自己的父控件触摸事件被消费。只有当下层控件不消费触摸事件时,其父控件才有机会自己消费。
  • 触摸事件的传递是从根视图自顶向下“递”的过程,触摸事件的消费是自下而上“归”的过程。
  • ACTION_MOVEACTION_UP会沿着刚才ACTION_DOWN的传递路径,传递给消费了ACTION_DOWN的控件,如果该控件没有声明消费这些后序事件,则它们也像ACTION_DOWN一样会向上回溯让其父控件消费。
  • 父控件可以通过在onInterceptTouchEvent()返回true来拦截事件向其孩子传递。如果在孩子已经消费了ACTION_DOWN事情后才进行拦截,父控件会发送ACTION_CANCEL给孩子。

猜你喜欢

转载自juejin.im/post/5c7c7b13f265da2d993da374