美文网首页触摸事件安卓
Android事件分发机制--拿来吧你

Android事件分发机制--拿来吧你

作者: Elfkind | 来源:发表于2021-08-02 08:46 被阅读0次

    网上关于Android事件分发机制的资料有许多,看过很多次,但是每次过一段时间就会忘记,感觉还是自己研究的不够深入,这一次,决定自己根据源码,来好好梳理一遍Android事件分发机制的知识,本文绝对的简单易懂,不像其他博客一样,上来就画事件分发流程图,完全看不懂呀有没有。现在将自己的理解分享出来希望对初学者有所帮助,只要你耐心看下去,定有收获。
    注:文中源码基于android-26,版本略有差异,但大致流程是一致的。

    前言

    基本知识介绍。

    1.事件分发的”事件“是指什么?

    即Touch事件,触摸过程中产生的一系列Touch事件被封装成一组事件列,并由底层向上传递到View。所谓的事件列,即指从手指接触屏幕至手指离开屏幕这个过程产生的一系列事件。一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件。

    2.事件分发的本质

    事件分发机制,总的来说就是对一次屏幕触摸事件的响应过程,从触摸行为开始,到触摸行为结束,将点击事件(MotionEvent)传递到某个具体的View & 处理的整个过程。

    3.事件列有哪些事件类型

    事件分发过程涉及到的事件类型包括:

    ACTION_DOWN 按下屏幕时,触发此事件,只会触发一次
    ACTION_MOVE 移动手指时,触发此事件,会多次触发
    ACTION_UP 手指离开屏幕时,触发此事件,只会触发一次
    ACTION_CANCEL 取消事件时,触发此事件,只会触发一次

    对于ACTION_CANCEL事件的触发时机,会在后面给出,这里先简要介绍下。当当前view消费了ACTION_DOWN之后,在后续的ACTION_MOVE过程中,调用了requestDisallowInterceptTouchEvent(false),使得父容器拦截了事件,这时候,就会先将当前这个ACTION_MOVE转变成ACTION_CANCEL事件,发送给之前消费ACTION_DOWN事件的View,同时会将mFirstTouchTarget置为null,这样的话,在下一个ACTION_MOVE到来时,就不会走拦截和分发流程,直接将ACTION_MOVE事件交给父容器处理,这也是事件冲突处理的主要理论知识。

            //第一部分,判断事件是否需要拦截,disallowIntercept的值可由子view调用requestDisallowInterceptTouchEvent
            //来改变,可以看出子类调用requestDisallowInterceptTouchEvent(false)时,disallowIntercept置为false,
            //允许父容器拦截,onInterceptTouchEvent返回true时,intercepted = true,
            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;
            }
            //...省略...
            final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
            //intercepted为true时,cancelChild为true,dispatchTransformedTouchEvent调用时,传入true
            if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
                handled = true;
            }
            //消费ACTION_CANCEL之后,将mFirstTouchTarget 置空,因此在后面的MOVE事件来到时,不会走入第一部分的拦截判断
            //会走入else分支,intercepted = true;将后续MOVE交给自己处理。
            if (cancelChild) {
              mFirstTouchTarget 置空
            }
            //cancel为true
        private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;
    
            // cancel为true时,event.setAction(MotionEvent.ACTION_CANCEL),将MOVE事件变成ACTION_CANCEL事件。
            // child为消费ACTION_DOWN的view。child不为空,即消费ACTION_CANCEL。
            // 需要注意的是,手指移动时ACTION_MOVE会多次触发,这里消费的是当前转换来的MOVE事件,
            // 当下一个MOVE事件来的时候,mFirstTouchTarget已经为空了,所以会将后续的ACTION_MOVE事件交给父容器处理。
            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);
                }
                //转换为CANCEL事件并消费后,重新设置回ACTION_MOVE类型。
                event.setAction(oldAction);
                return handled;
            }
         }
    

    4.事件分发涉及到的类和方法。

    4.1 事件分发由哪些对象完成

    先来看一下Activity的大体层级。事件分发过程实际上就是Activity、ViewGroup、View对MotionEvent事件的分发,拦截,消费的过程。


    在这里插入图片描述

    其中,只有ViewGroup能进行事件拦截,因此只有ViewGroup有onInterceptTouchEvent方法。

    Activity 拥有dispathTouchEvent和onTouchEvent方法
    ViewGroup 拥有dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent
    View 拥有dispathTouchEvent和onTouchEvent方法

    4.2 事件分发由哪些方法完成

    dispatchTouchEvent(MotionEvent ev) 事件分发
    onInterceptTouchEvent(MotionEvent ev) 事件拦截
    onTouchEvent(MotionEvent event) 事件消费

    三个方法都有三种返回类型,true、false、super.xxx,不同返回值,触发的流程会有所差异,在后面会进行详细的分析。

    触摸事件是如何传递到Activity的?

    1.首先,当我们触摸屏幕时,通过Android消息机制,从Looper从MessageQueue中取出该事件,发送给WindowInputEventReceiver。
    2.WindowInputEventReceiver是ViewRootImpl的内部类,通过enqueueInputEvent方法,将输入事件加入输入事件队列中,并进行处理和转发。
    3.ViewPostImeInputStage收到输入事件,将事件传递给DecorView的dispatchPointerEvent()方法(是View的方法)
    4.dispatchPointerEvent()方法通过DecorView中的dispatchTouchEvent()方法,通过回调,调用了Activity的dispatchTouchEvent()方法。
    到此事件进入Activity中!

    一个事件的整体流程:

    ViewRootImpl.processPointerEvent--->DecorView.dispatchTouchEvent--->Activity.dispatchTouchEvent--->PhoneWindow.superDispatchTouchEvent--->DecorView.superDispatchTouchEvent--->ViewGroup.dispatchTouchEvent--->view...
    具体流程如下图,想详细了解的,可参考文章:原来Android触控机制竟是这样的?https://www.jianshu.com/p/b7cef3b3e703

    事件传递流程图.png

    触摸事件是如何从Activity一层层传到View的?

    上面我们知道了系统是如何将触摸行为封装成MotionEvent事件,并传递给Activity中去的,下面介绍下,系统如何将事件由Activity一步步传到我们的View中去的。我们知道,Activity的dispatchTouchEvent方法,将事件传给window,而window是在Activity调用attach方法的时候,创建的,唯一实现子类是PhoneWindow。

        //Activity
        final void attach(Context context, ActivityThread aThread,
                Instrumentation instr, IBinder token, int ident,
                Application application, Intent intent, ActivityInfo info,
                CharSequence title, Activity parent, String id,
                NonConfigurationInstances lastNonConfigurationInstances,
                Configuration config, String referrer, IVoiceInteractor voiceInteractor,
                Window window, ActivityConfigCallback activityConfigCallback) {
            attachBaseContext(context);
    
            //....省略...
    
            mWindow = new PhoneWindow(this, window, activityConfigCallback);
            mWindow.setWindowControllerCallback(this);
            mWindow.setCallback(this);
            mWindow.setOnWindowDismissedCallback(this);
            mWindow.getLayoutInflater().setPrivateFactory(this);
            //....省略...
        }
        //分发事件
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            return onTouchEvent(ev);
        }
    

    而在PhoneWindow中调用了mDecor.superDispatchTouchEvent(MotionEvent event),可以看到PhoneWindow只是起中转作用,将事件交给了DecorView,PhoneWindow中的DecorView是在setContentView时生成并绑定的。

        //PhoneWindow
        @Override
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return mDecor.superDispatchTouchEvent(event);
        }
    

    DecorView是一个继承FrameLayout的ViewGroup,在superDispatchTouchEvent(MotionEvent event)方法中调用super.dispatchTouchEvent(event),即将event事件传递到了ViewGroup层,实现Activity到ViewGroup的传递。

        //DecorView
        public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
    

    由上可知,因为DecorView是一个继承自FrameLayout的ViewGroup,因此实现了MotionEvent事件,由Activity传递到ViewGroup中去的过程,接下来事件就在ViewGroup和子View中进行分发,消费。

    事件分发流程

    先来个传说中的U型事件图镇楼:后续的分析结合这个图,会更好理解。


    在这里插入图片描述

    1.Activity的事件分发

    先看下源码:

        //Activity dispatchTouchEvent分发事件
        public boolean dispatchTouchEvent(MotionEvent ev) {
            //step 1 当DOWN事件来临时,做一些特殊处理,onUserInteraction默认是空实现,你可以自己定义想要操作的事件。
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            //step 2 将事件交给Window,进而交给decorView处理。
            // 若getWindow().superDispatchTouchEvent(ev)的返回true
            // 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束
            // 否则:继续往下调用Activity.onTouchEvent
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            //step 3 如果在step 2中,整个View Tree都不处理事件,那么就将事件交给Activity的onTouchEvent(ev)处理,不论
            //onTouchEvent(ev)返回值如何,都表示Activity消费了该事件,后续的MOVE和UP事件,也会直接交给Activity处理,不在向下分发。
            return onTouchEvent(ev);
        }
        
        //Activity onTouchEvent消费事件
        public boolean onTouchEvent(MotionEvent event) {
            //shouldCloseOnTouch主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界
            //内等,所以Activity的onTouchEvent事件,默认是返回false的,除非你重写了。
            if (mWindow.shouldCloseOnTouch(this, event)) {
                finish();
                return true;
            }
            //默认是返回false的
            return false;
        }
    

    Activity的事件分发与消费比较简单,总结如下:

    1.1 Activity.dispatchTouchEvent(MotionEvent ev)分析--针对DOWN事件(后续分析CANCEL和UP事件)

    1.当事件由底层传到Activity时,进入Activity.dispatchTouchEvent(MotionEvent ev),在Activity中有三种返回值,true,false,以及super.dispatchTouchEvent(ev);

        //我们自己的Activity,进行了重写
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
    //        return true;
    //        return false;
            return super.dispatchTouchEvent(ev);
        }
    

    再来看下父类Activity的dispatchTouchEvent方法。

        //父类的Activity
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            return onTouchEvent(ev);
        }
    

    第一种情况:返回true;
    由于我们在自己的Activity中重写了父类的dispatchTouchEvent(MotionEvent ev)方法,因此不会再调用getWindow().superDispatchTouchEvent(ev)方法了,即不会将事件传递到View层,同时也不会调用onTouchEvent(ev);该事件直接被消费了。后续再来的MOVE和UP事件也不会再向下分发,而是会直接经过DecorView.dispatchTouchEvent方法,通过回调交给Activity的dispatchTouchEvent,由于返回了true,会直接消费,也不会将事件交给onTouchEvent了。

    第二种情况:返回false;
    同第一种情况,也是直接消费了,不会将事件向下传了,也不会将事件交给onTouchEvent了。

    第三种情况:返回super.dispatchTouchEvent(ev);
    如果调用默认的方法,即不对dispatchTouchEvent做任何处理,那么就会调用父类Activity的dispatchTouchEvent方法,将会调用getWindow().superDispatchTouchEvent(ev)方法,将event事件传递给decorView(ViewGroup)的dispatchTouchEvent,如果getWindow().superDispatchTouchEvent(ev)返回true,表示有子view消费了此事件,那么Activity的dispatchTouchEvent会返回true,事件到此结束,否则,会将事件传递给Activity的onTouchEvent,不论onTouchEvent返回值如何,事件分发都到此结束。

    那么getWindow().superDispatchTouchEvent(ev)什么时候返回true,什么时候返回false呢,这就需要分析ViewGroup的事件分发流程了。后续分析,此处先介绍下,Activity的onTouchEvent事件。

    1.2 Activity.onTouchEvent(MotionEvent ev)事件消费

        //Activity onTouchEvent消费事件
        public boolean onTouchEvent(MotionEvent event) {
            //shouldCloseOnTouch主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界
            //内等,所以Activity的onTouchEvent事件,默认是返回false的,除非你重写了。
            if (mWindow.shouldCloseOnTouch(this, event)) {
                finish();
                return true;
            }
            //默认是返回false的
            return false;
        }
    

    假如Activity中的所有控件都不消费触摸事件DOWN,即getWindow().superDispatchTouchEvent(ev)返回false,就会将事件交给Activity.onTouchEvent,事件到此终结,后续的MOVE和UP就会直接交给Activity.onTouchEvent处理。
    在我们自己的Activity中,onTouchEvent也有三种返回值,true,false,以及super.onTouchEvent(ev),只是不论哪种返回值,最终的结果都是终结了事件传递,对后续事件的处理一致。

    2.ViewGroup的事件分发

    2.1 ViewGroup.dispatchTouchEvent(ev)---事件分发,分段式分析

    下面只给出了dispatchTouchEvent的关键代码,非主要代码省略,可自己去看下源码,这里主要是分析分发过程。

        //ViewGroup.dispatchTouchEvent
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            //...省略...
            boolean handled = false;
            if (onFilterTouchEventForSecurity(ev)) {
                final int action = ev.getAction();
                final int actionMasked = action & MotionEvent.ACTION_MASK;
                // 分析1 初始化DOWN事件,每次接收到down事件时,会先清除mFirstTouchTarget
                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();
                }
                //第一部分,事件拦截,可以看到只有DOWN事件,或者mFirstTouchTarget不为null的情况下才会
                //进行拦截判断,move、up事件,或者mFirstTouchTarget为null时,会跳过这一部分。
                final boolean intercepted;
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {
                    //分析2 子view通知父iew是否拦截 requestDisallowInterceptTouchEvent(false)
                    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                    if (!disallowIntercept) {
                        //分析3 判断是否拦截
                        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;
                }
                //...省略...
                TouchTarget newTouchTarget = null;
                boolean alreadyDispatchedToNewTouchTarget = false;
                //第二部分,事件分发
                if (!canceled && !intercepted) {
                                      //...省略....
                    //分析4 只有Down事件才会走这里的if语句块,move事件和up事件都不会走
                    if (actionMasked == MotionEvent.ACTION_DOWN
                            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                        //...省略...
                        final int childrenCount = mChildrenCount;
                        if (newTouchTarget == null && childrenCount != 0) {
                            //...省略...
                            final View[] children = mChildren;
                            for (int i = childrenCount - 1; i >= 0; i--) {
                                           //...省略...
                             //分析4 循环遍历子view,只有VISIBLE且包含落点坐标的子view才能接收down事件
                                if (!canViewReceivePointerEvents(child)
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
                                //...省略...
                               //分析5 将事件交给每一个view处理,返回true,说明该子view消费了down事件,
                               if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                    //...省略...
                                    //分析6 如果有子view消费了down事件,则给mFirstTouchTarget赋值 
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
                            }
                        }
                        //...省略...
                    }
                }
    
                //第三部分,事件处理消费 Dispatch to touch targets.
                //分析7  经过上面的遍历没有找到能够处理down事件的子view,只能将事件交给自己处理了,
                //此时child为null
                if (mFirstTouchTarget == null) {
                    // No touch targets so treat this as an ordinary view.
                    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
                } 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;
                        //分析8 在分析6处遍历找到了能够处理down事件的子view后,
                        //alreadyDispatchedToNewTouchTarget为true,在分析5处已经处理了,所以这里直接返回
                        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                            handled = true;
                        } else {
                        //如果子view调用requestDisallowInterceptTouchEvent(false),会将interceptd置true,使得cancelChild为true
                            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                    || intercepted;
                            //分析9  此处主要处理move、up、cancel事件,具体分析后面给出
                            if (dispatchTransformedTouchEvent(ev, cancelChild,
                                    target.child, target.pointerIdBits)) {
                                handled = true;
                            }
                            //分析10 如果事件cancel了,这里会将mFirstTouchTarget置为null
                            if (cancelChild) {
                                if (predecessor == null) {
                                    mFirstTouchTarget = next;
                                } else {
                                    predecessor.next = next;
                                }
                                target.recycle();
                                target = next;
                                continue;
                            }
                        }
                        predecessor = target;
                        target = next;
                    }
                }
                //...省略...
            return handled;
        }
    
        //分析4处
        //ViewGroup.canViewReceivePointerEvents,只有VISIBLE的控件才能接收触摸事件,这也解释了,
        //为什么INVISILE的控件,能够measure,能够layout,但是不能响应触摸事件的原因
        private static boolean canViewReceivePointerEvents(@NonNull View child) {
            return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                    || child.getAnimation() != null;
        }
        
        //分析4处
        //ViewGroup.isTransformedTouchPointInView ,如果触摸事件的xy坐标落到了当前正在遍历的子view的
        //区域内,返回true,表示这个子view能接收触摸事件。
        protected boolean isTransformedTouchPointInView(float x, float y, View child,
                PointF outLocalPoint) {
            final float[] point = getTempPoint();
            point[0] = x;
            point[1] = y;
            transformPointToViewLocal(point, child);
            final boolean isInView = child.pointInView(point[0], point[1]);
            if (isInView && outLocalPoint != null) {
                outLocalPoint.set(point[0], point[1]);
            }
            return isInView;
        }
        
        //分析5处,这个方法才是真正分发处理事件的方法,down,move,up,cancel都由这个方法处理
        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();
            
            //分析11, 如果cancel为true,会先将MOVE事件换成ACTION_CANCEL,交给child处理,然后又改成MOVE。
            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;
            }
            //...省略...
            
            //分析12, 将DOWN、MOVE、UP事件,交给child处理。
            // Perform any necessary transformations and dispatch.
            if (child == null) {
                handled = super.dispatchTouchEvent(transformedEvent);
            } else {
                //...省略...
                handled = child.dispatchTouchEvent(transformedEvent);
            }
            // Done.
            transformedEvent.recycle();
            return handled;
        }
    
        //分析6处,如果子view消费了down事件,说明有某个view能够处理触摸事件,此时就会给mFirstTouchTarget
        //赋值,等后面的move和up事件来到时,就会进入第一部分的代码块,重新判断是否需要拦截
        private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
            final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
            target.next = mFirstTouchTarget;
            mFirstTouchTarget = target;
            return target;
        }
    

    上面是viewGroup进行事件分发的源码,几个重要的地方已经给出了说明,这里将各种情况进行总结。
    在我们自己定义的viewGroup中,可以重写dispatchTouchEvent来自己定义是否分发给子view
    第一种情况:返回true;
    如果直接返回true,就不会走父类中拦截,分发,消费的流程,直接告诉父容器自己处理了这个事件。注意,这句话里说的父类和父容器不是同一个东西,父容器在分发事件时,会遍历自己子view,即分析5处的代码,调用子view的dispatchTouchEvent,这时候,因为自己返回了true,父容器的分发事件就会进入到分析6处的代码,给mFirstTouchTarget赋值。

    第二种情况:返回false;
    如果直接返回false,同样也不会走父类中拦截,分发,消费的流程,直接告诉父容器自己不处理这个事件。父容器在分发事件时,会遍历自己子view,即分析5处的代码,调用子view的dispatchTouchEvent,这时候,因为自己返回了false,不会进入到分析6处的代码,不会给mFirstTouchTarget赋值。在分析7代码处,由于mFirstTouchTarget为null,会调用super.dispatchTouchEvent,即将事件交给父容器自己处理。子view不处理了,只能自己处理了。

    第三种情况:返回super.dispatchTouchEvent;
    如果我们自定义的ViewGroup重写了dispatchTouchEvent,并返回super.dispatchTouchEvent,即会走父类默认的分发流程。这里也是事件分发最复杂的地方。

    重点分析第三种情况。

    这块我们可以总体把dispatchTouchEvent分为三部分,第一部分用于判断ViewGroup是否拦截事件,第二部分用于遍历子view,对DOWN事件进行分发,找到能够处理触摸事件的子View。第三部分则是处理事件,主要是处理MOVE、UP、CANCEL事件。

    ACTION_DOWN事件分发流程:

    1.当DOWN事件由Activity分发到ViewGroup的dispatchTouchEvent方法之后,分析1处,会将之前的触摸重新初始化,mFirstTouchTarget置为null,接着进入第一部分,判断是否拦截,且只有当事件类型是DOWN事件,或者mFirstTouchTarget不为空的情况下,才会进行拦截判断。

    2.由于是第一个DOWN事件,mFirstTouchTarget一定为空,进入到拦截判断,即分析3处的判断,onInterceptTouchEvent返回值决定是否拦截,即变量intercepted的赋值,DOWN事件时,intercepted赋值false,此时会进入到第二部分代码中去,即在分析5处,遍历子View,对DOWN事件进行处理。

    3.如果分析5处,找到了能够消费DOWN事件的子View,就会进入分析6处,将该View赋值给mFirstTouchTarget,如果所有子View都不消费DOWN事件,则mFirstTouchTarget为null。

    4.第三部分代码是对事件的处理,如果第二步中的执行结果,使得mFirstTouchTarget为null,则在第三部分就会进入分析7的代码,将DOWN事件交给自己处理。

    5.如果mFirstTouchTarget不为null,表示已经有View消费了DOWN事件,此时代码会走到分析8处,直接将handle置为true。表示消费了DOWN事件。

    ACTION_MOVE、ACTION_UP事件分发流程:

    上面是对DOWN事件的处理,主要是用于找出是否有子View消费了DOWN事件。
    1.当MOVE或UP事件来到时,由于不是DOWN事件,因此不会走分析1处发初始化。当走到第一部分判断是否拦截时,由于不是DOWN事件,只能由mFirstTouchTarget决定是否进行判断。

    2.如果在处理DOWN事件时,找到了消费事件的View,即mFirstTouchTarget不为null,则需要根据onInterceptTouchEvent返回值决定是否拦截,不论拦截与否,都不会进入到第二部分的事件分发。因为第二部分分发只处理DOWN事件。如果mFirstTouchTarget为null,则intercepted = true。

    3.第三部分执行时,如果mFirstTouchTarget为null,继续讲MOVE和UP事件交给自己处理。

    4.第三部分执行时,如果mFirstTouchTarget不为null,则会进入到分析9的代码分支。如果在步骤2中onInterceptTouchEvent返回了false,即intercepted = false,cancelChild也为false,就将MOVE和UP事件交给mFirstTouchTarget处理。

    ACTION_CANCEL事件分发流程:

    在分析ACTION_MOVE、ACTION_UP事件时,第4步中,如果mFirstTouchTarget不为null,则会进入到分析9的代码分支。如果在步骤2中onInterceptTouchEvent返回了true,即intercepted = true,此时cancelChild也为true,分析9调用dispatchTransformedTouchEvent(ev, true, target.child, target.pointerIdBits),传入参数cancelChild为true。

        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();
            
            //分析10, 如果cancel为true,会先将MOVE事件换成ACTION_CANCEL,交给child处理,然后又改成MOVE。
            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;
            }
    
            return handled;
        }
    

    由源码可知,当cance为true时,event.setAction(MotionEvent.ACTION_CANCEL);会将MOVE事件改成CANCEL事件,并交给mFirstTouchTarget处理,处理后又设置回MOVE事件。这就是ACTION_CANCEL事件触发的时机,即之前有子VIew消费了DOWN事件,但是后来MOVE事件被拦截了。

    在这一个MOVE事件装换成CANCEL事件被消费后,分析10处,会将mFirstTouchTarget置为null。那么在下一个MOVE来临时,mFirstTouchTarget为null,同时又不是DOWN事件,ViewGroup.dispatchTouchEvent会直接走入到分析7的代码分支中去,由自己消费后续的MOVE事件。

    至此,ViewGroup的事件分发分析结束。

    2.2 ViewGroup.onInterceptTouchEvent(ev)---事件拦截分析

    事件拦截方法onInterceptTouchEvent,只在ViewGroup中存在,默认情况下,都是返回false,即不拦截。在我们自己定义的viewGroup中,可以重写onInterceptTouchEvent来自己定义是否拦截,返回true表示拦截,返回false表示不拦截,如果返回super,则表示调用父类默认的方法,即不拦截,返回false和super,都表示不拦截事件。

    第一种情况:返回true;
    表示拦截事件,交给自己的onTouchEvent处理。

    第二种情况:返回false;
    表示不拦截事件,会将事件分发给子view。

    第三种情况:返回super;
    同返回false的情况,表示不拦截事件,会将事件分发给子view。

        //ViewGroup 事件拦截
        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;
        }
    

    2.3 ViewGroup.onTouchEvent(ev)---事件消费

    ViewGroup没有重写onTouchEvent方法,因此,还是调用的父类(即View)的onTouchEvent方法,在View的分析中会给出。

    3.View的事件分发

    3.1 View.dispatchTouchEvent(ev)---事件分发分析

        // View.dispatchTouchEvent
        public boolean dispatchTouchEvent(MotionEvent event) {
            //...省略...
            if (onFilterTouchEventForSecurity(event)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
                //分析1 如果view设置了mOnTouchListener,且view是enable的,同时onTouch返回true,
                //则表示消费了事件,不会再将事件交给onTouchEvent处理。
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
                //分析2 如果没有设置mOnTouchListener,或者view不是enable的,或者onTouch返回了false,则会
                //将事件交给View的onTouchEvent处理。默认情况下,该方法只要view是clickable或者
                //LONG_CLICKABLE,就会返回true,表示消费了事件
                if (!result && onTouchEvent(event)) {
                    result = true;
                }
            }
            //...省略...
            return result;
        }
    

    我们自定义View的dispatchTouchEvent,也有三种重写方式。
    第一种情况:返回true;
    如果直接返回true,就不会走分发,消费的流程,直接告诉父容器自己处理了这个事件。父容器在分发事件时,会遍历自己子view,即ViewGroup中分析5处的代码,调用子view的dispatchTouchEvent,这时候,因为View自己返回了true,父容器的分发事件就会进入到ViewGroup中分析6处的代码,给mFirstTouchTarget赋值,在下一个MOVE事件或UP事件来到时,直接交给这个消费了DOWN事件的View。

    第二种情况:返回false;
    如果直接返回false,同样也不会走分发,消费的流程,直接告诉父容器自己不处理这个事件。父容器在分发事件时,会遍历自己子view,即ViewGroup中分析5处的代码,调用子view的dispatchTouchEvent,这时候,因为View自己返回了false,不会进入到ViewGroup中分析6处的代码,不会给mFirstTouchTarget赋值。在ViewGroup中分析7代码处,由于mFirstTouchTarget为null,会调用super.dispatchTouchEvent,即将事件交给父容器自己处理。子view不处理了,只能父容器自己处理了。

    第三种情况:返回super.dispatchTouchEvent;
    如果我们自定义的View重写了dispatchTouchEvent,并返回super.dispatchTouchEvent,即会走父类View默认的分发流程。

    分析1:在dispatchTouchEvent中,首先会去查找View是否设置了mOnTouchListener,即是否调用了
    View.setOnTouchListener,且判断onTouch的返回值,如果返回true,则表示事件被当前这个View消费了,不会再把事件传递到onTouchEvent中去。

    分析2:如果没有设置mOnTouchListener,或者view不是enable的,或者onTouch返回了false,则会将事件交给View的onTouchEvent处理。默认情况下,该方法只要view是clickable或者LONG_CLICKABLE,就会返回true,表示消费了事件。

    3.2 View.onTouchEvent(ev)---事件消费

     //View.onTouchEvent
    public boolean onTouchEvent(MotionEvent event) {
            //...省略...
            //分析1 只要view是CLICKABLE或LONG_CLICKABLE,clickable就为true,onTouchEvent就一定返回true
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
            if ((viewFlags & ENABLED_MASK) == DISABLED) {
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                //分析2 如果view是disabled,但clickable为true,onTouchEvent也返回true
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                return clickable;
            }
          //分析3 如果view是设置了代理,且代理View的onTouchEvent返回true,则自己的onTouchEvent也返回true
            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }
            //分析4 clickable为true,onTouchEvent一定返回true,表示消费了事件。
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        if ((viewFlags & TOOLTIP) == TOOLTIP) {
                            handleTooltipUp();
                        }
                        if (!clickable) {
                            removeTapCallback();
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
                        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                        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();
                            }
    
                            if (prepressed) {
                                // The button is being released before we actually
                                // showed it as pressed.  Make it show the pressed
                                // state now (before scheduling the click) to ensure
                                // the user sees it.
                                setPressed(true, x, y);
                            }
    
                            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                                // This is a tap, so remove the longpress check
                                removeLongPressCallback();
    
                                // 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();
                                    }
                                    //分析5 如果设置了点击事件,在收到ACTION_UP事件时,执行click事件
                                    if (!post(mPerformClick)) {
                                        performClick();
                                    }
                                }
                            }
    
                            if (mUnsetPressedState == null) {
                                mUnsetPressedState = new UnsetPressedState();
                            }
    
                            if (prepressed) {
                                postDelayed(mUnsetPressedState,
                                        ViewConfiguration.getPressedStateDuration());
                            } else if (!post(mUnsetPressedState)) {
                                // If the post failed, unpress right now
                                mUnsetPressedState.run();
                            }
    
                            removeTapCallback();
                        }
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                        if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                            mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                        }
                        mHasPerformedLongPress = false;
    
                        if (!clickable) {
                            checkForLongClick(0, x, y);
                            break;
                        }
    
                        if (performButtonActionOnTouchDown(event)) {
                            break;
                        }
    
                        // Walk up the hierarchy to determine if we're inside a scrolling container.
                        boolean isInScrollingContainer = isInScrollingContainer();
    
                        // For views inside a scrolling container, delay the pressed feedback for
                        // a short period in case this is a scroll.
                        if (isInScrollingContainer) {
                            mPrivateFlags |= PFLAG_PREPRESSED;
                            if (mPendingCheckForTap == null) {
                                mPendingCheckForTap = new CheckForTap();
                            }
                            mPendingCheckForTap.x = event.getX();
                            mPendingCheckForTap.y = event.getY();
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                        } else {
                            // Not inside a scrolling container, so show the feedback right away
                            setPressed(true, x, y);
                       //分析6 如果设置了长按事件,在收到ACTION_DOWN事件时,在500ms时,会检查执行长按事件
                            checkForLongClick(0, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        if (clickable) {
                            setPressed(false);
                        }
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        if (clickable) {
                            drawableHotspotChanged(x, y);
                        }
    
                        // Be lenient about moving outside of buttons
                        if (!pointInView(x, y, mTouchSlop)) {
                            // Outside button
                            // Remove any future long press/tap checks
                            removeTapCallback();
                            removeLongPressCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                setPressed(false);
                            }
                            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        }
                        break;
                }
    
                return true;
            }
    
            return false;
        }
    

    我们自定义View时,可以重写onTouchEvent方法,来自己定义是否消费事件,返回true表示消费,返回false表示不消费,如果返回super,则表示调用父类默认的方法。
    第一种情况:返回true;
    表示消费事件,此时,dispatchTouchEvent返回true,事件终结。

    第二种情况:返回false;
    表示不消费事件,此时,子view的dispatchTouchEvent返回false,父容器没有找到能够处理down事件的子view,此时父容器的mFirstTouchTarget就为null,将事件交给自己处理,详见ViewGroup的分析7。

    第三种情况:返回super;
    如果自定义view返回了super.onTouchEvent,表示调用父类默认的事件处理方法。
    这种情况下,只要View是clickable的,就会返回true,表示消费了。总结下代码中的多出分析点:
    分析1:只要view是CLICKABLE或LONG_CLICKABLE,clickable就为true,onTouchEvent就一定返回true
    分析2:如果view是disabled,但clickable为true,onTouchEvent也返回true
    分析3:如果view是设置了代理,且代理View的onTouchEvent返回true,则自己的onTouchEvent也返回true。
    分析4:如果clickable为true,就会进入处理DOWN、MOVE、UP事件的代码块中,最终onTouchEvent一定返回true,表示消费了事件。
    分析5:在处理事件时,如果设置了点击事件,在收到ACTION_UP事件时,会执行click事件
    分析6:如果设置了长按事件,在收到ACTION_DOWN事件时,会启动一个runnable,在500ms时,会检查执行长按事件。

    由上面的分析可知,View想要消费事件,必须要是clickable的,默认情况下,View的LONG_CLICKABLE为false,而CLICKABLE则就View类型有关,比如TextView默认不可点,Button默认是可点击的。另外在设置监听的时候,是会将CLICKABLE和LONG_CLICKABLE置为true的,也就是说,设置了监听,就一定会消费事件。

        //设置点击事件
        public void setOnClickListener(@Nullable OnClickListener l) {
            if (!isClickable()) {
                setClickable(true);
            }
            getListenerInfo().mOnClickListener = l;
        }
        //设置长按事件
        public void setOnLongClickListener(@Nullable OnLongClickListener l) {
            if (!isLongClickable()) {
                setLongClickable(true);
            }
            getListenerInfo().mOnLongClickListener = l;
        }
    

    根据上面的分析可知,onClick事件是在View.onTouchEvent收到UP事件的时候,才会触发,而View.onTouchEvent能够执行的先决条件就是,View的没有设置OnTouchListener,或者虽然设置了OnTouchListener,但是其onTouch返回了false。由此可知View几个事件的执行顺序是:

        onTouch ---> onTouchEvent ---> onClick
    

    U型事件流和L型事件流

    经过上面的理论分析,再来看这个图是不是就很清楚了,分发的时候,如果都不拦截,处理的时候,都不处理,那么完整的事件流就是个U型事件流。文中的事件图是参考的别人的博客,只是为了让大家加深理解,如果想了解更多,可以参考Kelin大神的文章:图解 Android 事件分发机制

    在这里插入图片描述
    如果在消费事件的时候,存在某一个子View消费了DOWN事件,那么接下来的ACTION_MOVE 和 ACTION_UP 事件就会直接交给该子View的onTouchEvent处理,即就是我们常说的L型事件流。
    红色的箭头代表ACTION_DOWN 事件的流向
    蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

    在这里插入图片描述
    总结下:
    对于在onTouchEvent消费事件的情况:在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。

    对于ACTION_MOVE、ACTION_UP总结:ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。

    如何解决滑动冲突

    经过前面的分析,我们知道子View可以通过调用父容器的requestDisallowInterceptTouchEvent(true/false)方法控制父容器是否对事件进行拦截,解决滑动冲突也是基于这一点进行处理的。当然DOWN事件是不能拦截的,因为如果DOWN事件被拦截了,那么子View就永远无法获取到事件了。
    解决滑动冲突有两种方法:一种是内部拦截法,另一种是外部拦截法。

    1.内部拦截法

    是指我们可以重写child的dispatchTouchEvent方法,判断是否需要让parent拦截事件,需要修改父容器的onInterceptTouchEvent方法,和子View的dispatchTouchEvent,代码如下:

        //内部拦截法(父容器的onInterceptTouchEvent)
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            //down事件不能拦截,否则,子view就接收不到事件了
            if (ev.getAction() == MotionEvent.ACTION_DOWN){
                return false;
            }
            return true;
        }
        
        //2.内部拦截法(父容器-->onInterceptTouchEvent,  子View-->dispatchTouchEvent)
        private float lastX = 0;
        private float lastY = 0;
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            float x = ev.getX();
            float y = ev.getY();
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //true 会使得父容器 dispatchTouchEvent中的disallowIntercept为true,
                    //导致父容器不会调用onInterceptTouchEvent,即不会拦截
                    getParent().requestDisallowInterceptTouchEvent(true);
                    lastX = x;
                    lastY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    float moveX = lastX - x;
                    float moveY = lastY - y;
                    //false 会使得父容器 dispatchTouchEvent中的disallowIntercept为false,
                    //导致父容器会调用onInterceptTouchEvent,即会拦截事件
                    if (Math.abs(moveX) > Math.abs(moveY) + 5) {//横向滑动大于纵向滑动,拦截事件
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                    lastX = ev.getX();
                    lastY = ev.getY();
    
                    break;
                case MotionEvent.ACTION_UP:
                    //此处设置不设置都不会影响后续的流程,up时,不会走到这里,都是由父容器处理
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                default:
                    break;
            }
            return super.dispatchTouchEvent(ev);
        }
    

    2.外部拦截法(只需修改父容器的onInterceptTouchEvent事件)

    是指我们可以重写parent的onInterceptTouchEvent方法,判断当前的事件是否需要拦截,代码如下:

        private float lastX = 0;
        private float lastY = 0;
        //1.外部拦截法 (父容器-->onInterceptTouchEvent)
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            float x = ev.getX();
            float y = ev.getY();
            boolean intercept = false;
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    lastX = x;
                    lastY = y;
                    intercept = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    float moveX = lastX - x;
                    float moveY = lastY - y;
                    if (Math.abs(moveX) > Math.abs(moveY) + 5) {//横向滑动大于纵向滑动,拦截事件
                        intercept = true;
                    } else {
                        intercept = false;
                    }
                    lastX = ev.getX();
                    lastY = ev.getY();
    
                    break;
                case MotionEvent.ACTION_UP:
                    intercept = false;
                    break;
                default:
                    break;
            }
            return intercept;
        }
    

    相关文章

      网友评论

        本文标题:Android事件分发机制--拿来吧你

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