Android View事件分发简述

一、前言

最近重新复习了一遍Android的事件分发机制,直接写成文章的形式记录下来,有助于加深记忆。本文源码基于android 9.0。

二、View、ViewGroup

1、View、ViewGroup

事件的分发离不开View和ViewGroup。在网上看到了一张图,很直观的把二者的关系表现出来了,这里不过多解释了。
在这里插入图片描述

2、MotionEvent

在Android中,每一次触摸屏幕都会产生对应的触摸事件,该事件被封装成一个MotionEvent对象。主要关注下面4个事件。

事件 描述
ACTION_DOWN 手指初次触摸屏幕时触发
ACTION_MOVE 手指在屏幕上滑动时触发,随着滑动会不断触发该事件
ACTION_UP 手指离开屏幕时触发
ACTION_CANCLE 事件被上层(父控件)拦截

将一次滑动应用中列表的操作(从按下屏幕,开始滑动,到最后抬起手指)通过事件来体现,可以描述为以下方式:
ACTION_DOWN(按下屏幕)
ACTION_MOVE(滑动列表)
ACTION_MOVE(滑动列表)
ACTION_MOVE(滑动列表)

ACTION_MOVE(滑动列表)
ACTION_UP(抬手)

三、应用

Android的事件分发机制主要关注的就是这3种触摸事件:OnTouch()、onTouchEvent()、OnClick(),也是平时使用频率最高的。以下就通过源码加demo的形式学习一下Android的事件分发机制。
首先修改activity_main.xml。得到一个既有View又有ViewGroup的界面,之间同时还互相包含着嵌套关系。

<?xml version="1.0" encoding="utf-8"?>
<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"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <com.sk.eventdemo.FirstLayout
        android:id="@+id/first_lt"
        android:layout_width="280dp"
        android:layout_height="280dp"
        android:background="#00a8f3"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第一个layout"/>

        <com.sk.eventdemo.SecondLayout
            android:id="@+id/second_lt"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:layout_marginTop="10dp"
            android:background="#f29a2e"
            android:orientation="vertical"
        android:clickable="true">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="第二个layout"/>

            <com.sk.eventdemo.ThirdView
                android:id="@+id/third_view"
                android:layout_width="120dp"
                android:layout_height="120dp"
                android:layout_marginTop="10dp"
                android:background="#f373a6"
                android:clickable="true">
            </com.sk.eventdemo.ThirdView>

        </com.sk.eventdemo.SecondLayout>

    </com.sk.eventdemo.FirstLayout>

</LinearLayout>

大概长这个样子,蓝色和橙色部分为两个LinearLayout,LinearLayout继承自ViewGroup,粉色部分是一个View。
在这里插入图片描述
代码部分先将准备工作做好,修改MainActivity的onCreate()。为上面的3个视图添加touch和click事件。touch事件中暂时先全部返回false。为了下面观察方便,该demo中所有的log,tag都设为"Demo"。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    first_lt.setOnTouchListener { view, motionEvent ->
        false
    }

    second_lt.setOnTouchListener { view, motionEvent ->
        false
    }

    second_lt.setOnTouchListener { view, motionEvent ->
        false
    }

    first_lt.setOnClickListener{ view ->
        Log.d("Demo", "first_lt: OnClick");
    }

    second_lt.setOnClickListener{ view ->
        Log.d("Demo", "second_lt: OnClick");
    }

    third_view.setOnClickListener{ view ->
        Log.d("Demo", "third_view: OnClick");
    }
    
	override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("Demo", "MainActivity: dispatchTouchEvent");
        return super.dispatchTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        Log.d("Demo", "MainActivity: onTouchEvent");
        return super.onTouchEvent(event)
    }
}

新建FirstLayout.kt继承自LinearLayout。

class FirstLayout : LinearLayout {

    constructor(context: Context): super(context) {}

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {}

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("Demo", "FirstLayout: dispatchTouchEvent")
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("Demo", "FirstLayout: onInterceptTouchEvent")
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> Log.d("Demo", "FirstLayout: DOWN事件")

