美文网首页
触摸事件分发阅读笔记

触摸事件分发阅读笔记

作者: 范锦浩 | 来源:发表于2017-07-21 16:22 被阅读29次

    简介

    Android开发中触摸事件是经常用到,除了对基本事件的处理,另外一个很重要的就是事件的分发,事件的分发是指在一个屏幕中那么多的视图,究竟是遵循什么规则分发,从而将整个事件完成
    以下代码均来源与Android的API25的源码

    事件

    首先说一下Android中常用的触摸事件
    ACTION_DOWN:手指按下触摸,一定是某一个事件的开端
    ACTION_MOVE:手指移动
    ACTION_UP:手机松开
    正常来说一个序列会有
    DOWN->UP(手指按下然后松开,类似点击,虽然点击的判断没有那么简单)
    DOWN->MOVE...->MOVE->UP(手指按下然后移动,最后松开)
    等等
    ACTION_POINTER_DOWN:当前已经有一个手指触发了DOWN事件,此时有其它手指按下的时候会触发的事件。
    ACTION_POINTER_UP:当前有手指不再触摸,并且一定还有手指在继续触摸的时候触发的事件。
    getActionIndex:每一个手指触摸到屏幕之后都会分配一个index,这个index会随着手指离开之类的情况变化,但是可以通过index获取当前手指唯一对应的PointerId。
    getPointerId:每一个手指触摸到屏幕之后都会分配一个唯一的id,后续有手指离开之类的话也不会发生变化。

    分发

    在Android中,触摸事件要想到指定的视图上面,那么当手指按下的时候,事件需要层层向下进行分发。而事件首先是来到activity上
    Activity

        /**
         * Activity进行触摸事件的分发
         * @param ev 当前对应的事件
         */
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            //可以看到,将事件的分发交给了window
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;//如果window成功将事件分发下去,则当前事件结束
            }
            //否则事件回到Activity的onTouchEvent(ev)进行处理
            //该方法默认为空,需要自己处理
            return onTouchEvent(ev);
        }
    

    Activity将当前事件分发给了window,
    其中window的实现类为PhoneWindow

        //实际上就是Activity的顶层视图
        private DecorView mDecor;
        
        /**
         * 实际上就是将事件分发给了DecorView
         */
        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return mDecor.superDispatchTouchEvent(event);
        }
    

    PhoneWindow将事件再次分发到DecorView,DecorView是一个Activity的视图的起点,Android的视图结构本身是一个视图树,那么对于一个Activity来说,DecorView就是视图树的根节点。

        public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks
    
        /**
         * 交给父类处理
         */
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
    

    实际上DecorView就是一个FrameLayout,那么这里委托最后就是到了ViewGroup来处理

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            //... 去掉一些代码
            boolean handled = false;
            if (onFilterTouchEventForSecurity(ev)) {
                final int action = ev.getAction();
                final int actionMasked = action & MotionEvent.ACTION_MASK;
    
                // 手指按下事件,也是所有事件的开头
                if (actionMasked == MotionEvent.ACTION_DOWN) {
                    // 还原一些标志,主要是还原Touch链表为null,即mFirstTouchTarget为null
                    cancelAndClearTouchTargets(ev);
                    resetTouchState();
                }
    
                final boolean intercepted;
                //1.当前事件为DOWN事件,可以理解为一次新的事件的开始
                //2.mFirstTouchTarget != null意味着之前DOWN事件已经有子视图处理,现在是非DOWN的后续事件
                //当DOWN事件发下去之后,对于任何ViewGroup来说如果还有子视图(ViewGroup/View)来处理事件
                //那么对于这些ViewGroup来说自身的mFirstTouchTarget必然不会为空,从顶向下看形成了一个事件链条
                //对于后续事件来说,如果没有禁止当前ViewGroup拦截事件,则事件还是会先询问ViewGroup是否拦截
                //也就是说每一个事件都会尝试先问父布局是否拦截事件,也就是说父布局有处理事件的优先权
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {
                    //获得当前ViewGroup是否被禁止尝试拦截事件
                    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                    if (!disallowIntercept) {//允许当前ViewGroup去尝试拦截事件
                        intercepted = onInterceptTouchEvent(ev);//尝试去拦截当前事件
                        ev.setAction(action); //还原事件,避免在onInterceptTouchEvent(ev)过程中事件被修改
                    } else {//当前ViewGroup不拦截事件
                        intercepted = false;
                    }
                } else {//DOWN事件没有子视图处理,非DOWN的事件默认都由当前ViewGroup处理
                    intercepted = true;
                }
                //...
    
                // 标记当前是否为ACTION_CANCEL事件
                final boolean canceled = resetCancelNextUpFlag(this)
                        || actionMasked == MotionEvent.ACTION_CANCEL;
    
                //这个属性默认为true,实际上可以在xml中通过splitMotionEvents设置
                //实际是用于标志当前ViewGroup是否允许将事件分发到两个同级的子视图
                //后面会说到
                final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
                TouchTarget newTouchTarget = null;
                boolean alreadyDispatchedToNewTouchTarget = false;
                if (!canceled && !intercepted) {//当前并非取消事件,并且当前ViewGroup不拦截事件
                    // ...
                    //1.当前是处理DOWN事件
                    //2.当前是否允许分发同级事件
                    //如果不允许,则不会重新查找子视图来接收ACTION_POINTER_DOWN
                    //那么将会把ACTION_POINTER_DOWN交给之前处理了事件的视图
                    //如果允许,会重新查找子视图来接收ACTION_POINTER_DOWN,此时mFirstTouchTarget有可能出现多个节点的情况
                    //ACTION_HOVER_MOVE不考虑,基本没用过
                    if (actionMasked == MotionEvent.ACTION_DOWN
                            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                        final int actionIndex = ev.getActionIndex();
                        //每一个手指的触摸事件的PointerId都是固定的,0-1-2...N
                        //设计的时候通过移位来表示即 0000 .... 0010表示id为1
                        //0000 .... 0111 表示当前手指有3个,id分别为0,1,2
                        //这里如果允许同级分发的话
                        //idBitsToAssign实际上就是用于记录当前事件的PointerId,类似0000 .... 0001
                        //不允许同级,使用默认的-1
                        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                                : TouchTarget.ALL_POINTER_IDS;
                        //将之前TouchTarget链表中有着相同PointerId的节点移除
                        removePointersFromTouchTargets(idBitsToAssign);
                        //获得当前ViewGroup的所有孩子数目
                        final int childrenCount = mChildrenCount;
                        //如果当前有可以分发的子视图
                        if (newTouchTarget == null && childrenCount != 0) {
                            //获取当前DOWN事件的坐标
                            final float x = ev.getX(actionIndex);
                            final float y = ev.getY(actionIndex);
                            //获得一个子视图组,这里的子视图组顺序会影响到那一个子视图可以优先处理DOWN事件
                            //在setChildrenDrawingOrderEnabled的基础上,可以通过getChildDrawingOrder改变映射
                            //具体见buildTouchDispatchChildList
                            final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                            final boolean customOrder = preorderedList == null
                                    && isChildrenDrawingOrderEnabled();
                            final View[] children = mChildren;
                            //倒序遍历,如果没有设置setChildrenDrawingOrderEnabled的情况,默认是按照正序
                            //简单说就是在一个xml中,节点位置越靠后的越先
                            //比方说FrameLayout中有两个全屏的View,那么在xml中后面的View会优先(不考虑Z轴的前提)
                            for (int i = childrenCount - 1; i >= 0; i--) {
                                //之前可能通过setChildrenDrawingOrderEnabled,然后重写了getChildDrawingOrder改变了位置映射
                                //所以后续都要映射回原来View[]的下标
                                final int childIndex = getAndVerifyPreorderedIndex(
                                        childrenCount, i, customOrder);
                                final View child = getAndVerifyPreorderedView(
                                        preorderedList, children, childIndex);
                                //...
    
                                //判断当前view是否可以处理当前事件,
                                //主要是要求当前view可见并且当前事件的坐标在view的范围内
                                if (!canViewReceivePointerEvents(child)
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;//否则继续循环查找下一个可以接收这个事件的view
                                }
                                //当前子视图可以接收当前事件
                                //尝试从当前事件节点链表中查找当前view,链表中不应该重复
                                newTouchTarget = getTouchTarget(child);
                                if (newTouchTarget != null) {
                                    //可能出现DOWN和ACTION_POINTER_DOWN同时由一个子视图处理
                                    //此时复用链表中的一个节点即可
                                    //修改节点对应的PointerId
                                    //类似于0000 .... 0011表示当前视图处理id为0和1的两个手指事件
                                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                                    break;
                                }
    
                                resetCancelNextUpFlag(child);
                                //尝试进行DOWN、ACTION_POINTER_DOWN的事件分发
                                //这里实际上是一个递归调用,将事件交给当前ViewGroup的下一级ViewGroup
                                //然后下一级ViewGroup重新走DispatchTouchEvent进行分发
                                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                    //有视图成功处理了当前事件
    
                                    //... 记录一些DOWN事件的信息
    
                                    //将事件节点添加到链表的头部
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    //标记DOWN事件已经分发给了新的节点处理
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
                            }
                            //...
                        }
                        //上面主要是处理当前ViewGroup不拦截事件,然后分发DOWN事件给子视图的逻辑
                        //如果之前有子视图处理了DOWN事件,同时默认split为true
                        //此时如果是ACTION_POINTER_DOWN事件,但是却没有找到合适的子视图处理
                        if (newTouchTarget == null && mFirstTouchTarget != null) {
                            //这里相当于找到最开始处理DOWN事件的节点
                            newTouchTarget = mFirstTouchTarget;
                            while (newTouchTarget.next != null) {
                                newTouchTarget = newTouchTarget.next;
                            }
                            //标记当前节点处理当前手指,简单理解就是一个手指对应一个id
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                        }
                    }
                }
    
                if (mFirstTouchTarget == null) {
                    //1.当前DOWN事件没有子视图处理,那么最后会到这里由当前ViewGroup处理
                    //形象来说就是把事件从顶向下发送,如果最下面的视图都不处理,那么事件会向上传递
                    //2.当前事件本来就被当前ViewGroup拦截,无论是DOWN还是MOVE之类的事件,mFirstTouchTarget也会一直为null
                    //直接把事件交给当前ViewGroup处理即可
                    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
                } else {//之前事件已经分发给了子视图处理
                    TouchTarget predecessor = null;
                    TouchTarget target = mFirstTouchTarget;
                    //链表在一个手指的前提下基本可以认为只有一个节点,但是有的时候比方说允许split,而且当前DOWN和ACTION_POINTER_DOWN所处理的子视图不同,此时的节点数量就会大于1
                    while (target != null) {
                        final TouchTarget next = target.next;
                        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                            //因为之前专门处理了DOWN/POINTED_DOWN事件,这里可以直接标记事件已经处理
                            handled = true;
                        } else {//到这里的一般都是MOVE、UP之类的事件
                            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                    || intercepted;
                            //在之前DOWN事件已经分发给了指定的子视图的前提下
                            //1.如果当前ViewGroup要求拦截事件
                            //假设现在有一个MOVE事件到来,但是当前ViewGroup要拦截MOVE事件
                            //这意味这事件序列不能交给之前的子视图处理
                            //此时向之前处理事件的子视图分发一个cancel事件,告诉它这个事件序列被取消了
                            //此时子视图的事件序列完成
                            //2.当前ViewGroup不拦截事件
                            //当前事件直接交给上一次处理事件的子视图即可
                            //这里会通过一直向下分发起到最终到之前的子视图
                            if (dispatchTransformedTouchEvent(ev, cancelChild,
                                    target.child, target.pointerIdBits)) {
                                handled = true;
                            }
                            //因为当前ViewGroup拦截当前事件,导致子视图的事件被取消
                            //此时回收节点,意味着mFirstTouchTarget链表为null,事件会给当前ViewGroup处理
                            if (cancelChild) {
                                if (predecessor == null) {
                                    mFirstTouchTarget = next;
                                } else {
                                    predecessor.next = next;
                                }
                                target.recycle();
                                target = next;
                                continue;
                            }
                        }
                        predecessor = target;
                        target = next;
                    }
                }
                if (canceled
                        || actionMasked == MotionEvent.ACTION_UP
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    //当前事件已经取消、或者是ACTION_UP
                    //还原状态,主要是清空TouchTarget链表等操作
                    resetTouchState();
                } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                    //当前允许同级分发,此时为ACTION_POINTER_UP事件,意味着有一个手指离开
                    //此时将该手指对应的链表节点移除,后续不应该处理其它事件
                    final int actionIndex = ev.getActionIndex();
                    final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                    removePointersFromTouchTargets(idBitsToRemove);
                }
            }
            //...
            return handled;
        }
    

    1.首先可以看到对于每一个ViewGroup来说都会有一个TouchTarget链表,这个是用来存储当前ViewGroup确定分发事件的下一级视图(ViewGroup或者View),如果只是单纯的单指操作,那么TouchTarget只会有一个节点,如果有多指操作,并且接收的子视图不一样,此时会有多个节点。每一个ViewGroup都有一个链表,从顶往下看就可以形成一个事件传递链表。
    2.可以通过在xml中设置splitMotionEvents="false"来禁止多个同级视图处理事件,常用的场景就是一个页面,有时候不希望有人同时点击两个地方,导致同时拉起两个页面或者出现一些不可预知问题。
    3.对于简单的单指操作来说,事件本身可以看做一个序列,只要有一个子视图处理了DOWN事件,后续事件都会交给它继续处理,但是每一个事件是否可以到达子视图,要看当前ViewGroup是否拦截事件,如果ViewGroup拦截了事件,则子视图只会收到一个ACTION_CANCEL事件,然后事件重新开始。
    4.可以通过requestDisallowInterceptTouchEvent禁止ViewGroup拦截事件,不过注意这个标志每一次DOWN事件开始分发的时候都会被重置。

        /**
         * getAndVerifyPreorderedIndex的实际执行方法
         * 获得当前ViewGroup的视图组
         * 后面默认是倒序遍历,主要用于确认哪一个视图优先可以尝试处理事件
         */
        ArrayList<View> buildOrderedChildList() {
            final int childrenCount = mChildrenCount;
            if (childrenCount <= 1 || !hasChildWithZ()) return null;
    
            if (mPreSortedChildren == null) {
                mPreSortedChildren = new ArrayList<>(childrenCount);
            } else {
                // callers should clear, so clear shouldn't be necessary, but for safety...
                mPreSortedChildren.clear();
                mPreSortedChildren.ensureCapacity(childrenCount);
            }
            //这个可以通过setChildrenDrawingOrderEnabled设置,默认false
            final boolean customOrder = isChildrenDrawingOrderEnabled();
            for (int i = 0; i < childrenCount; i++) {
                //如果customOrder为true,这里可以通过重写ViewGroup的getChildDrawingOrder来改变接收事件的视图顺序
                //默认就是i
                final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                //拿出当前视图
                final View nextChild = mChildren[childIndex];
                //在Android5.0之后提出的Z轴概念
                final float currentZ = nextChild.getZ();
    
                //无论是否重新映射了下标位置,但是都会按照Z轴排序
                //简单说就是Z坐标越大的在列表的位置越后
                //列表是按照Z坐标的升序排列
                int insertIndex = i;
                while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
                    insertIndex--;
                }
                mPreSortedChildren.add(insertIndex, nextChild);
            }
            return mPreSortedChildren;
        }
    

    可以通过setChildrenDrawingOrderEnabled和重写getChildDrawingOrder来改变优先尝试处理事件的位置,比方说ViewA的index为0,ViewB的index为1,此时事件同时在ViewA和ViewB的区域,事件默认会先到ViewB,如果要到ViewA,可以倒序映射。不过实际上这两个参数就是用于改变绘制的顺序,传统的是index小的先画即ViewA,然后ViewB可能会盖住它,此时事件先到ViewB也是正常,如果颠倒绘制顺序,那么就会先画ViewB,ViewA会盖住ViewB,此时事件先到ViewA也是正常的。
    之前提到事件的分发

    /**
         * 分发指定的事件给指定的视图
         * @param event 当前应该被分发的事件
         * @param cancel 当前事件是否被取消
         * @param child 当前应该被分发事件的子视图,null表示当前ViewGroup处理
         * @param desiredPointerIdBits 当前事件的id
         * @return true表示当前事件分发完成,并且被处理,false表示没视图处理当前事件
         */
        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) {//由当前ViewGroup处理
                    handled = super.dispatchTouchEvent(event);
                } else {//将事件交由子视图处理
                    handled = child.dispatchTouchEvent(event);
                }
                event.setAction(oldAction);
                return handled;
            }
    
            // 获得当前有多少个手指触摸中,并且可以获得对应id
            // 比方0000 .... 0111表示3个手指,其中ID为0,1,2
            final int oldPointerIdBits = event.getPointerIdBits();
            //与当前事件id
            //比方说当前事件id为1,即0000 .... 0010
            //得到就是0000 .... 0010
            //除非当前事件id不在记录的触摸点中,此时会得到0
            //对于一个手指的事件来说,oldPointerIdBits总是等于newPointerIdBits
            final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
    
    
            //因为一些原因可能产生了一个没有触摸点对应的事件
            //丢弃当前事件
            if (newPointerIdBits == 0) {
                return false;
            }
    
            // If the number of pointers is the same and we don't need to perform any fancy
            // irreversible transformations, then we can reuse the motion event for this
            // dispatch as long as we are careful to revert any changes we make.
            // Otherwise we need to make a copy.
            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 {
                //当前有多个手指的触摸中,此时要转换事件
                //比方说在Split模式下,POINTED_DOWN和POINTED_UP在某一个视图中会被转换为DOWN和UP
                //从而触发点击事件等等
                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;
        }
    

    最终处理事件的时候会到super.dispatchTouchEvent,其实就是View的dispatchTouchEvent方法

    public boolean dispatchTouchEvent(MotionEvent event) {
            //...
            boolean result = false;
            if (onFilterTouchEventForSecurity(event)) {
                //...
                //首先回调了OnTouchListener
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
                //如果OnTouchListener返回true
                //不会继续调用OnTouchEvent
                if (!result && onTouchEvent(event)) {
                    //实际上默认的OnClickListener是在默认的onTouchEvent里面处理的
                    result = true;
                }
            }
            //...
    
            return result;
        }
    

    可以看到,实际上处理事件的时候,如果通过setOnTouchListener设置了OnTouchListener,则优先回调OnTouchListener,如果OnTouchListener处理后返回true,则当前事件处理完成。
    否则继续onTouchEvent方法,其中onTouchEvent方法里面默认实现了onClickListener的回调。

    结语

    可以简单的总结Android中的事件传递成几个规则
    1.事件是序列化的,即一般情况下,DOWN先开始,然后是MOVE或者UP等操作,这就是一个完整序列。
    2.每一个事件父布局总是有优先拦截权利的,子视图可以通过requestDisallowInterceptTouchEvent禁止父布局拦截事件,合理利用这些就可以处理大部分手势冲突。
    3.当事件到一个视图处理的时候onTouchListener>onTouchEvent>onClickListener

    相关文章

      网友评论

          本文标题:触摸事件分发阅读笔记

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