Android事件分发机制完全解析,带你从源码的角度彻底理解

2023-05-16

Android中所有事件的传递都是自上而下,然后自下而上的一个传递过程,直到事件被消费,结束掉。

Android事件构成

在Android中,事件主要包括点按、长按、拖拽、滑动等,点按又包括单击和双击,另外还包括单指操作和多指操作。所有这些都构成了Android中得事件响应。总的来说,所有的事件都由如下三个部分作为基础:

  • 按下(ACTION_DOWN)
  • 移动(ACTION_MOVE)
  • 抬起(ACTION_UP)
  • ()取消(ACTION_CANCLE)-意外原因导致事件被取消了。

所有的操作事件首先必须执行的是按下操作(ACTION_DOWN),之后所有的操作都是以按下操作作为前提,当按下操作完成后,接下来可能是一段移动(ACTION_MOVE)然后抬起(ACTION_UP),或者是按下操作执行完成后没有移动就直接抬起。这一系列的动作在Android中都可以进行控制。

我们知道,所有的事件操作都发生在触摸屏上,而在屏幕上与我们交互的就是各种各样的视图组件(View),在Android中,所有的视图都继承于View,另外通过各种布局组件(ViewGroup)来对View进行布局,ViewGroup也继承于View。所有的UI控件例如Button、TextView都是继承于View,而所有的布局控件例如RelativeLayout、容器控件例如ListView都是继承于ViewGroup。所以,我们的事件操作主要就是发生在View和ViewGroup之间,那么View和ViewGroup中主要有哪些方法来对这些事件进行响应呢?记住如下3个方法,我们通过查看View和ViewGroup的源码可以看到:

View.java

    public boolean dispatchTouchEvent(MotionEvent event)
    public boolean onTouchEvent(MotionEvent event)

ViewGroup.java

    public boolean dispatchTouchEvent(MotionEvent event)
    public boolean onTouchEvent(MotionEvent event) 
    public boolean onInterceptTouchEvent(MotionEvent event)

在View和ViewGroup中都存在dispatchTouchEvent和onTouchEvent方法,但是在ViewGroup中还有一个onInterceptTouchEvent方法,那这些方法都是干嘛的呢?别急,我们先看看他们的返回值。这些方法的返回值全部都是boolean型,为什么是boolean型呢,看看本文的标题,“事件传递”,传递的过程就是一个接一个,那到了某一个点后是否要继续往下传递呢?你发现了吗,“是否”二字就决定了这些方法应该用boolean来作为返回值。没错,这些方法都返回true或者是false。在Android中,所有的事件都是从开始经过传递到完成事件的消费,这些方法的返回值就决定了某一事件是否是继续往下传,还是被拦截了,或是被消费了。

接下来就是这些方法的参数,都接受了一个MotionEvent类型的参数,MotionEvent继承于InputEvent,用于标记各种动作事件。之前提到的ACTION_DOWN、ACTION_MOVE、ACTION_UP都是MotinEvent中定义的常量。我们通过MotionEvent传进来的事件类型来判断接收的是哪一种类型的事件。到现在,这三个方法的返回值和参数你应该都明白了,接下来就解释一下这三个方法分别在什么时候处理事件。

  • dispatchTouchEvent方法用于事件的分发,Android中所有的事件都必须经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。返回true表示不继续分发,事件没有被消费。返回false则继续往下分发,如果是ViewGroup则分发给onInterceptTouchEvent进行判断是否拦截该事件。
  • onTouchEvent方法用于事件的处理,返回true表示消费处理当前事件,返回false则不处理,交给子控件进行继续分发。
  • onInterceptTouchEvent是ViewGroup中才有的方法,View中没有,它的作用是负责事件的拦截,返回true的时候表示拦截当前事件,不继续往下分发,交给自身的onTouchEvent进行处理。返回false则不拦截,继续往下传。这是ViewGroup特有的方法,因为ViewGroup中可能还有子View,而在Android中View中是不能再包含子View的(iOS可以)。
  • 到目前为止,Android中事件的构成以及事件处理方法的作用你应该比较清楚了,接下来我们就通过一个Demo来实际体验实验一下。