            MotionEvent.ACTION_MOVE -> Log.d("Demo", "FirstLayout: MOVE事件")

            MotionEvent.ACTION_UP -> Log.d("Demo", "FirstLayout: UP事件")

            else -> Log.d("Demo", "FirstLayout")
        }
        Log.d("Demo", "FirstLayout: onTouchEvent: " + super.onTouchEvent(event))
        return super.onTouchEvent(event)
    }
}

新建SecondLayout继承自LinearLayout。

class SecondLayout : LinearLayout {

    constructor(context: Context): super(context) {}

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {}

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("Demo", "SecondLayout: dispatchTouchEvent")
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("Demo", "SecondLayout: onInterceptTouchEvent")
        return super.onInterceptTouchEvent(ev)
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> Log.d("Demo", "SecondLayout: DOWN事件")

            MotionEvent.ACTION_MOVE -> Log.d("Demo", "SecondLayout: MOVE事件")

            MotionEvent.ACTION_UP -> Log.d("Demo", "SecondLayout: UP事件")

            else -> Log.d("Demo", "SecondLayout")
        }
        Log.d("Demo", "SecondLayout: onTouchEvent: " + super.onTouchEvent(event))

        return super.onTouchEvent(event)
    }
}

新建ThirdView继承自View

class ThirdView : View {

    constructor(context: Context): super(context) {}

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {}

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        Log.d("Demo", "ThirdView: dispatchTouchEvent")
        return super.dispatchTouchEvent(ev)
    }
    
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        when (event?.action) {
            MotionEvent.ACTION_DOWN -> Log.d("Demo", "ThirdView: DOWN事件")

            MotionEvent.ACTION_MOVE -> Log.d("Demo", "ThirdView: MOVE事件")

            MotionEvent.ACTION_UP -> Log.d("Demo", "ThirdView: UP事件")

            else -> Log.d("Demo", "ThirdView")
        }
        Log.d("Demo", "ThirdView: onTouchEvent: " + super.onTouchEvent(event))
        return super.onTouchEvent(event)
    }
}

这3个类全部重写dispatchTouchEvent()和onTouchEvent(),其中两个ViewGroup还需要重写onInterceptTouchEvent()。因为View并没有onInterceptTouchEvent()这个方法。onInterceptTouchEvent()是用来拦截事件的,决定该视图是拦截事件自己消费还是不拦截然后将事件向下传递给子控件,但View没有子控件,所以拦不拦截都是你,也就没有onInterceptTouchEvent()这个方法了。
将上面的代码在机器上跑一遍,然后手指按下粉色的区域内,移动再抬起(手指移动到粉色区域外,后面会解释)。这时你会发现log是长这个样子的。

MainActivity: dispatchTouchEvent
08-28 23:24:58.751 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:24:58.751 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:24:58.751 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:24:58.751 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: onInterceptTouchEvent
08-28 23:24:58.751 25494-25494/com.sk.eventdemo D/Demo: ThirdView: dispatchTouchEvent
08-28 23:24:58.752 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent DOWN
08-28 23:24:58.752 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent: 
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: onInterceptTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: ThirdView: dispatchTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent MOVE
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent: 
...
08-28 23:24:59.056 25494-25494/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:24:59.056 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:24:59.056 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:24:59.056 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:24:59.056 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: onInterceptTouchEvent
08-28 23:24:59.056 25494-25494/com.sk.eventdemo D/Demo: ThirdView: dispatchTouchEvent
08-28 23:24:59.056 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent MOVE
08-28 23:24:59.056 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent: 
08-28 23:24:59.057 25494-25494/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:24:59.057 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:24:59.057 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:24:59.057 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:24:59.057 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: onInterceptTouchEvent
08-28 23:24:59.057 25494-25494/com.sk.eventdemo D/Demo: ThirdView: dispatchTouchEvent
08-28 23:24:59.057 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent UP
08-28 23:24:59.057 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent: 

