Android的事件分发源码分析,告别事件冲突。

一、前言

android的事件分发,大多数人都是似懂非懂,很多时候就卡在事件冲突这一步。比如在按钮上不能滑动出侧边栏,比如说ViewPager和banner冲突。我之前也是这样,然后狠下心去看了一遍源码,并且看了很多大神的博客,然后以我自身的理解配合源码来查看一个事件的传递过程。源码用的是API-8的,因为版本越高,健壮性越好,代码阅读性越差。
因为篇幅比较长,所以更底层的代码我也不准备写了,日后有机会再研究。在看博客之前,我们需要先来了解一些事件分发的基本流程,然后再一步步的深入去研究。
这其中有三个关键方法,首先我们先来理解几个方法的方法名和它们的返回值所代表的意义。

  1. dispatchTouchEvent: 简单的理解就是分发Touch事件,如果return true,表示事件已经被消费,不继续分发。return false表示没有被消费,继续分发。
  2. onInterceptTouchEvent: 拦截Touch事件,ViewGroup在dispatchTouchEvent返回之前就会调用这个方法,根据onInterceptTouchEvent的返回值决定事件是否继续往下分发。onInterceptTouchEvent的默认返回值都是false,表示不拦截。想要拦截的话需要开发者自己去重载这个方法。
  3. onTouchEvent: 处理Touch事件,如果return ture,那么这个时间就消费掉了。

细心的网友发现了,dispatch无论return true还是false,都不往下继续分发,不对吧!
dispatchTouchEvent一般不会直接return true或false。而是将事件抛给onInterceptTouchEvent和onTouchevent处理,问它们需不需要,最后再由dispatchTouchEvent进行最终的返回。这是很多新手开发者的理解误区,包括以前的我……

二、事件分发的基本过程

事件是怎么产生的这种底层问题我们先不管,我们只从知道的地方开始说起——Activity。
当一个事件开始传递后,最先接收到的是当前的Activity。然后Activity调用public boolean dispatchTouchEvent(MotionEvent ev)开始分发事件。内部又会调用PhoneWindow的内部对象DecorView的superdispatchKeyEvent,也就是ViewGroup的dispatchTouchEvent开始事件分发,DecorView是最顶层的View。

在dispatchTouchEvent方法中,ViewGroup会遍历自身的child(move事件和up事件有点不同,不会遍历child,下面会另外说明)
,再去调用子child的dispathTouchEvent方法,直到该事件被消费。传递途中还有onInterceptTouchEvent和onTouchEvent方法参与。

事件是由最外层的View开始传递,然后结果从最底层往外层返回。
如下图:ViewGroupA,ViewGroupB ,View的关系: A 是B的parent, B是C的parent。
图中的call是“调用”
这里写图片描述

我们再用代码来看下大致流程。
我们先建一个项目,然后写几个类继承FrameLayout,重写dispatchEvnet、onInterceptTouchEvent,onTouchEvent几个方法,看log输出。

ViewGroupA,B一样。

package com.aitsuki.touchevent;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;

/**
 * Created by AItsuki on 2015/12/30.
 */
public class ViewGroupA extends FrameLayout {