Android事件处理

项目中新建一个类RTButton继承Button,用来实现我们对按钮事件的跟踪。

RTButton.java

raw.githubusercontent.com

    /**
     * Created by  qy【晴雨】 on 2018/1/3.
     * github.com/qydq/an-aw-base
     */
    
    @SuppressLint("AppCompatCustomView")
    public class RTButton extends Button {
        public RTButton(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    System.out.println("Motionevent--RTButton---dispatchTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    System.out.println("Motionevent--RTButton---dispatchTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    System.out.println("Motionevent--RTButton---dispatchTouchEvent---UP");
                    break;
                default:
                    break;
            }
            boolean dispatch = super.dispatchTouchEvent(event);
            System.out.println("Motionevent--RTButton---dispatchTouchEvent-"+dispatch);
            return dispatch;
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    System.out.println("Motionevent--RTButton---onTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    System.out.println("Motionevent--RTButton---onTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    System.out.println("Motionevent--RTButton---onTouchEvent---UP");
                    break;
                default:
                    break;
            }
            boolean onTouchEvent = super.onTouchEvent(event);
            System.out.println("Motionevent--RTButton---onTouchEvent-"+onTouchEvent);
            return onTouchEvent;
        }
    }

在RTButton中我重写了dispatchTouchEvent和onTouchEvent方法,并获取了MotionEvent各个事件状态,打印输出了每一个状态下的信息。然后在layout.xml中直接在根布局下放入自定义的按钮RTButton。

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <com.integrate.demo.moudule.motionevent.RTButton
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Button" />
    </LinearLayout>

接下来在MainActivity中为RTButton设置onTouch和onClick的监听器来跟踪事件传递的过程,另外,Activity中也有一个dispatchTouchEvent方法和一个onTouchEvent方法,我们也重写他们并输出打印信息。

MainActivity.java

raw.githubusercontent.com

    public class MainActivity extends AppCompatActivity {
        private static String TAG = "Motionevent";
        private RTButton button;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.layout);
    
            button = findViewById(R.id.btn);
    
    
            button.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            System.out.println("Motionevent--RTButton---onTouch---DOWN");
                            break;
                        case MotionEvent.ACTION_MOVE:
                            System.out.println("Motionevent--RTButton---onTouch---MOVE");
                            break;
                        case MotionEvent.ACTION_UP:
                            System.out.println("Motionevent--RTButton---onTouch---UP");
                            break;
                        default:
                            break;
                    }
                    return false;
                }
            });
    
        }
    
        /*控制事件分发:
        * Android中所有的事件都必须经过这个方法的 分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。
        * 返回true表示不继续分发,事件没有被消费。返回false则继续往下分发,如果是 ViewGroup则分发给onInterceptTouchEvent进行判断是否拦截该事件。
        * */
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    System.out.println("Motionevent--Activity---dispatchTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    System.out.println("Motionevent--Activity---dispatchTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    System.out.println("Motionevent--Activity---dispatchTouchEvent---UP");
                    break;
                default:
                    break;
            }
            boolean dispatch = super.dispatchTouchEvent(event);
            System.out.println("Motionevent--Activity---dispatch-" + dispatch);
            return dispatch;
    
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    System.out.println("Motionevent--Activity---onTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    System.out.println("Motionevent--Activity---onTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    System.out.println("Motionevent--Activity---onTouchEvent---UP");
                    break;
                default:
                    break;
            }
            boolean onTouchEvent = super.onTouchEvent(event);
            System.out.println("Motionevent--Activity---onTouchEvent-" + onTouchEvent);
            return onTouchEvent;
        }
    }

代码部分已经完成了,接下来运行工程,并分别点击空白处和按钮,查看日志输出信息,我们可以看到如下结果:
点击空白部分打印信息

通过日志输出可以看到,点击空白的地方只会执行Activity种的方法,首先执行了Activity中dispatchTouchEvent,然后调用onTouchEvent方法,默认onTouchEvent返回false,onTouchEvent先返回false,然后dispatch再返回false,那这是怎么回事呢?我们接着看,
在这里插入图片描述