接下来结合代码和这段log来看一下源码。

四、源码分析

通过log,发现会先走Activity的dispatchTouchEvent,那就跟着进到Activity的dispatchTouchEvent()方法。
Activity.java

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

在第5行,判断getWindow().superDispatchTouchEvent(ev),发现getWindow()返回的mWindow类型为Window。

private Window mWindow;
...
public Window getWindow() {
	return mWindow;
}

而Window只有一个实现类就是PhoneWindow,所以第5行的superDispatchTouchEvent(ev)其实是在PhoneWindow类中实现的。我们进到PhoneWindow这个类中(双击shift搜索PhoneWindow即可),找到superDispatchTouchEvent()方法。

@Override
private DecorView mDecor;
...
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

发现这个mDecor是一个DecorView,也就是我们的跟布局。继续往里跟。

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

返回时调用了super.dispatchTouchEvent(event),而DecorView继承自FrameLayout,FrameLayout又继承自ViewGroup,跟进代码发现确实是调用了ViewGroup的dispatchTouchEvent()方法。
进到ViewGroup的dispatchTouchEvent()方法中。发现代码很长很长,只关注主要的(能看懂的)部分。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
		...
        // 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;
        }
		...
		// Update list of touch targets for pointer down, if needed.
        if (!canceled && !intercepted) {

            // If the event is targeting accessibility focus we give it to the
            // view that has accessibility focus and if it does not handle it
            // we clear the flag and dispatch the event to all children as usual.
            // We are looking up the accessibility focused host to avoid keeping
            // state since these events are very rare.
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);

                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--) {
                        ...
                        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;
                }
            }
        }

        // 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);
        } 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;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // Update list of touch targets for pointer up or cancel, if needed.
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

第11行的mFirstTouchTarget代表着子控件是否处理了上一个事件,如果子控件的onTouchEvent()返回了false,代码会直接走到22行,将intercepted赋值为true进行拦截,根本不去14行判断是否需要拦截。通俗点讲,父控件有权决定是否将事件下发到子控件,有一次父控件大发慈悲,将事件下发到了子控件,可是子控件不想处理(onTouchEvent返回了false),父控件一看,既然你不想要,好吧,以后谁说都没用了(父控件的onInterceptTouchEvent返回true,但是第14行代码根本不走),事件不会再给到子控件了。到这里我们知道了,事件在分发的时候一般情况下是会判断onInterceptTouchEvent来决定是否要传给子控件的(稍后看怎么判断的)。现在的代码中我们没有在onInterceptTouchEvent中返回false进行事件拦截。
回到dispatchTouchEvent()方法继续往下看。将第26、36和48行一起看,判断如果没有取消并且没有拦截事件,并且这是一个DOWN事件,并且存在子控件,就会走到第57行的for中,接着在第59行调用了一个dispatchTransformedTouchEvent()方法。

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();
    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;
    }

    ...
}

直接跳到第10行,如果有子视图,则调用子视图child的dispatchTouchEvent。这里的child可能为ViewGroup或者View,如果child依然是一个ViewGroup的话,会重复这个过程,直到最后的child是一个View,接着会调用View的dispatchTouchEvent方法。

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    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;
}

发现如果监听了OnTouch并且onTouch()方法返回了true,就不会执行第15行的onTouchEvent()方法了,说明onTouch()是先于onTouchEvent()执行的,并且可以决定后续是否需要执行onTouchEvent()方法。从前面的log中也可以证实,一开始在修改MainActivity时,将控件的OnTouch()方法都返回了false,从而代码才可以执行到onTouchEvent()方法。注意这里执行的onTouchEvent()方法是我们自己重写的那个方法。在返回时,默认会执行super.onTouchEvent(event),也就是View类中的onTouchEvent()方法。
进到View中的onTouchEvent()中。

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 (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                ...
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    ...
                    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();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }

                    ...
                }
                mIgnoreNextUpEvent = false;
                break;
                
			...
		}
		
		return true;
    }

    return false;
}