    public ViewGroupA(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e("Event","===ViewGroup:A======onTouchEvent===============Down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("Event","===ViewGroup:A======onTouchEvent===============Move");
                break;
            case MotionEvent.ACTION_UP:
                Log.e("Event","===ViewGroup:A======onTouchEvent===============Up");
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Move");
                break;
            case MotionEvent.ACTION_UP:
                Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Up");
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Move");
                break;
            case MotionEvent.ACTION_UP:
                Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Up");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

MyView

package com.aitsuki.touchevent;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

/**
 * Created by AItsuki on 2015/12/30.
 */
public class MyView extends View {
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e("Event","===View=============onTouchEvent===============Down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("Event","===View=============onTouchEvent===============Move");
                break;
            case MotionEvent.ACTION_UP:
                Log.e("Event","===View=============onTouchEvent===============Up");
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.e("Event","===View=============dispatchTouchEvent=========Down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e("Event","===View=============dispatchTouchEvent=========Move");
                break;
            case MotionEvent.ACTION_UP:
                Log.e("Event","===View=============dispatchTouchEvent=========Up");
                break;
        }
        return super.dispatchTouchEvent(event);
    }
}

布局activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.aitsuki.touchevent.ViewGroupA
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#FF6A6A"
        android:layout_gravity="center">

        <com.aitsuki.touchevent.ViewGroupB
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_gravity="center"
            android:background="#9ACD32">

            <com.aitsuki.touchevent.MyView
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:background="#1E90FF"
                android:layout_gravity="center"/>

        </com.aitsuki.touchevent.ViewGroupB>

    </com.aitsuki.touchevent.ViewGroupA>

</FrameLayout>

布局预览:
这里写图片描述
红色:ViewGroupA
绿色:ViewGroupB
蓝色:View
现在我点击一下蓝色区域(View),看Log输出。
这里写图片描述
我们给ViewGroupA加上拦截之后再看看(让onInterceptTouchEvent返回true)
这里写图片描述
好了,基本流程和我们的图是一致的。
但是要注意 一、前言 的那段红字

三、 View的源码走读

大概理解了事件的传递过程之后,我们来看一下源码。
为什么先看View的源码而不看ViewGroup的原因有两点:
1. View是ViewGroup的父类,ViewGroup的onTouchEvent方法继承自View, 并没有重写。
2. View没有child,事件传递简单,不会打消各位的阅读源码积极性。
那么,开始吧。

先来看View的dispatchTouchView方法,我直接在上面注释。
mViewFlags:一种通过位运算记录开关的方式。mViewFlags一个32位的int值,用每一位的0或1记录属性。比如第1位的0和1记录focusable(是否可以获取焦点)

    /**
     * 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) {
        // 如果这个View设置了触摸监听onTouchListener并且View是可用的,并且onTouch返回的是true
        // 那么事件就消费掉了,传递结束。
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            return true;
        }
        // 否则,交给onTouchEvent处理(View中没有onInterceptTouchEvent方法,因为它没有
        // child了,不需要有拦截方法)
        return onTouchEvent(event);
    }

// 注意看谷歌工程师的注释,它们称消费事件的View为target view,如果这里的dispatch或者说
// onTouchEvent返回true,那么这个View就是target View了。先记下来

继续看onTouchEvent

 /**
     * Implement this method to handle touch screen motion events.
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;
        // 如果View不可用,但它却是可点击的(clickable属性),那么仍然消费这个事件(但是不执行任何
        // 操作,也就是不会响应)。也就是说,只要有clickable属性,那么这个点击事件就必然被消费掉。
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
        }

        // 这个是触摸代理,就是点击另一个View,这个view会响应点击事件。默认是null,开发者可以通过
        // setTouchDelegate设置。详情请自行查看TouchDelegate
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        // 如果View是可点击的,那么消费掉这个事件,否则返回给上层处理(parent)
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            //...... 此处省略N行代码,没有return语句,我们pass。
            return true;
        }
        return false;
    }

好了,View处理事件的源码就这么点。一会就看完了,挺简单的。
从上面这两段代码可以得出的结论:

结论1:onTouch优先于onTouchEvent执行,并且onTouch消费掉事件后,onTouEvent不会再执行。
结论2:如果View是可点击的(clickable),那么事件一定会被消费掉,不会再继续传递。

我们来验证一下结论,用的还是 二、事件分发的基本过程 的那个项目。
我们给MyView设置touchListener并return true,然后点击蓝色区域看看log输出

activity代码:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MyView view = (MyView) findViewById(R.id.view);
        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.e("Event","===View=============onTouch====================Down");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.e("Event","===View=============onTouch====================Move");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.e("Event","===View=============onTouch====================Up");
                        break;
                }
                return true;
            }
        });
    }
}

这里写图片描述
结论1:onTouchEvent没有执行,验证正确。

将MainActivity设置侦听的代码注释掉,给View设置clickable属性,测试结论3

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MyView view = (MyView) findViewById(R.id.view);
        view.setClickable(true);
//        view.setOnTouchListener(new View.OnTouchListener() {
//            @Override
//            public boolean onTouch(View v, MotionEvent event) {
//                switch (event.getAction()) {
//                    case MotionEvent.ACTION_DOWN:
//                        Log.e("Event","===View=============onTouch====================Down");
//                        break;
//                    case MotionEvent.ACTION_MOVE:
//                        Log.e("Event","===View=============onTouch====================Move");
//                        break;
//                    case MotionEvent.ACTION_UP:
//                        Log.e("Event","===View=============onTouch====================Up");
//                        break;
//                }
//                return true;
//            }
//        });
    }
}

这里写图片描述
View执行了onTouchEvent,消费掉了事件,不再传递,结论3验证正确

请各位网友理解了View的事件传递的结论消化之后再继续往下看,因为ViewGroup中多次调用到super.dispatchTouchEvent, 其实也就是调用View的dispatchTouchEvent,因为View就是ViewGroup的父类。

四、ViewGroup源码走读

因为注释比较多,所以有点影响阅读性,最好可以配合没有注释的源码(api-8 Android2.2)一起阅读。同时思考一下我的分析是否和你的一样,有什么错误也可以在评论中指出。

我们只需要看dispatch的源码就可以了,onInterceptTouchEvent默认都是return false,没有onTouchEvent。
和View一样,也是使用注释的方式来说明。

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 获取事件的类型和触摸坐标
        final int action = ev.getAction();
        final float xf = ev.getX();
        final float yf = ev.getY();
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        // mTempRect初始是null的,这个Rect对象主要是用来记录child的可点击范围。
        final Rect frame = mTempRect;

        // mGroupFlags和mViewFlags一样是记录当前控件的状态。这里记录的是“是否允许拦截”这个属性。
        // 可以通过requestDisallowInterceptTouchEvent这个方法进行设置,如果设置了这个属性,
        // 那么ViewGroup就不会拦截child的事件了。
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

        // 如果是down事件,就遍历child进行分发。
        if (action == MotionEvent.ACTION_DOWN) {

            // 两位谷歌工程师在聊天么=。=,大概意思就是:
            // 为什么一个View响应完down事件后还没有消失,继续响应了第二次down事件。
            // xxx: 我们可能应该发送一个up事件,而不是down……

            // target:在之前也说过了,响应了down事件的那个View就是target,而在up或者cancel
            // 事件执行后,这个target应该会被重置为null。
            // 但是这里居然不是空的。博主我也不知道什么回事=。=

            // 如果不是null,那么就让它重置为null。
            if (mMotionTarget != null) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }


            // If we're disallowing intercept or if we're allowing and we didn't 
            // intercept
            // 这里判断disallowIntercept有点多余,因为执行up或者cancel之后,这个属性就会被重
            // 置为false(往下找)。而requestDisallowInterceptTouchEvent方法一般在child的
            // dispatchTouchEvent中调用,但是事件还没有传到chid,那么这个方法也就不会执行,
            // disallowIntercept肯定也只能是false。

            // 我们无视掉这里的disallowIntercept,这里判断是否拦截child的事件。
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                // reset this event's action (just to protect ourselves)
                // 重置这个事件为donw事件,只是为了保证健壮性。上面那个disallowIntercept也是
                // 为了健壮性么=。=
                ev.setAction(MotionEvent.ACTION_DOWN);
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                // 不得不说谷歌工程师的注释也是很详细的=。=
                // 我们想要分发这个down事件,遍历子View看谁可以持有它,从最上层的子View开始。
                final int scrolledXInt = (int) scrolledXFloat;
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;
                final int count = mChildrenCount;
                // 这里开始遍历所有child
                for (int i = count - 1; i >= 0; i--) {
                    final View child = children[i];
                    // 如果child是可见的,或者child正在进行动画(动画中的View这个我没细看
                    // ,无视掉好了)。
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                        // 获得child的有效点击范围,判断点击事件是否点在此child中。
                        child.getHitRect(frame);
                        if (frame.contains(scrolledXInt, scrolledYInt)) {
                            // offset the event to the view's coordinate system
                            // 获取到事件的坐标是屏幕的绝对坐标,要转成child的相对坐标。
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            ev.setLocation(xc, yc);

                            // 如果点击事件在此child中,重置属性:CANCEL_NEXT_UP_EVENT
                            // 该属性的描述:Indicates whether the view is temporarily 
                            // detached。
                            // 标记哪一个View是暂时和parent分离,就是和当前这个ViewGroup分
                            // 离。
                            // 有什么应用场景我也不知道,不过abslistView中是用到了,那边的源码

                            // PS:位运算没忘吧=。=,比如CANCEL_NEXT_UP_EVENT = 0000 1000 
                            // 取反 1111 0111。  与上这个数就是取消 CANCEL_NEXT_UP_EVENT
                            // 这个属性……

                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;

                            // 然后这里调用child的事件分发
                            // 这里会出现几种情况
                            // 1:child是View,那么回想一下View的源码吧(三、View源码走读)
                            // 如果child消费了此down事件,那么这个child就是target了。
                            // 如果不消费,那么返回给当前的ViewGroup的onTouchEvent消费。

                            // 2:child是ViewGroup,那么继续调用child的dispatch,
                            // 继续遍历child的child(噗,孙子),直到有响应这个事件的。
                            // 如果最底层的child也是ViewGroup,那么请直接跳过这个遍历,
                            // 往下看…… if(target == null)那里。
                            // 调用super.dispatchEvent。
                            // 也就是说,如果最底层的child是ViewGroup,那么将它作为View处理。
                            if (child.dispatchTouchEvent(ev))  {
                                // Event handled, we have a target now.
                                // 如果响应了down事件,那么这个child就是target
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }

// 思考:dispatch中调用child的dispatch方法,这看起来是不是像递归遍历。直到有child响应down事件,
// 否则将所有child遍历完后这个事件流产。

// 注意:child有可能是View,也有可能是ViewGroup。它们两个的dispatch方法是不同的。如果是View,
// 就代表这个事件分发已经到了最底下的View了。

//===================================================================================
        // 判断事件是否是up或者cancel
        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);

        // 如果是up或者cancel事件,将FLAG_DISALLOW_INTERCEPT(disallowIntercept是通过
        // 这个值计算的)这个属性移除。
        // 就是说,既然是up事件和cancel事件已经接收到,那么就代表这在target上的事件结束了,
        // 重置这个属性,不然会影响到下一个事件。
        if (isUpOrCancel) {
            // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
            // 我们已经复制了上一个的状态到变量,所以这里就影响了下一个事件。
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // The event wasn't an ACTION_DOWN, dispatch it to our target if
        // we have one.
        // 如果traget存在,并且这个事件不是down,那么就将事件交给target处理
        // (谁响应了down事件,后续事件就交给谁处理)
        final View target = mMotionTarget;

        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.
            // 如果target为null,那么代表down事件没有被响应(也可能是target的后续事件被拦截,
            // 那么target也会被重置为null)
            ev.setLocation(xf, yf);
            // 如果当前的View或者说ViewGroup已经从parent中分离,那么将事件改为cancel
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }
            // 这里关键了,如果这个ViewGroup没有child,那么它就会走到这里,然后调用View的事件
            // 分发, 将自己作为一个View处理。
            // 当返回dispatch返回true之后,这个ViewGroup本身就是target了,不过这个target引用
            // 是在parent中,不是当前的=。=
            return super.dispatchTouchEvent(ev);
        }

        // 代码能走到这里的话就代表,这不是一个down事件。

        // if have a target, see if we're allowed to and want to intercept its
        // events
        // 如果我们有一个target,我们就会将后续的事件都交给target处理(跳过这个if判断直接往
        // 下看)。
        // 结合前面分析,从这里看出,如果某个child请求了requestDisallow,那么child就肯定能
        // 接收到move和up事件,前提是没有拦截down事件。

        // 假设我们拦截了后续事件(不拦截child的down事件,但是拦截了child的其他事件)
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            ev.setAction(MotionEvent.ACTION_CANCEL);
            ev.setLocation(xc, yc);
            // 将事件改成cancel,然后交给target的dispatch处理。(这里可以看出,如果有
            // target响应的down事件,但是target后续事件被拦截,那么就会传一个cancel给target)

            // 不管它有没有响应,我们都将target置为空,就是说,后续事件当做没有target来处理。
            if (!target.dispatchTouchEvent(ev)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }

        // 如果事件是up或者cancel,那么将target重置为null。因为响应down事件的就是target,
        // 既然已经抬起手指了,那target就没了。
        if (isUpOrCancel) {
            mMotionTarget = null;
        }

        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        // 将坐标转成target的相对坐标。
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);

        // 如果target已经被分离出去(相当于remove),那么将事件改成cancel。
        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            mMotionTarget = null;
        }

        // 将所有后续事件都交给target处理。
        return target.dispatchTouchEvent(ev);
    }

注释太多看起来有点乱,不过没关系,大家配合无注释的代码一起看就行了。我只是将每一行的代码的作用注释出来,各种结论还是需要推测。

五、结论

现在我们来总结一下分析源码得出的一些结论:

结论1:onTouch优先于onTouchEvent执行,并且onTouch消费掉事件后,onTouEvent不会再执行。

结论2:如果View是可点击的(clickable),那么事件一定会被消费掉,不会再继续传递。

结论3: 如果ViewGroup在onInterceptTouchEvent中拦截了child的事件,那么这个事件会交给ViewGroup的onTouchEvent处理。

结论4:onInterceptTouchEvent方法在一次事件序列(down到up或者cancel的过程)中,只要返回true就不会再调用,或者说只要拦截过一次之后就不会再调用,直到下一次down事件开始前。

结论5:响应了down事件的View被称为target,Android会将后续事件都交给target处理。

结论6:在结论5的基础上,如果onInterceptTouchEvent中拦截了target的其他事件,比如move或up,那么target就会接受到一个cancel事件,并且将target置为null,后续的事件交给target的parent处理。(比如,我可以让child响应down事件,然后只拦截它的move事件,那么我就可以接受到除了down的所有后续事件了,而child则会接收到一个cancel事件,这样就可以解决滑动事件和按钮冲突的问题了。)

结论7:在结论6的基础上,child可以通过requestDisallowInterceptTouchEvent请求parent不拦截他的事件,前提是child能响应到down事件。(例如:parent在onIntercept中拦截了事件,child就没机会请求了。再例如:parent不拦截down事件,但是拦截了move和up事件,这时候requestDisallowInterceptTouchEvent就派上用场了)

结论8: 如果一个View在处理一个事件序列(down到up或者cancel的过程)的时候,parent将他remove掉了,那么这个View会接收到一个cancel事件。

结论的验证过程都很简单,因为文章篇幅已经太长了不打算贴出来,请自行验证结论的正确性,也可惜选择相信我的验证结果。

六、写在后面

这博客真的很难写,花了两天多的时间。不知道应该怎么写才能更加简洁易懂,最后干脆以注释的方式了。
虽然写了七八个结论,但是这并不是全部,我也很难将所有结论一个一个列出来。

分析源码,读懂源码的好处就是,当你发现事件冲突的时候,不会像无头苍蝇一样在百度胡乱搜方案,最后还搞得一头雾水。而是让你自己有能力解决冲突,能找到冲突的源头。

至于解决事件冲突的一些案例,我有空的话可能会整理几个出来,但是写着博客有种身心疲惫的感觉_(:з」∠)_,休息一段时间再说,就这样,下次见。

©️2020 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值