点击按钮的时候可以看到首先Activity执行了onDispatchTouchEvent(ev)方法,在MainActivity方法中返回dispatchTouchEvent方法的返回值是super.dispatchTouchEvent(event),因此调用了父类方法,我们进入Activity.java的源码中看看具体实现。

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     * 
     * @param ev The touch screen event.
     * 
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

从源码中可以看到,dispatchTouchEvent方法只处理了ACTION_DOWN事件,前面提到过,所有的事件都是以按下为起点的,所以,Android认为当ACTION_DOWN事件没有执行时,后面的事件都是没有意义的,所以这里首先判断ACTION_DOWN事件。如果事件成立,则调用了onUserInteraction方法,该方法可以在Activity中被重写,在事件被分发前会调用该方法。该方法的返回值是void型,不会对事件传递结果造成影响,接着会判断getWindow().superDispatchTouchEvent(ev)的执行结果,看看它的源码:

      /**
         * Used by custom windows, such as Dialog, to pass the touch screen event
         * further down the view hierarchy. Application developers should
         * not need to implement or call this.
         *
         */
    
    public abstract boolean superDispatchTouchEvent(MotionEvent event);

通过源码注释我们可以了解到这是个抽象方法,用于自定义的Window,例如自定义Dialog传递触屏事件,并且提到开发者不需要去实现或调用该方法,系统会完成,可以看到源码中onTouchEvent(ev)方法,也就解释了上面点击空白的地方先执行了onTouchEvent里面的返回值,MainActivity中onTouchEvent默认返回了false,然后事件没有被消费,然后事件才调用onDispatchTouchEvent(ev)。

这里是不是有一个疑问呢?为什么点击按钮,没有执行MainActivity中的onTouchEvent方法,是不是和上面说的矛盾,其实不是的,事件的传递过程是自上而下,然后之下而上的过程,首先触发一个事件,都先调用onDisPatchTouchEvent(ev)方法判断事件是否进行分发,在Activity中,我们调用了super.onDisPatchTouchEvent方法,ok,别急。

如果我们在MainActivity中将dispatchTouchEvent方法的返回值设为true,那么这里的执行结果就为true,从而不会返回执行onTouchEvent(ev),如果这里返回false,那么最终会返回执行onTouchEvent方法,由此可知,接下来要调用的就是onTouchEvent方法了。别急,通过日志输出信息可以看到,ACTION_DOWN事件从Activity被分发到了RTButton,这是因为在MainActivity中接着执行了onTouch和onTouchEvent方法,为什么先执行onTouch方法呢?我们到RTButton中的dispatchTouchEvent看看View中的源码是如何处理的。

    /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
    
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                return true;
            }
    
            if (onTouchEvent(event)) {
                return true;
            }
        }
    
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
    }

挑选关键代码进行分析,可以看代码第16行,这里有几个条件,当几个条件都满足时该方法就返回true,当条件li.mOnTouchListener不为空时,通过在源码中查找,发现mOnTouchListener实在以下方法中进行设置的。

    /**
     * Register a callback to be invoked when a touch event is sent to this view.
     * @param l the touch listener to attach to this view
     */
    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

这个方法就已经很熟悉了,就是我们在MainActivity.java中为RTButton设置的onTouchListener,条件(mViewFlags & ENABLED_MASK) == ENABLED判断的是当前View是否是ENABLE的,默认都是ENABLE状态的。接着就是li.mOnTouchListener.onTouch(this, event)条件,这里调用了onTouch方法,该方法的调用就是我们在MainActivity.java中为RTButton设置的监听回调,如果该方法返回true,则整个条件都满足,dispatchTouchEvent就返回true,表示该事件就不继续向下分发了,因为已经被onTouch消费了。

如果onTouch返回的是false,则这个判断条件不成立,接着执行onTouchEvent(event)方法进行判断,如果该方法返回true,表示事件被onTouchEvent处理了,则整个dispatchTouchEvent就返回true。到这里,我们就可以回答之前提出的“为什么先执行onTouch方法”的问题了。到目前为止,ACTION_DOWN的事件经过了从Activity到RTButton的分发,然后经过onTouch和onTouchEvent的处理,最终,ACTION_DOWN事件交给了RTButton得onTouchEvent进行处理。