这里主要关注ACTION_UP这个事件。第7行,判断了该控件是否设置了clickable和longclickable,也就是单击和长按。可以通过isClickable()或者在xml对应的控件中添加android:clickable="true"来进行设置。如果设置了单击或者长按,发现最终会执行到第33行的performClickInternal()方法。

private boolean performClickInternal() {
    // Must notify autofill manager before performing the click actions to avoid scenarios where
    // the app has a click listener that changes the state of views the autofill service might
    // be interested on.
    notifyAutofillManagerOnClick();

    return performClick();
}

继续进到performClick()方法中。发现在第8行,如果监听了OnClick的话,会执行控件的onClick()方法。也就是说onClick()方法其实是在onTouchEvent()的ACTION_UP事件中执行的。

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

至此,一次最常见的事件传递就完成了,从最顶层的控件一层一层的分发,是否拦截,向下传递,直到最下层处理。
在回顾以下一开始的代码和log。每一层的onInterceptTouchEvent()方法都返回的false(super.onInterceptTouchEvent(ev)),所以事件会一直传递到最后一个View(ThirdView)中。

08-28 23:24:58.750 25494-25494/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:24:58.751 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:24:58.751 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:24:58.751 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:24:58.751 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: onInterceptTouchEvent
08-28 23:24:58.751 25494-25494/com.sk.eventdemo D/Demo: ThirdView: dispatchTouchEvent

又因为在setOnTouchListener()中返回了false,使得代码可以执行到ThirdView的onTouchEvent()。

08-28 23:24:58.752 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent DOWN
08-28 23:24:58.752 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent: 

又因为ThirdView的onTouchEvent()默认返回了true,从而使上层控件得以继续将后续的事件传来。

08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: onInterceptTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: ThirdView: dispatchTouchEvent
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent MOVE
08-28 23:24:58.844 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent: 
...

还记得我们当时的操作,“手指按下粉色的区域内,移动再抬起(手指移动到粉色区域外,后面会解释)”。现在保证手指一直在粉色区域内,按下,抬起。会发现log最后一行变了,打出了一个OnClick的log。这也正好验证了前面分析的源码,在onTouchEvent()中,如果设置了click的监听,是会走到onClick()方法的。

08-28 23:34:28.988 25494-25494/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:34:28.988 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:34:28.988 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:34:28.988 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:34:28.988 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: onInterceptTouchEvent
08-28 23:34:28.988 25494-25494/com.sk.eventdemo D/Demo: ThirdView: dispatchTouchEvent
08-28 23:34:28.988 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent DOWN
08-28 23:34:28.988 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent: 
08-28 23:34:29.123 25494-25494/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:34:29.123 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:34:29.123 25494-25494/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:34:29.123 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:34:29.123 25494-25494/com.sk.eventdemo D/Demo: SecondLayout: onInterceptTouchEvent
08-28 23:34:29.123 25494-25494/com.sk.eventdemo D/Demo: ThirdView: dispatchTouchEvent
08-28 23:34:29.123 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent UP
08-28 23:34:29.123 25494-25494/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent: 
08-28 23:34:29.123 25494-25494/com.sk.eventdemo D/Demo: third_view: OnClick

继续修改代码,在SecondLayout的onInterceptTouchEvent()中返回true,然后运行代码。

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    Log.d("Demo", "SecondLayout: onInterceptTouchEvent")
//        return super.onInterceptTouchEvent(ev)
    return true
}

观察log,发现ThirdView的log都没有了。因为我们在SecondLayout中拦截了事件,所以SecondLayout再往下的控件就没有办法收到事件了。

08-28 23:40:18.607 31284-31284/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:40:18.607 31284-31284/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:40:18.607 31284-31284/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:40:18.607 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:40:18.607 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: onInterceptTouchEvent
08-28 23:40:18.608 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent DOWN
08-28 23:40:18.608 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent: 
08-28 23:40:18.710 31284-31284/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:40:18.710 31284-31284/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:40:18.710 31284-31284/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:40:18.710 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:40:18.711 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent MOVE
08-28 23:40:18.711 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent: 
...
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent MOVE
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent: 
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent UP
08-28 23:40:19.004 31284-31284/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent: 

