美文网首页
2019-12-15

2019-12-15

作者: 遥望星空forward | 来源:发表于2019-12-15 21:16 被阅读0次

    Android事件分发机制源码解析

    我们都知道,事件分发在Android的知识体系中是相当重要的一环,只要我们熟悉事件的分发流程,我们无论是在解决view之间的点击响应失效还是滑动冲突都是相对比较容易的,尤其是在自定义view中如果我们熟悉事件分发就能更好的处理和扩展控件的功能,同时,事件分发机制也可以说是面试的必问点之一,所以无论是在找工作还是在自身技术的进阶中,事件分发都是我们必须要掌握的重要知识点。最近更好重温了一遍事件分发的源码,所以就此做个总结和记录。

    1、事件分发流程

    下面先简单绘制和介绍了下view的坐标体系:

    当我们的手指触摸的屏幕上的时候就会产生一个MotionEvent, 它主要有如下三种事件类型:

    • MotionEvent.ACTION_DOWN :手指刚接触到屏幕时触发
    • MotionEvent.ACTION_MOVE : 手指在屏幕上进行移动时触发
    • MotionEvent.ACTION_UP : 手指从屏幕上抬起的时候触发

    点击事件的分发过程主要由三个重要方法来共同完成的,事件的分发可以说就是根据这三个方法的返回值来决定事件的分发流程的:

    • public boolean dispatchTouchEvent(MotionEvent event) :分发Touch事件。用来进行事件的分发,如果事件能够传递给当前的View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响。表示是否消耗当前事件。
    • public boolean onInterceptTouchEvent(MotionEvent ev) :拦截Touch事件。在dispatchTouchEvent方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
    • public boolean onTouchEvent(MotionEvent ev) :处理Touch事件。在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

    下面提供了一个伪代码和事件分发流程图可以很好的展示和总结了事件的分发过程:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean intercepted = onInterceptTouchEvent(ev);
        if (intercepted) {
            return onTouchEvent(ev);
        } else {
            boolean childDisposeEvent = childView.dispatchTouchEvent(ev);
            if (childDisposeEvent) {
                return childDisposeEvent;
            } else {
                return onTouchEvent(ev);
            }
        }
    }
    

    从上面的事件分发流程图中可以看到事件的流向是根据三个事件分发方法的返回值来进行决定的。上面描述的都是MotionEvent.ACTION_DOWN的事件走向,至于MotionEvent.ACTION_MOVE、MotionEvent.ACTION_UP则会根据最终消耗事件的View(以下简称ConsumeView)的位置选择相应的传输路径,并且不会再分发到这个ConsumeView的子View(如果有的话)的事件分发方法中,如果ACTION_DOWN事件是在ConsumeView的dispatchTouchEvent中消费的,那么ACTION_MOVE、ACTION_UP事件也会传到这个ConsumeView的dispatchTouchEvent方法为止并停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,ACTION_MOVE、ACTION_UP事件会走dispatchTouchEvent方法但不会再走这个ConsumeView的onInterceptTouchEvent拦截方法,因为这个View需要处理消耗事件,所以无需再调用拦截方法判断需不需要对事件进行拦截处理了,那么就会把ACTION_MOVE或ACTION_UP事件传给ConsumeView的onTouchEvent处理并结束传递。如果经过整个事件分发流程都没有View消耗事件,那么ACTION_DOWN事件最终则会被Activity的onTouchEvent进行处理,并且ACTION_MOVE、ACTION_UP事件也不会往下分发,直接交由Activity的onTouchEvent进行处理。

    其实事件是从ACTION_DOWN的走向来决定其余事件的分发,也可以用行军打仗进行这样的形容描述:ACTION_MOVE和ACTION_UP是一支大军,ACTION_DOWN是一支斥候,一开始ACTION_DOWN从营地(Activity)出发,按照一张地图路线(事件分发流程图),沿路进行侦测,当发现敌人营地有敌人存在时(比如onTouchEvent返回true消耗事件),并且找到避开敌人营地附近(onInterceptTouchEvent)的拦截路径,此时大军ACTION_MOVE和ACTION_UP就会根据斥候ACTION_DOWN侦测好的路线错开无用的多余的路径进行快速移动到达敌人的营地进行攻击作战,如果斥候ACTION_DOWN在一张地图路线下来都没有发现敌人,则大军则会继续驻扎在营地,无需出战,并继续在营地对士兵进行常规的操练(Activity的onTouchEvent)。

    好了,上面比较啰嗦也比较绕,下面我们可以直接打印看效果,这样对事件分发有个大概也便于接下来分析事件分发源码。我们先新建一个简单的项目EventProcess,里面主要有三个view:

    public class FirstGroupView extends LinearLayout {
    
        public FirstGroupView(Context context) {
            super(context);
        }
    
        public FirstGroupView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public FirstGroupView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            Log.i("eventProcess-->","FirstGroupView---dispatchTouchEvent---" + Utils.getMotionEventType(ev));
            return super.dispatchTouchEvent(ev);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            Log.i("eventProcess-->","FirstGroupView---onInterceptTouchEvent---" + Utils.getMotionEventType(ev));
            return super.onInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.i("eventProcess-->","FirstGroupView---onTouchEvent---" + Utils.getMotionEventType(event));
            return super.onTouchEvent(event);
        }
    }
    
    public class SecondGroupView extends LinearLayout {
    
        public SecondGroupView(Context context) {
            super(context);
        }
    
        public SecondGroupView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public SecondGroupView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            Log.i("eventProcess-->","SecondGroupView---dispatchTouchEvent---" + Utils.getMotionEventType(ev));
            return super.dispatchTouchEvent(ev);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            Log.i("eventProcess-->","SecondGroupView---onInterceptTouchEvent---" + Utils.getMotionEventType(ev));
            return super.onInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.i("eventProcess-->","SecondGroupView---onTouchEvent---" + Utils.getMotionEventType(event));
            return super.onTouchEvent(event);
        }
    }
    
    public class DisposeView extends View {
        public DisposeView(Context context) {
            super(context);
        }
    
        public DisposeView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public DisposeView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            Log.i("eventProcess-->","DisposeView---dispatchTouchEvent---" + Utils.getMotionEventType(ev));
            return super.dispatchTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.i("eventProcess-->","DisposeView---onTouchEvent---" + Utils.getMotionEventType(event));
            return super.onTouchEvent(event);
        }
    }
    
    public class Utils {
        public static String getMotionEventType(MotionEvent ev) {
            String motionEventType = "";
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                motionEventType = "ACTION_DOWN";
            } else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
                motionEventType = "ACTION_MOVE";
            } else if (ev.getAction() == MotionEvent.ACTION_UP){
                motionEventType = "ACTION_UP";
            }
            return motionEventType;
        }
    }
    
    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_event);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            Log.i("eventProcess-->","MainActivity---dispatchTouchEvent---" + Utils.getMotionEventType(ev));
            return super.dispatchTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            Log.i("eventProcess-->","MainActivity---onTouchEvent---" + Utils.getMotionEventType(event));
            return super.onTouchEvent(event);
        }
    }
    
    <?xml version="1.0" encoding="utf-8"?>
    <com.example.resourcechecked.view.FirstGroupView
        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"
        android:background="@color/colorWhite"
        tools:context=".MainActivity">
    
        <com.example.resourcechecked.view.SecondGroupView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="60dp"
            android:background="@color/colorPrimary">
    
            <com.example.resourcechecked.view.DisposeView
                android:id="@+id/disposeView"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginTop="120dp"
                android:background="@color/colorAccent" />
    
        </com.example.resourcechecked.view.SecondGroupView>
    
    </com.example.resourcechecked.view.FirstGroupView>
    

    我们可以看到这些代码也非常简单,只是对View的分发事件方法进行了打印,在页面布局文件中我们可以看到布局是FirstGroupView-->SecondGroupView-->DisposeView。

    当我们不去对事件流程进行处理时我们看看打印效果,我们可以看到当没有任何View对ACTION_DOWN事件进行拦截消耗,最终事件会回到MainActivity中,ACTION_MOVE和ACTION_UP事件也不会往下传递,会在MainActivity的dispatchTouchEvent方法中分发到onTouchEvent进行处理。

    I/eventProcess-->: MainActivity---dispatchTouchEvent---ACTION_DOWN
    I/eventProcess-->: FirstGroupView---dispatchTouchEvent---ACTION_DOWN
    I/eventProcess-->: FirstGroupView---onInterceptTouchEvent---ACTION_DOWN
    I/eventProcess-->: SecondGroupView---dispatchTouchEvent---ACTION_DOWN
    I/eventProcess-->: SecondGroupView---onInterceptTouchEvent---ACTION_DOWN
    I/eventProcess-->: DisposeView---dispatchTouchEvent---ACTION_DOWN
    I/eventProcess-->: DisposeView---onTouchEvent---ACTION_DOWN
    I/eventProcess-->: SecondGroupView---onTouchEvent---ACTION_DOWN
    I/eventProcess-->: FirstGroupView---onTouchEvent---ACTION_DOWN
    I/eventProcess-->: MainActivity---onTouchEvent---ACTION_DOWN
    I/eventProcess-->: MainActivity---dispatchTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---onTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---dispatchTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---onTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---dispatchTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---onTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---dispatchTouchEvent---ACTION_UP
    I/eventProcess-->: MainActivity---onTouchEvent---ACTION_UP
    

    当SecondGroupView的onInterceptTouchEvent返回true拦截传递事件时,事件的传递是怎么样的,从打印出来的数据看到事件一旦被SecondGroupView拦截,那么事件不会再分发到DisposeView,会直接分发到SecondGroupView的onTouchEvent中进行处理,此时因为onTouchEvent不处理事件所以此时事件会继续往上进行传递,最后返回到MainActivit

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("eventProcess-->","SecondGroupView---onInterceptTouchEvent---" + Utils.getMotionEventType(ev));
        // return super.onInterceptTouchEvent(ev);
        return true;
    }
    
    I/eventProcess-->: MainActivity---dispatchTouchEvent---ACTION_DOWN
    I/eventProcess-->: FirstGroupView---dispatchTouchEvent---ACTION_DOWN
    I/eventProcess-->: FirstGroupView---onInterceptTouchEvent---ACTION_DOWN
    I/eventProcess-->: SecondGroupView---dispatchTouchEvent---ACTION_DOWN
    I/eventProcess-->: SecondGroupView---onInterceptTouchEvent---ACTION_DOWN
    I/eventProcess-->: SecondGroupView---onTouchEvent---ACTION_DOWN
    I/eventProcess-->: FirstGroupView---onTouchEvent---ACTION_DOWN
    I/eventProcess-->: MainActivity---onTouchEvent---ACTION_DOWN
    I/eventProcess-->: MainActivity---dispatchTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---onTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---dispatchTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---onTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---dispatchTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---onTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---dispatchTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---onTouchEvent---ACTION_MOVE
    I/eventProcess-->: MainActivity---dispatchTouchEvent---ACTION_UP
    I/eventProcess-->: MainActivity---onTouchEvent---ACTION_UP
    

    我们再来看看当SecondGroupView的onTouchEvent返回true消耗传递事件时事件的分发流程是怎么样的,从打印的数据可以看到ACTION_DOWN事件从Activity开始分发,一直往下执行到SecondGroupView的onTouchEvent返回true消耗事件,此时事件就不会往上传递了,ACTION_MOVE和ACTION_UP也会从上往下开始分发,但不会分发到DisposeView,SecondGroupView的onInterceptTouchEvent也不再执行拦截,因为onTouchEvent消耗事件,无需onInterceptTouchEvent进行判断是否需要拦截事件。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("eventProcess-->","SecondGroupView---onTouchEvent---" + Utils.getMotionEventType(event));
        // return super.onTouchEvent(event);
        return true;
    }
    
    MainActivity---dispatchTouchEvent---ACTION_DOWN
    FirstGroupView---dispatchTouchEvent---ACTION_DOWN
    FirstGroupView---onInterceptTouchEvent---ACTION_DOWN
    SecondGroupView---dispatchTouchEvent---ACTION_DOWN
    SecondGroupView---onInterceptTouchEvent---ACTION_DOWN
    DisposeView---dispatchTouchEvent---ACTION_DOWN
    DisposeView---onTouchEvent---ACTION_DOWN
    SecondGroupView---onTouchEvent---ACTION_DOWN
    MainActivity---dispatchTouchEvent---ACTION_MOVE
    FirstGroupView---dispatchTouchEvent---ACTION_MOVE
    FirstGroupView---onInterceptTouchEvent---ACTION_MOVE
    SecondGroupView---dispatchTouchEvent---ACTION_MOVE
    SecondGroupView---onTouchEvent---ACTION_MOVE
    MainActivity---dispatchTouchEvent---ACTION_MOVE
    FirstGroupView---dispatchTouchEvent---ACTION_MOVE
    FirstGroupView---onInterceptTouchEvent---ACTION_MOVE
    SecondGroupView---dispatchTouchEvent---ACTION_MOVE
    SecondGroupView---onTouchEvent---ACTION_MOVE
    MainActivity---dispatchTouchEvent---ACTION
    FirstGroupView---dispatchTouchEvent---ACTION_UP
    FirstGroupView---onInterceptTouchEvent---ACTION_UP
    SecondGroupView---dispatchTouchEvent---ACTION_UP
    SecondGroupView---onTouchEvent---ACTION_UP
    

    还有很多种情况,大家可以根据上面的事件分发的伪代码和事件分发的流程图可以分析出各种情况下事件的分发流程,也可以像上面一样创建一个简单的项目自己去打印log查看各种条件下事件分发的流程,这样可以对事件分发流程有更加清晰的认识,另外,上面只是相当于对事件分发流程的具体总结,我们还要做到知其然知其所以然,所以我们还要去查看事件分发的源码。

    2、源码解析

    事件MotionEvent产生后,最先传递给当前的Activity,由Activity的dispatchTouchEvent开始进行事件分发,具体是通过Activity的Window来完成的,Window的作用主要是管理 View 的创建,可以控制顶级View的外观和行为策略,以及与 ViewRootImpl 的交互,将 Activity 与 View 解耦,一个 Activity 对应一个 Window 也就是 PhoneWindow,一个 PhoneWindow 持有一个 DecorView 的实例,Window会把事件传递到DecorView,DecorView是一个FrameLayout,同时它也是根View,我们在布局文件中设置的View都是它的子View,此时事件就可从Activity传递到View了。

    我们先来看Activity的dispatchTouchEvent方法,可以看到它调用了Window的superDispatchTouchEvent,如果返回true说明下发事件中有View消耗了事件,如果返回false,则会调用Activity自身的onTouchEvent对事件进行处理。

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

    进入Window的superDispatchTouchEvent,我们会发现Window是一个抽象类,而这个方法是一个抽象方法,那么肯定有一个它的子类去实现了这个方法,我们需要找到这个Window的实现类,这个实现类其实就是PhoneWindow,我们从这个类的说明描述中可以知道Window这个抽象类的唯一实现是PhoneWindow。

    public abstract boolean superDispatchTouchEvent(MotionEvent event);
    
    /**
     * Abstract base class for a top-level window look and behavior policy.  An
     * instance of this class should be used as the top-level view added to the
     * window manager. It provides standard UI policies such as a background, title
     * area, default key processing, etc.
     *
     * <p>The only existing implementation of this abstract class is
     * android.view.PhoneWindow, which you should instantiate when needing a
     * Window.
     */
    public abstract class Window {
        ...
    } 
    

    现在我们看看PhoneWindow的superDispatchTouchEvent方法实现,这个方法一目了然,这里调了DecorView的superDispatchTouchEvent方法,而DecorView是根View,此时,事件分发就从Activity传递到了View。

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
        
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    
    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
            ...
            public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }
        ...
    }
    

    DecorView是一个FrameLayout,它继承了ViewGroup,这里进入了ViewGroup的dispatchTouchEvent方法,因为这个方法有点长,我们就分段进行分析说明,可以看到分发事件一开始就对ACTION_DOWN事件做了一些初始化,然后判断actionMasked == MotionEvent.ACTION_DOWN或者 mFirstTouchTarget != null时会去执行onInterceptTouchEvent方法判断是否要对事件进行拦截,当我们对事件进行拦截时intercepted会被置为true。

    mFirstTouchTarget其实是一个单链表结构,当它的子View需要对分发事件进行处理时,这个mFirstTouchTarget就会被赋值并指向子View,如果子View没有消耗事件或者onInterceptTouchEvent返回true拦截事件那么mFirstTouchTarget为null,接下去的代码分析中也可以看到。如果mFirstTouchTarget为null那么ACTION_MOVE、ACTION_UP到来时条件actionMasked ==MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null就为false,那么onInterceptTouchEvent也不会被执行,直接会intercepter=true默认拦截,接下去的代码中会看到拦截后会调用onTouchEvent方法并且不会往下进行分发事件,这个也跟上面说的那样事件拦截之后就不会再执行onInterceptTouchEvent方法判断是否要拦截事件了。

    这里还有个FLAG_DISALLOW_INTERCEPT标记位,这个一般是在子View想要处理触摸事件,然后调用它的父View的requestDisallowInterceptTouchEvent方法设置这个标记位从而让父View不要对事件进行拦截,对父View的事件分发进行干预,一般用在解决一些滑动冲突的问题时,但是对ACTION_DOWN事件除外,因为分发事件方法一开始就对ACTION_DOWN事件做了一些初始化,会在resetTouchState方法中对这个标记重置。

    // Handle an initial down.
    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();
    }
    
    // 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;
    }
    
    /**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    

    接着我们再看事件没有进行拦截也就是intercepted = false时事件走向,可以看到首先会去遍历它的所有子View,获取到子View之后会去判断子View是否可以接收到事件,能接收到到事件需要满足两个条件:一是可以接收指针事件,二是事件坐标落在子View的区域内,如果这两个条件都满足,则事件就会传递给这个子View进行处理。

    final int childrenCount = mChildrenCount;
    if (newTouchTarget == null && childrenCount != 0) {
        final float x = ev.getX(actionIndex);
        final float y = ev.getY(actionIndex);
        // Find a child that can receive the event.
        // Scan children from front to back.
        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
        final View[] children = mChildren;
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(
                    childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(
                    preorderedList, children, childIndex);
    
            // If there is a view that has accessibility focus we want it
            // to get the event first and if not handled we will perform a
            // normal dispatch. We may do a double iteration but this is
            // safer given the timeframe.
            if (childWithAccessibilityFocus != null) {
                if (childWithAccessibilityFocus != child) {
                    continue;
                }
                childWithAccessibilityFocus = null;
                i = childrenCount - 1;
            }
    
            if (!child.canReceivePointerEvents()
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }
    
            newTouchTarget = getTouchTarget(child);
            if (newTouchTarget != null) {
                // Child is already receiving touch within its bounds.
                // Give it the new pointer in addition to the ones it is handling.
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }
    
            resetCancelNextUpFlag(child);
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                // Child wants to receive touch within its bounds.
                mLastTouchDownTime = ev.getDownTime();
                if (preorderedList != null) {
                    // childIndex points into presorted list, find original index
                    for (int j = 0; j < childrenCount; j++) {
                        if (children[childIndex] == mChildren[j]) {
                            mLastTouchDownIndex = j;
                            break;
                        }
                    }
                } else {
                    mLastTouchDownIndex = childIndex;
                }
                mLastTouchDownX = ev.getX();
                mLastTouchDownY = ev.getY();
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }
    
            // The accessibility focus didn't handle the event, so clear
            // the flag and do a normal dispatch to all children.
            ev.setTargetAccessibilityFocus(false);
        }
        if (preorderedList != null) preorderedList.clear();
    }
    
    if (newTouchTarget == null && mFirstTouchTarget != null) {
        // Did not find a child to receive the event.
        // Assign the pointer to the least recently added target.
        newTouchTarget = mFirstTouchTarget;
        while (newTouchTarget.next != null) {
            newTouchTarget = newTouchTarget.next;
        }
        newTouchTarget.pointerIdBits |= idBitsToAssign;
    }
    
    /**
     * Returns whether this view can receive pointer events.
     *
     * @return {@code true} if this view can receive pointer events.
     * @hide
     */
    protected boolean canReceivePointerEvents() {
        return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
    }
    
    /**
     * Returns true if a child view contains the specified point when transformed
     * into its coordinate space.
     * Child must not be null.
     * @hide
     */
    @UnsupportedAppUsage
    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;
    }
    

    当子View满足上面说的两个条件后则会调用dispatchTransformedTouchEvent方法把事件传递给子View,可以看到调用了子View的dispatchTouchEvent,这样事件就完成一轮分发并交给子View进行分发处理了。

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
            ...
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            ...
    }
    

    当dispatchTransformedTouchEvent返回了true时则说明子View消耗了事件,往上看看上面的代码,此时会执行addTouchTarget方法并跳出循环,这个方法是对mFirstTouchTarget进行赋值的,可以看到mFirstTouchTarget是一种单链表结构并指向子View,从一开始分析源码的时候就知道mFirstTouchTarget是否被赋值将会影响到事件的分发流程。

    当dispatchTransformedTouchEvent返回了false时,则说明子View没有对事件进行消耗处理,此时就不会执行addTouchTarget方法,所以也就不会对mFirstTouchTarget进行赋值了,mFirstTouchTarget为null,所以,其余事件则会被ViewGroup默认拦截intercepted = true。

    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    alreadyDispatchedToNewTouchTarget = true;
    break;
    
    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
    

    我们接着往下看,当事件一开始就被拦截了intercepted = true或者经过遍历子View,调用子View的dispatchTouchEvent返回false,子View没有消耗事件,所以并没有对mFirstTouchTarget赋值,这是就会执行到下面这段代码,可以看到,这里也是调用了dispatchTransformedTouchEvent,不过传递的第三个参数child是null,所以会执行这句代码super.dispatchTouchEvent(event),到了这里,就转到了View的dispatchTouchEvent方法。

    // Dispatch to touch targets.
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    }
    

    View的dispatchTouchEvent方法比起ViewGroup的就比较简单一点了,因为它是单独元素所以不需要向下分发事件,这里会去判断View是否设置了mOnTouchListener,如果mOnTouchListener.onTouch的返回值为true,则不会去执行onTouchEvent,这也说明了mOnTouchListener的优先级是要高于onTouchEvent方法的,这样就可以在外部对事件进行自行处理了。

    public boolean dispatchTouchEvent(MotionEvent event) {
            ...
        boolean result = false;
            ...
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
    
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ...
        return result;
    }
    

    接下来再简单分析下View的onTouchEvent方法的实现,可以看到只要CLICKABLE或者LONG_CLICKABLE只要一个设置成true,clickable就等于true,那么onTouchEvent就会返回true消耗这个事件,从代码中还可以看出就算View设置成DISABLED状态,依然会执行onTouchEvent返回clickable。至于mTouchDelegate.onTouchEvent就相当于代理,跟上面提到mOnTouchListener.onTouch在思想上其实差不多。在事件为ACTION_UP时会执行performClickInternal方法,这个方法内部又会调用执行performClick方法,如果有设置点击监听的话,performClick中会去执行mOnClickListener.onClick。

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
    
        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;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
    
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                    // 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();
                        }
                        if (!post(mPerformClick)) {
                            performClickInternal();
                        }
                    }             
                    ...
                    break;
    
                case MotionEvent.ACTION_DOWN:
                    ...
                    break;
    
                case MotionEvent.ACTION_CANCEL:
                    ...
                    break;
    
                case MotionEvent.ACTION_MOVE:
                                    ...
                    break;
            }
    
            return true;
        }
    
        return false;
    }
    
    public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();
    
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        notifyEnterOrExitForAutoFillIfNeeded(true);
        return result;
    }
    

    事件分发源码就分析到这了,如果有不对的地方欢迎指出和交流。

    参考:

    图解 Android 事件分发机制: https://www.jianshu.com/p/e99b5e8bd67b

    书籍:《Android开发艺术探索》

    相关文章

      网友评论

          本文标题:2019-12-15

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