当我们的手(我这里用的Genymotion然后用鼠标进行的操作,用手的话可能会执行一些ACTION_MOVE操作)从屏幕抬起时,会发生ACTION_UP事件。从之前输出的日志信心中可以看到,ACTION_UP事件同样从Activity开始到RTButton进行分发和处理,最后,由于我们注册了onClick事件,当onTouchEvent执行完毕后,就调用了onClick事件,那么onClick是在哪里被调用的呢?继续回到View.java的源代码中寻找。由于onTouchEvent在View.java中的源码比较长,这里就不贴出来了,感兴趣的可以自己去研究一下,通过源码阅读,我们在ACTION_UP的处理分支中可以看到一个performClick()方法,从这个方法的源码中可以看到执行了哪些操作。

    /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener that was called, false
     *         otherwise is returned.
     */
    public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            return true;
        }
    
        return false;
    }

在if分支里可以看到执行了li.mOnClickListener.onClick(this);这句代码,这里就执行了我们为RTButton实现的onClick方法,所以,到目前为止,可以回答前一个“onClick是在哪里被调用的呢?”的问题了,onClick是在onTouchEvent中被执行的,并且,onClick要后于onTouch的执行。

到此,点击按钮的事件传递就结束了,我们结合源代码窥探了其中的执行细节,如果我们修改各个事件控制方法的返回值又会发生什么情况呢,带着这个问题,进入下一节的讨论。

Android事件拦截

上面,我们知道了在Android中存在哪些事件类型,事件的传递过程以及在源码中对应哪些处理方法。我们可以知道在Android中,事件是通过层级传递的,一次事件传递对应一个完整的层级关系,例如上节中分析的ACTION_DOWN事件从Activity传递到RTButton,ACTION_UP事件也同样。结合源码分析各个事件处理的方法,也可以明确看到事件的处理流程。

之前提过,所有事件处理方法的返回值都是boolean类型的,现在我们来修改这个返回值,首先从Activity开始,根据之前的日志输出结果,首先执行的是Activity的dispatchTouchEvent方法,现在将之前的返回值super.dispatchTouchEvent(event)修改为true,然后重新编译运行并点击按钮,看到如下的日志输出结果。
Activity中dispatchTouchEvent为true打印信息

可以看到,事件执行到dispatchTouchEvent方法就没有再继续往下分发了,这也验证了之前的说法,返回true时,不再继续往下分发,从之前分析过的Activity的dispatchTouchEvent源码中也可知,当返回true时,就没有去执行onTouchEvent方法了。

但是但是,*这里有个问题,如果我直接把返回值改为false,我们再看看打印信息。

Activity中dispatchTouchEvent为false打印信息
根据这个打印结果,我很怀疑我参考这边文章的作者没有解释清楚?因为无论返回false还是true都不会执行onTouchEvent,不是说返回fales就会执行onTouchEvent吗?那什么地方出现问题了呢,1,我怀疑手机荣耀9公司测试手机本身有问题,因为在测试很多问题这个手机又重大bug感觉。2,难道又是super.dispatchTouchEvent惹的祸。ok,那换个手机再来看。
另一个手机打印信息true
另一个手机打印信息true

根据实际结果证明不是手机的问题,确确实实是super.onDispatchTouchEvent没有调用导致的,如果我们调用父类的super则结果符合预期-onDispatchTouchEvent返回false事件继续分发,返回true事件不再往下分发。为此这里有一个结论:

在Activity中,复写父类的onDispatchTouchEvent,无论返回true|false,方法会被拦截。onTouchEvent
不会执行此时Activity中的所有view都没法获取focus。-(晴雨sun)

接着,将上述修改还原,让事件在Activity这继续往下分发,接着就分发到了RTButton,将RTButton的dispatchTouchEvent方法的返回值修改为true,重新编译运行并查看输出日志结果。
RTButton中dispatchTouchEvent返回true

从结果可以看到,事件在RTButton的dispatchTouchEvent方法中就没有再继续往下分发了。接着将上述修改还原,将RTButton的onTouchEvent方法返回值修改为true,让其消费事件,根据之前的分析,onClick方法是在onTouchEvent方法中被调用的,事件在这被消费后将不会调用onClick方法了,编译运行,得到如下日志输出结果。
RTButton中onTouchEvent返回true

