美文网首页Android开发经验谈Android技术知识Android开发
大领导又给小明安排任务——Android触摸事件

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

作者: 唐子玄 | 来源:发表于2019-03-04 16:39 被阅读12次

    这是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给孩子。

    相关文章

      网友评论

        本文标题:大领导又给小明安排任务——Android触摸事件

        本文链接:https://www.haomeiwen.com/subject/eydbuqtx.html