继续修改,先将SecondLayout中的onInterceptTouchEvent()恢复原样。

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    Log.d("Demo", "SecondLayout: onInterceptTouchEvent")
    return super.onInterceptTouchEvent(ev)
//        return true
}

将ThirdView的onTouchEvent()方法返回false,运行代码。

override fun onTouchEvent(event: MotionEvent?): Boolean {
    when (event?.action) {
        MotionEvent.ACTION_DOWN -> Log.d("Demo", "ThirdView: onTouchEvent DOWN")

        MotionEvent.ACTION_MOVE -> Log.d("Demo", "ThirdView: onTouchEvent MOVE")

        MotionEvent.ACTION_UP -> Log.d("Demo", "ThirdView: onTouchEvent UP")

        else -> Log.d("Demo", "ThirdView")
    }
    Log.d("Demo", "ThirdView: onTouchEvent: ")
    return false
}

观察log发现,只有第一次的ACTION_DOWN事件传递到了ThirdView,而且以后的事件都和ThirdView没有关系了。正如前面分析,当ThirdView的onTouchEvent()方法返回false时,上层控件认为你什么都不想要了,以后的事件传递到SecondLayout时,无论是否拦截都不往下传了。

08-28 23:50:20.456 2276-2276/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:50:20.457 2276-2276/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:50:20.457 2276-2276/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:50:20.457 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:50:20.458 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: onInterceptTouchEvent
08-28 23:50:20.458 2276-2276/com.sk.eventdemo D/Demo: ThirdView: dispatchTouchEvent
08-28 23:50:20.458 2276-2276/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent DOWN
08-28 23:50:20.458 2276-2276/com.sk.eventdemo D/Demo: ThirdView: onTouchEvent: 
08-28 23:50:20.458 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent DOWN
08-28 23:50:20.458 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent: 
08-28 23:50:20.519 2276-2276/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:50:20.519 2276-2276/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:50:20.519 2276-2276/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:50:20.519 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:50:20.520 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent MOVE
08-28 23:50:20.520 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent: 
...
08-28 23:50:20.772 2276-2276/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:50:20.772 2276-2276/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:50:20.772 2276-2276/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:50:20.772 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:50:20.772 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent MOVE
08-28 23:50:20.772 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent: 
08-28 23:50:20.780 2276-2276/com.sk.eventdemo D/Demo: MainActivity: dispatchTouchEvent
08-28 23:50:20.780 2276-2276/com.sk.eventdemo D/Demo: FirstLayout: dispatchTouchEvent
08-28 23:50:20.780 2276-2276/com.sk.eventdemo D/Demo: FirstLayout: onInterceptTouchEvent
08-28 23:50:20.780 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: dispatchTouchEvent
08-28 23:50:20.780 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent UP
08-28 23:50:20.780 2276-2276/com.sk.eventdemo D/Demo: SecondLayout: onTouchEvent: 

这段log还有个地方需要注意,为什么第一次的ACTION_DOWN事件会传递到SecondLayout中,但是不会传递到FirstLayout和MainActivity中。因为当一个控件的onTouchEvent返回false时,说明这个控件不想处理这次事件,这个事件会还给上一层,并且告诉上一层,以后不用再给我了。如果上一层的onTouchEvent返回的也是false,那就继续往上传。因为SecondLayout的onTouchEvent()方法默认返回了true,就是说,既然你不想要,那我收下吧,我也不再麻烦上面人了。所以FirstLayout和MainActivity的onTouchEvent就没有机会处理ACTION_DOWN事件了。

如果本文有什么对源码理解有误的地方,欢迎大家指出。

发布了10 篇原创文章 · 获赞 19 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/songkai0825/article/details/88726859
今日推荐