跟分析结果一样,onClick方法并没有被执行,因为事件在RTButton的onTouchEvent方法中被消费了。下图是整个事件传递的流程图。
参考博文中事件传递图示

到目前为止,Android中的事件拦截机制就分析完了。但这里我们只讨论了单布局结构下单控件的情况,如果是嵌套布局,那情况又是怎样的呢?接下来我们就在嵌套布局的情况下对Android的事件传递机制进行进一步的探究和分析。

Android嵌套布局事件传递

首先,新建一个类RTLayout继承于LinearLayout,同样重写dispatchTouchEvent和onTouchEvent方法,另外,还需要重写onInterceptTouchEvent方法,在文章开头介绍过,这个方法只有在ViewGroup和其子类中才存在,作用是控制是否需要拦截事件。这里不要和dispatchTouchEvent弄混淆了,后者是控制对事件的分发,并且后者要先执行。

那么,事件是先传递到View呢,还是先传递到ViewGroup的?通过下面的分析我们可以得出结论。首先,我们需要对工程代码进行一些修改。

RTLayout.java

raw.githubusercontent.com

    public class RTLayout extends LinearLayout {
        public RTLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    System.out.println("Motionevent--RTLayout---dispatchTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    System.out.println("Motionevent--RTLayout---dispatchTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    System.out.println("Motionevent--RTLayout---dispatchTouchEvent---UP");
                    break;
                default:
                    break;
            }
            return super.dispatchTouchEvent(event);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    System.out.println("Motionevent--RTLayout---onInterceptTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    System.out.println("Motionevent--RTLayout---onInterceptTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    System.out.println("Motionevent--RTLayout---onInterceptTouchEvent---UP");
                    break;
                default:
                    break;
            }
            return super.onInterceptTouchEvent(event);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    System.out.println("Motionevent--RTLayout---onTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    System.out.println("Motionevent--RTLayout---onTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    System.out.println("Motionevent--RTLayout---onTouchEvent---UP");
                    break;
                default:
                    break;
            }
            return super.onTouchEvent(event);
        }
    }

同时,在布局文件中为RTButton添加一个父布局,指明为自定义的RTLayout,修改后的布局文件如下

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <com.integrate.demo.moudule.motionevent.RTLayout
            android:id="@+id/myLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent" >
    
            <com.integrate.demo.moudule.motionevent.RTButton
                android:id="@+id/btn"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="Button" />
        </com.integrate.demo.moudule.motionevent.RTLayout>
    
    </LinearLayout>

MainActivity.java

raw.githubusercontent.com

     public class MainActivity extends AppCompatActivity {
            private static String TAG = "Motionevent";
            private RTButton button;
            private RTLayout rtLayout;
        
            @Override
            protected void onCreate(@Nullable Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.layout);
        
                button = findViewById(R.id.btn);
                rtLayout = findViewById(R.id.myLayout);
        
        
                button.setOnTouchListener(new View.OnTouchListener() {
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        switch (event.getAction()) {
                            case MotionEvent.ACTION_DOWN:
                                System.out.println("Motionevent--RTButton---onTouch---DOWN");
                                break;
                            case MotionEvent.ACTION_MOVE:
                                System.out.println("Motionevent--RTButton---onTouch---MOVE");
                                break;
                            case MotionEvent.ACTION_UP:
                                System.out.println("Motionevent--RTButton---onTouch---UP");
                                break;
                            default:
                                break;
                        }
                        return false;
                    }
                });
        
                rtLayout.setOnTouchListener(new View.OnTouchListener() {
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        switch (event.getAction()) {
                            case MotionEvent.ACTION_DOWN:
                                System.out.println("Motionevent--RTLayout---onTouch---DOWN");
                                break;
                            case MotionEvent.ACTION_MOVE:
                                System.out.println("Motionevent--RTLayout---onTouch---MOVE");
                                break;
                            case MotionEvent.ACTION_UP:
                                System.out.println("Motionevent--RTLayout---onTouch---UP");
                                break;
                            default:
                                break;
                        }
                        return false;
                    }
                });
        
                rtLayout.setOnClickListener(new View.OnClickListener() {
        
                    @Override
                    public void onClick(View v) {
                        System.out.println("Motionevent--RTLayout clicked!");
                    }
                });
        
            }
    
        /*控制事件分发:
        * Android中所有的事件都必须经过这个方法的 分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。
        * 返回true表示不继续分发,事件没有被消费。返回false则继续往下分发,如果是 ViewGroup则分发给onInterceptTouchEvent进行判断是否拦截该事件。
        * */
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    System.out.println("Motionevent--Activity---dispatchTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    System.out.println("Motionevent--Activity---dispatchTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    System.out.println("Motionevent--Activity---dispatchTouchEvent---UP");
                    break;
                default:
                    break;
            }
            boolean dispatch = super.dispatchTouchEvent(event);
            System.out.println("Motionevent--Activity---dispatch-" + dispatch);
            return dispatch;
    
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    System.out.println("Motionevent--Activity---onTouchEvent---DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    System.out.println("Motionevent--Activity---onTouchEvent---MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    System.out.println("Motionevent--Activity---onTouchEvent---UP");
                    break;
                default:
                    break;
            }
            boolean onTouchEvent = super.onTouchEvent(event);
            System.out.println("Motionevent--Activity---onTouchEvent-" + onTouchEvent);
            return onTouchEvent;
        }
    }

代码修改完毕后,编译运行工程,同样,点击按钮,查看日志输出结果如下:
正常情况运行结果图

从日志输出结果我们可以看到,嵌套了RTLayout以后,事件传递的顺序变成了Activity->RTLayout->RTButton,这也就解释了前面提出的Android中事件是从上到下的传递,直到事件被消费掉,Android中事件传递是从ViewGroup传递到View的,而不是反过来传递的。

从输出结果第三行可以看到,执行了RTLayout的onInterceptTouchEvent方法,该方法的作用就是判断是否需要拦截事件,我们到ViewGroup的源码中看看该方法的实现。

    public boolean onInterceptTouchEvent(MotionEvent ev) {
            return false;
        }

该方法的实现很简单,只返回了一个false。那么这个方法是在哪被调用的呢,通过日志输出分析可知它是在RTLayout的dispatchTouchEvent执行后执行的,那我们就进到dispatchTouchEvent源码里面去看看。由于源码比较长,我将其中的关键部分截取出来做解释说明。

    // 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;
                }

从这部分代码中可以看到onInterceptTouchEvent调用后返回值被赋值给intercepted,该变量控制了事件是否要向其子控件分发,所以它起到拦截的作用,如果onInterceptTouchEvent返回false则不拦截,如果返回true则拦截当前事件。我们现在将RTLayout中的该方法返回值修改为true,并重新编译运行,然后点击按钮,查看输出结果如下。
RTLayout中onInterceptTOuchEvent为true运行结果图

可以看到,我们明明点击的按钮,但输出结果显示RTLayout点击事件被执行了,再通过输出结果分析,对比上次的输出结果,发现本次的输出结果完全没有RTButton的信息,没错,由于onInterceptTouchEvent方法我们返回了true,在这里就将事件拦截了,所以他不会继续分发给RTButton了,反而交给自身的onTouchEvent方法执行了,理所当然,最后执行的就是RTLayout的点击事件了。

再看一下整个ViewGroup事件分发过程的流程图吧,相信可以帮助大家更好地去理解:
ViewGroup事件分发流程图

详解onInterceptTouchEvent()

  1. down事件首先会传递到onInterceptTouchEvent()方法
  2. 如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return
    false,那么后续的move,
    up等事件将继续会先传递给该ViewGroup,之后才和down事件一样传递给最终的目标view的onTouchEvent()处理。
  3. 如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return
    true,那么后续的move,
    up等事件将不再传递给onInterceptTouchEvent(),而是和down事件一样传递给该ViewGroup的
    onTouchEvent()处理,注意,目标view将接收不到任何事件。
  4. 如果最终需要处理事件的view的onTouchEvent()返回了false,那么该事件将被传递至其上一层次的view的onTouchEvent()处理。
  5. 如果最终需要处理事件的view
    的onTouchEvent()返回了true,那么后续事件将可以继续传递给该view的onTouchEvent()处理。

总结

以上我们对Android事件传递机制进行了分析,期间结合系统源码对事件传递过程中的处理情况进行了探究。通过单布局情况和嵌套布局情况下的事件传递和处理进行了分析,现总结如下:

  • Android中事件传递按照从上到下进行层级传递,事件处理从Activity开始到ViewGroup再到View。如果View没有消费事件会再次从View到ViewGroup再到Activity最后事件被抛出消费掉。
  • 事件传递方法包括dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent,其中前两个是View和ViewGroup都有的,最后一个是只有ViewGroup才有的方法。这三个方法的作用分别是负责事件分发、事件处理、事件拦截。
  • 事件从被点击的控件开始,先开始从下到上的通过getParent()找到根布局rootView,然后开始从上向下的开始事件分发dispatchTouchEvent(),事件拦截onInterceptTouchEvent()和事件处理onTouchEvent()。如果事件被拦截了被处理但事件没有被消费的话,事件就会从被拦截处理的控件开始自下到上的开始往上传递,在传递过程中会调用父类的事
    件拦截onInterceptTouchEvent()和事件处理onTouchEvent()方法;如果事件一直没被消费,将会被rootView抛出
    结束事件传递。在事件被触发开始从下到上的传递后开始自上到下的事件分发,等事件传到最底部的控件后如果没有被消费的话,事件又开始自下
  • 到上的传递直到被 根部布局抛出,一旦事件被消费,事件传递机制就会停止结束。
  • 在ViewGroup中可以通过onInterceptTouchEvent方法对事件传递进行拦截,onInterceptTouchEvent方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。
  • 在事件从上到下分发到底后开始自下到上的传递过程中,子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true);
    让其父类ViewGroup布局不拦截事件(requestDisallowInterceptTouchEvent(true)此方法参数为true
    表示不拦截事件,为false 表示拦截事件)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android事件分发机制完全解析,带你从源码的角度彻底理解 的相关文章

  • redis学习-33-SpringBoot整合redis

    36 SpringBoot整合redis SpringBoot操作数据 xff1a 使用spring data项目 例如 xff1a jpa jdbc mongodb redis SpringData也是和SpringBoot齐名的项目 x
  • redis学习-34-PHP/Python使用Redis

    37 PHP使用Redis 首先需要熟练掌握 PHP 语言 xff0c 再者是已经安装了 PHP Redis 扩展程序 37 1 安装扩展程序 首先下载 PHP Redis 扩展程序 xff0c 下载的扩展程序要与 Redis 版本相符合
  • redis学习-35-Redis配置项汇总

    40 配置项汇总 对 Redis 的常用配置项和基本命令做简单的总结 xff0c 可以把本章看做简版的速查手册 不过 xff0c 需要注意的是由于 Redis 不同版本的差异 xff0c 配置项会多少存在一些不同 xff0c 但总体的来说
  • 韦东山:机会总是留给有准备的人(转)

    最近电子发烧友 xff08 以下称 39 发烧友 39 xff09 采访了韦东山老师 xff0c 本文是采访原稿 xff0c 展示出来让大家更深入了解韦老师的同时也进一步学习嵌入式Linux经验 机会总是留给有准备的人 发烧友 为什么要学习
  • matplotlib-animation--制作动画--制作各种图形-圆形矩形三角形

    目录 各个图形覆盖的问题 1 xff0c 画矩形并让其动起来 2 xff0c 画一个三角形 xff1a 动起来 xff01 3 xff0c 画一个三角形让其自动顺时针旋转 本教程基于前面的animation基础 xff0c 让各种图形动起来
  • 使用TS开发小程序中遇到的问题

    在使用ts开发小程序的过程中 xff0c 遇到了一些开发工具和关于ts的使用上的一些问题 xff0c 并记录解决方法 1 ts开发过程中类型指定问题 Type 39 number undefined 39 is not assignable
  • RecyclerView 使用总结以及常见问题解决方案

    1 RecycleView设置了数据不显示 本文主要讲一下我个人对于RecycleView的使用的一些思考以及一些常见的问题怎么解决 先来看一下使用RecycleView时常见的问题以及一些需求 这个往往是因为你没有设置LayoutMang
  • AidLearning中如何启动设置VNC和XFCE4

    第一步 xff1a 打开桌面的Service 第二步 xff1a 在Service Lists中选择中 Vncserver on port 5901 xff0c 点击启动 xff0c 点击保存 xff0c 保存服务后下次进入LECE4就会自
  • 自定义类加载器及其双亲委托机制

    自定义类加载器及其双亲委托机制 JAVA自带的类加载器自定义类加载器代码示例 双亲委托机制 JAVA自带的类加载器 JAVA的类加载器分为三种 xff0c 启动类加载器 系统类加载器 扩展类加载器以及系统类加载器 由前到后每一个都是下一个的
  • 无U盘重建Windows引导

    在我们装了Linux 43 Windows双系统又卸载后 xff0c 往往会出现引导错误 这时大多数做法都是用U盘制作PE工具 xff0c 进行重建引导 事实上 xff0c 如果是Windows系统 xff0c 且能进入 xff0c 那么就
  • 生产者-消费者问题(Linux-C版本实现)

    生产者消费者问题 xff08 英语 xff1a Producer consumer problem xff09 xff0c 也称有限缓冲问题 xff08 英语 xff1a Bounded buffer problem xff09 xff0c
  • ubuntu首次SSH使用root账户远程登录

    文章目录 步骤1 安装ssh2 检查ssh是否成功启动3 远程连接 问题 xff1a 可以远程连接普通用户 ningan xff0c 但是却不可以远程连接root用户参考 步骤 1 安装ssh ubuntu 都原生有了ssh客户端 xff0
  • 在ubuntu中安装python3

    https blog csdn net qq 21429153 article details 79188679
  • 2023python自动化抢单茅台

    2023python自动化下单抢茅台 xff08 小白篇 xff09 提前声明 本人实测一周 xff0c 均以失败告终 网络超时 当前人数过多等等原因 人格担保程序没有问题 xff0c 毕竟天底下那有这么好的事 最终结果不是重要的 xff0
  • 解决Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?

    在ubuntu上使用vim编辑器时需要安装 xff0c 安装显示错误 xff1a Unable to fetch some archives maybe run apt get update or try with fix missing
  • 大学老师告诉我:20年内程序员会被取代,未来不需要程序员...

    今天收到一个同学的邮件 xff0c 他告诉我 xff0c 他们的大学计算机老师告诉他 xff0c 以后机器人可以自动生成代码 xff0c 估计20年 xff0c 程序员就会被取代 所以他现在感觉很苦恼 xff0c 既然会被淘汰 xff0c
  • android 手机性能测试工具,不服跑个分?安卓手机性能测试工具科普

    原标题 xff1a 不服跑个分 xff1f 安卓手机性能测试工具科普 Android用户们总是热衷于手机性能的测试和比较 xff0c 这当然是作为开放平台的优良传统 xff0c 除了自身的成就感 xff0c 了解自己手机的性能到底如何也具有
  • Android 官方架构组件(二)——LiveData

    上一篇文章讲到了Android架构组件之一Lifecycle xff08 Android 官方架构组件 xff08 一 xff09 Lifecycle xff0c 现在我们再来看看另一个成员LiveData 系列文章 Android 官方架
  • .NET(C#) 通过文字和logo图片生成视频封面

    本文主要介绍 NET C 中 xff0c 使用GDI 43 Graphics 绘图的方法 xff0c 将文字和logo图内容生成视频封面图片的方法 xff0c 以及相关示例代码 原文地址 xff1a NET C 通过文字和logo图片生成视
  • 虚拟机没有声音,提示 “使用的设备标识号已超出本地系统范围”的解决方法

    本人使用的虚拟机为 VMWare15 1 0 xff0c 系统为Ubuntu16 04 xff0c 主机系统为Windows10 首先在win10下 xff0c 设置界面找到声音 xff0c 如下图 选择输出 管理声音设备 xff0c 将输

随机推荐