Android事件分發(fā)與滑動沖突

Android事件分發(fā)機制

一愁拭、概述

1. 事件

事件通常指觸摸或點擊事件,用戶觸摸屏幕時產(chǎn)生 Touch 事件匣摘。Touch 事件的相關(guān)細節(jié)封裝于 MotionEvent 對象中店诗。

事件類型 具體動作
MotionEvent.ACTION_DOWN 按下事件(開始)
MotionEvent.ACTION_UP 抬起事件(結(jié)束)
MotionEvent.ACTION_MOVE 滑動事件
MotionEvent.ACTION_CANCEL 取消事件

2. 分發(fā)流程

事件分發(fā)流程

如上圖所示,onTouch事件產(chǎn)生后音榜,先傳給Activity庞瘸,再傳給View Group,最后傳給View赠叼。

事件分發(fā)流程的目的就是要找到第一個要處理事件的對象擦囊。一旦有一個對象消費了該事件违霞,事件分發(fā)結(jié)束。反之瞬场,如果事件沒有被消費买鸽,則會被廢棄。

3. 重要方法

方法 作用
dispatchTouchEvent(event: MotionEvent?): Boolean 進行事件分發(fā)
onInterceptTouchEvent(event: MotionEvent?): Boolean 進行事件攔截
onTouchEvent(event: MotionEvent?): Boolean 進行事件消耗

三個方法之間的關(guān)系可以使用如下偽代碼表示:

fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    val consume = false
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev)
    } else {
        consume = child.dispatchTouchEvent(ev)
    }
    return consume
}

事件的傳遞規(guī)則:對于ViewGroup贯被,點擊事件傳遞過來后眼五,首先調(diào)用 dispatchTouchEvent 方法。如果其 onInterceptTouchEvent 方法返回true彤灶,表示攔截該事件看幼,隨后它的 onTouch 方法被調(diào)用;如果 onInterceptTouchEvent 方法返回false幌陕,表示不攔截事件诵姜,該事件會繼續(xù)傳遞給子View,接著子View的 dispatchTouchEvent 方法被調(diào)用苞轿。重復該過程直至事件被消耗。

二逗物、Activity的事件分發(fā)

1. Demo演示

(1) 重寫Activity的 dispatchTouchEventonTouchEvent 方法搬卒。

class MainActivity : AppCompatActivity() {
    companion object {
        const val TAG = "Activity"
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        val eventName = EventUtil.getActionName(ev)
        LogUtil.i(TAG, "dispatchTouchEvent $eventName Start", LogUtil.Depth.ACTIVITY)
        val result = super.dispatchTouchEvent(ev)
        LogUtil.i(TAG, "dispatchTouchEvent $eventName End with $result", LogUtil.Depth.ACTIVITY)
        return result
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        val eventName = EventUtil.getActionName(event)
        LogUtil.i(TAG, "onTouchEvent $eventName", LogUtil.Depth.ACTIVITY)
        return super.onTouchEvent(event)
    }
}

(2) 自定義MyLayout (繼承自FrameLayout) 并重寫 dispatchTouchEvent方法

class MyLayout : FrameLayout {
    companion object {
        const val TAG = "MyLayout"
    }

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        val eventName = EventUtil.getActionName(ev)
        val result = false  // false or true
        LogUtil.i(TAG, "dispatchTouchEvent $eventName End with $result", LogUtil.Depth.VIEW_GROUP)
        return result
    }
}

(3) activity_main.xml

<com.example.eventdispatch.ui.MyLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_gravity="center"
    android:background="@color/colorPrimary"
    android:gravity="center">
</com.example.eventdispatch.ui.MyLayout>

當MyLayout dispatchTouchEvent 返回false時翎卓,表示其不對事件進行分發(fā)契邀。ACTION_DOWN事件傳遞到MyLayout時,dispatchTouchEvent 被調(diào)用失暴,返回false坯门,事件返回給Activity,Activity的 onTouchEvent 被調(diào)用逗扒。當ACTION_MOVE或ACTION_UP事件到來時古戴,由于上一個事件由Activity處理,因此該事件不再向下傳遞矩肩,直接交給Activity處理现恼。點擊MyLayout,打印的Log如下:

I/Activity:     dispatchTouchEvent ACTION_DOWN Start
I/  MyLayout:   dispatchTouchEvent ACTION_DOWN End with false
I/Activity:     onTouchEvent ACTION_DOWN
I/Activity:     dispatchTouchEvent ACTION_DOWN End with false
I/Activity:     dispatchTouchEvent ACTION_UP Start
I/Activity:     onTouchEvent ACTION_UP
I/Activity:     dispatchTouchEvent ACTION_UP End with false

當MyLayout dispatchTouchEvent 返回true時黍檩,事件被MyLayout消耗叉袍,Activity的 onTouchEvent 不會被調(diào)用。點擊MyLayout刽酱,打印的Log如下:

I/Activity:     dispatchTouchEvent ACTION_DOWN Start
I/  MyLayout:   dispatchTouchEvent ACTION_DOWN End with true
I/Activity:     dispatchTouchEvent ACTION_DOWN End with true
I/Activity:     dispatchTouchEvent ACTION_UP Start
I/  MyLayout:   dispatchTouchEvent ACTION_UP End with true
I/Activity:     dispatchTouchEvent ACTION_UP End with true

2. 源碼分析

注: 本文所有源碼為API Level 29

點擊事件產(chǎn)生后喳逛,最先傳遞給當前Activity,Activity的 dispatchTouchEvent 方法被調(diào)用棵里。

Activity的 dispatchTouchEvent 方法如下:

/**
 * Acticity.java
 * Line 3989-3997
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        // 空方法
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

可以看到润文,事件首先交給Activity所屬的Window進行分發(fā)姐呐,如果返回true,則事件分發(fā)結(jié)束转唉;如果返回false皮钠,意味著事件沒有被處理,Activity的 onTouchEvent 被調(diào)用赠法。

getWindow 返回Window對象麦轰,Window是一個抽象類,PhoneWindow是其唯一的實現(xiàn)類砖织。因此 getWindow().superDispatchTouchEvent(ev) 就是調(diào)用PhoneWindow的 superDispatchTouchEvent(ev) 方法款侵。

PhoneWindow的 superDispatchTouchEvent 方法如下:

/**
 * PhoneWindow.java
 * Line 1847-1850
 */
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

PhoneWindow將事件傳遞給了DecorView對象mDecor,mDecor是 getWindow().getDecorView() 返回的View侧纯,Activity中通過 setContentView 設置的View是它的一個子View新锈。

DecorView的 superDispatchTouchEvent 方法如下:

/**
 * DecorView.java
 */
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks{
    // ...
    // Line464-466
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
}

DecorView繼承自FramgLayout,F(xiàn)rameLayout又繼承自ViewGroup眶熬,所以 mDecor.superDispatchTouchEvent(event) 其實就是調(diào)用ViewGroup的 dispatchTouchEvent 方法妹笆。至此,事件已經(jīng)分發(fā)給ViewGroup了娜氏。

3. 分發(fā)流程圖

Activity分發(fā)流程

三拳缠、ViewGroup的事件分發(fā)

1. Demo演示

(1) 自定義MyLayout (繼承自FrameLayout) 并重寫 dispatchTouchEvent 方法、onInterceptTouchEvent 方法贸弥、onTouchEvent 方法窟坐。

class MyLayout : FrameLayout {
    companion object {
        const val TAG = "MyLayout"
    }

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        val eventName = EventUtil.getActionName(ev)
        LogUtil.i(TAG, "dispatchTouchEvent $eventName Start", LogUtil.Depth.VIEW_GROUP)
        val result = super.dispatchTouchEvent(ev)
        LogUtil.i(TAG, "dispatchTouchEvent $eventName End with $result", LogUtil.Depth.VIEW_GROUP)
        return result
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        val eventName = EventUtil.getActionName(ev)
        LogUtil.i(TAG, "onInterceptTouchEvent $eventName Start", LogUtil.Depth.VIEW_GROUP)
        val result = false  // false or true
        LogUtil.i(TAG, "onInterceptTouchEvent $eventName End with $result", LogUtil.Depth.VIEW_GROUP)
        return result
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        val eventName = EventUtil.getActionName(event)
        LogUtil.i(TAG, "onTouchEvent $eventName Start", LogUtil.Depth.VIEW_GROUP)
        val result = super.onTouchEvent(event)
        LogUtil.i(TAG, "onTouchEvent $eventName End with $result", LogUtil.Depth.VIEW_GROUP)
        return result
    }
}

(2) activity_main.xml

<com.example.eventdispatch.ui.MyLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_gravity="center"
    android:background="@color/colorPrimary"
    android:gravity="center">

    <Button
        android:id="@+id/my_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Button" />

</com.example.eventdispatch.ui.MyLayout>

(3) MainActivity中,為button添加點擊事件绵疲。

class MainActivity : AppCompatActivity() {
    companion object {
        const val TAG = "Activity"
    }

    private lateinit var button: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        button = findViewById(R.id.my_button)
        button.setOnClickListener {
            LogUtil.i(MyLayout.TAG, "onClick", LogUtil.Depth.VIEW_GROUP)
        }
    }
    
    // ...
}

當MyLayout的 onInterceptTouchEvent 方法返回false時哲鸳,點擊button,打印的Log如下:

I/Activity:     dispatchTouchEvent ACTION_DOWN Start
I/  MyLayout:   dispatchTouchEvent ACTION_DOWN Start
I/  MyLayout:   onInterceptTouchEvent ACTION_DOWN Start
I/  MyLayout:   onInterceptTouchEvent ACTION_DOWN End with false
I/  MyLayout:   dispatchTouchEvent ACTION_DOWN End with true
I/Activity:     dispatchTouchEvent ACTION_DOWN End with true
I/Activity:     dispatchTouchEvent ACTION_UP Start
I/  MyLayout:   dispatchTouchEvent ACTION_UP Start
I/  MyLayout:   onInterceptTouchEvent ACTION_UP Start
I/  MyLayout:   onInterceptTouchEvent ACTION_UP End with false
I/  MyLayout:   dispatchTouchEvent ACTION_UP End with true
I/Activity:     dispatchTouchEvent ACTION_UP End with true
I/  MyLayout:   onClick

可以看出盔憨,此時按鈕的點擊事件觸發(fā)徙菠,但是MyLayout的 onTouchEvent 方法未被調(diào)用。說明MyLayout并沒有攔截事件郁岩,而是將它傳遞給了button懒豹。

當MyLayout的 onInterceptTouchEvent 方法返回true時,點擊button驯用,打印的Log如下:

I/Activity:     dispatchTouchEvent ACTION_DOWN Start
I/  MyLayout:   dispatchTouchEvent ACTION_DOWN Start
I/  MyLayout:   onInterceptTouchEvent ACTION_DOWN Start
I/  MyLayout:   onInterceptTouchEvent ACTION_DOWN End with true
I/  MyLayout:   onTouchEvent ACTION_DOWN Start
I/  MyLayout:   onTouchEvent ACTION_DOWN End with false
I/  MyLayout:   dispatchTouchEvent ACTION_DOWN End with false
I/Activity:     onTouchEvent ACTION_DOWN
I/Activity:     dispatchTouchEvent ACTION_DOWN End with false
I/Activity:     dispatchTouchEvent ACTION_UP Start
I/Activity:     onTouchEvent ACTION_UP
I/Activity:     dispatchTouchEvent ACTION_UP End with false

這種情況下按鈕的點擊事件沒有觸發(fā)脸秽,但是MyLayout的 onTouchEvent 方法被調(diào)用。說明MyLayout攔截了事件蝴乔,沒有將它傳遞給button记餐。

2. 源碼分析

如上所述,Activity在 dispatchTouchEvent 方法內(nèi)將點擊事件傳遞給了ViewGroup的 dispatchTouchEvent 方法薇正。

ViewGroup的 dispacthTouchEvent 方法如下:

/**
 * ViewGroup.java
 * Line 2577-2791
 */
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        // ...
    }
    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

不難看出片酝,ViewGroup的 dispatchTouchEvent 的方法返回handled的值囚衔,默認為false。而改變handled值的部分位于第一個if塊內(nèi)雕沿,dispatchTouchEvent 被調(diào)用時首先進入 onFilterTouchEventForSecurity(ev) 方法练湿。

onFilterTouchEventForSecurity(ev) 方法如下:

/**
 * View.java
 * Line 13474-13482
 */
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
            && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
        return false;
    }
    return true;
}

if語句塊表示如果該視圖不位于頂部,并且有屬性設置不在頂部時不響應事件审轮,則不分發(fā)該事件肥哎。

FILTER_TOUCHES_WHEN_OBSCUREDandroid:filterTouchWhenObscured 屬性相對應,如果為true疾渣,表示有其他視圖在該視圖之上篡诽,該視圖不響應觸摸事件。

MotionEvent.FLAG_WINDOW_IS_OBSCURED 為true表示該窗口被隱藏榴捡。

當沒有設置相關(guān)屬性時杈女,onFilterTouchEventForSecurity(ev) 方法返回true。因此分發(fā)過程都會進入 if (onFilterTouchEventForSecurity(ev)) 語句塊內(nèi)吊圾,其內(nèi)容如下:

if (onFilterTouchEventForSecurity(ev)) {
    // Line 2591-2601
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    if (actionMasked == MotionEvent.ACTION_DOWN) {
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    // ...
}

其中 cancelAndClearTouchTargets 方法和 resetTouchState 方法的作用是在點擊后重置觸摸狀態(tài)达椰。

if (onFilterTouchEventForSecurity(ev)) {
    // Line 2604-2618
    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);
        } else {
            intercepted = false;
        }
    } else {
        intercepted = true;
    }
}

disallowIntercept 代表禁用事件攔截功能,默認為false项乒。進入到 if (!disallowIntercept) 語句塊內(nèi)啰劲,調(diào)用 onInterceptTouchEvent 方法。

onInterceptTouchEvent 方法如下:

/**
 * ViewGroup.java
 * Line 3224-3232
 */
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

在上一個if語句塊內(nèi)intercepted = onInterceptTouchEvent(ev)板丽,如果不攔截呈枉,則 intercepted 為false趁尼,進入 if (!canceled && !intercepted) 語句塊埃碱。

if (onFilterTouchEventForSecurity(ev)) {
    // Line 2634-2736
    if (!canceled && !intercepted) {
        // ...
        if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

            // ...
            final int childrenCount = mChildrenCount;
            if (newTouchTarget == null && childrenCount != 0) {
                // ...
                for (int i = childrenCount - 1; i >= 0; i--) {
                    // 判斷子元素能夠接受點擊事件
                    if (!child.canReceivePointerEvents()
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                        ev.setTargetAccessibilityFocus(false);
                        continue;
                    }

                    newTouchTarget = getTouchTarget(child);
                    if (newTouchTarget != null) {
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                        break;
                    }

                    // 調(diào)用子元素的dispatchTouchEvent方法
                    resetCancelNextUpFlag(child);
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        // ...
                    }
                    // ...
                }
                // ...
            }
        }
    }
    if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
    }
}

在該語句塊內(nèi),可以看到通過for循環(huán)遍歷所有子元素酥泞,判斷每個子元素是否可以接受點擊事件:(1) canReceivePointerEvents 判斷事件的坐標是否落在子元素的區(qū)域內(nèi)砚殿;(2) isTransformedTouchPointInView 判斷子元素是否在播放動畫。判斷結(jié)束后執(zhí)行ViewGroup的 dispatchTransformedTouchEvent 方法芝囤。

如果 intercepted 為true似炎,則ViewGroup攔截事件。此時不會進入第3行的if語句悯姊。又由于沒有對mFirstTouchTarget賦值羡藐,因此進入if (mFirstTouchTarget == null)語句塊,執(zhí)行ViewGroup的 dispatchTransformedTouchEvent 方法悯许。

dispatchTransformedTouchEvent方法如下:

/**
 * ViewGroup.java
 */
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    // Line 3072-3087
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }

    transformedEvent.recycle();
    return handled;    
}

不難發(fā)現(xiàn)仆嗦,當參數(shù)child為null時,對應上述intercepted 為true的情況先壕,此時調(diào)用 super.dispatchTouchEvent(event)瘩扼,即 View.dispatchTouchEvent(event)谆甜,事件由ViewGroup處理;當child不為null時集绰,對應上述intercepted 為false的情況规辱,此時調(diào)用 child.dispatchTouchEvent(event) 方法,事件由ViewGroup分發(fā)至View栽燕。

3. 分發(fā)流程圖

ViewGroup分發(fā)流程

四罕袋、View的事件分發(fā)

1. Demo演示

(1) 自定義MyButton(繼承自AppCompatButton)并重寫 dispatchTouchEvent 方法、onInterceptTouchEvent 方法纫谅。

class MyButton : AppCompatButton {
    companion object {
        const val TAG = "MyButton"
    }

    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        val eventName = EventUtil.getActionName(event)
        LogUtil.i(TAG, "dispatchTouchEvent $eventName Start", LogUtil.Depth.VIEW)
        val result = super.dispatchTouchEvent(event)
        LogUtil.i(TAG, "dispatchTouchEvent $eventName End with $result", LogUtil.Depth.VIEW)
        return result
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        val eventName = EventUtil.getActionName(event)
        LogUtil.i(TAG, "onTouchEvent $eventName", LogUtil.Depth.VIEW)
        return super.onTouchEvent(event)
    }
}

(2) activity_main.xml

<com.example.eventdispatch.ui.MyLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/my_layout"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_gravity="center"
    android:background="@color/colorPrimary"
    android:gravity="center">

    <com.example.eventdispatch.ui.MyButton
        android:id="@+id/my_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Button" />
</com.example.eventdispatch.ui.MyLayout>

(3) 在MainActivity中為myButton添加 OnTouchListenerOnClickListener炫贤。

class MainActivity : AppCompatActivity() {
    companion object {
        const val TAG = "Activity"
    }

    private lateinit var myButton: MyButton

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        myButton = findViewById(R.id.my_button)
        myButton.apply {
            setOnTouchListener { _, ev ->
                val eventName = EventUtil.getActionName(ev)
                LogUtil.i(MyButton.TAG, "onTouch $eventName", LogUtil.Depth.VIEW)
                false
            }
            setOnClickListener {
                LogUtil.i(MyButton.TAG, "onClick", LogUtil.Depth.VIEW)
            }
        }
    }
}

當myButton的 onTouch 返回false時,打印的Log如下:

I/Activity:     dispatchTouchEvent ACTION_DOWN Start
I/  MyLayout:   dispatchTouchEvent ACTION_DOWN Start
I/  MyLayout:   onInterceptTouchEvent ACTION_DOWN Start
I/  MyLayout:   onInterceptTouchEvent ACTION_DOWN End with false
I/    MyButton: dispatchTouchEvent ACTION_DOWN Start
I/    MyButton: onTouch ACTION_DOWN
I/    MyButton: onTouchEvent ACTION_DOWN
I/    MyButton: dispatchTouchEvent ACTION_DOWN End with true
I/  MyLayout:   dispatchTouchEvent ACTION_DOWN End with true
I/Activity:     dispatchTouchEvent ACTION_DOWN End with true
I/Activity:     dispatchTouchEvent ACTION_UP Start
I/  MyLayout:   dispatchTouchEvent ACTION_UP Start
I/  MyLayout:   onInterceptTouchEvent ACTION_UP Start
I/  MyLayout:   onInterceptTouchEvent ACTION_UP End with false
I/    MyButton: dispatchTouchEvent ACTION_UP Start
I/    MyButton: onTouch ACTION_UP
I/    MyButton: onTouchEvent ACTION_UP
I/    MyButton: dispatchTouchEvent ACTION_UP End with true
I/  MyLayout:   dispatchTouchEvent ACTION_UP End with true
I/Activity:     dispatchTouchEvent ACTION_UP End with true
I/    MyButton: onClick

onTouch返回true時付秕,打印的Log如下:

I/Activity:     dispatchTouchEvent ACTION_DOWN Start
I/  MyLayout:   dispatchTouchEvent ACTION_DOWN Start
I/  MyLayout:   onInterceptTouchEvent ACTION_DOWN Start
I/  MyLayout:   onInterceptTouchEvent ACTION_DOWN End with false
I/    MyButton: dispatchTouchEvent ACTION_DOWN Start
I/    MyButton: onTouch ACTION_DOWN
I/    MyButton: dispatchTouchEvent ACTION_DOWN End with true
I/  MyLayout:   dispatchTouchEvent ACTION_DOWN End with true
I/Activity:     dispatchTouchEvent ACTION_DOWN End with true
I/Activity:     dispatchTouchEvent ACTION_UP Start
I/  MyLayout:   dispatchTouchEvent ACTION_UP Start
I/  MyLayout:   onInterceptTouchEvent ACTION_UP Start
I/  MyLayout:   onInterceptTouchEvent ACTION_UP End with false
I/    MyButton: dispatchTouchEvent ACTION_UP Start
I/    MyButton: onTouch ACTION_UP
I/    MyButton: dispatchTouchEvent ACTION_UP End with true
I/  MyLayout:   dispatchTouchEvent ACTION_UP End with true
I/Activity:     dispatchTouchEvent ACTION_UP End with true

對比發(fā)現(xiàn)兰珍,當View的 onTouch 返回false時,onTouchEventonClick 都被調(diào)用询吴,返回true時掠河,二者都不會被調(diào)用。據(jù)此分析:onClick 方法在 onTouchEvent 方法中被調(diào)用猛计。

2. 源碼分析

如上所述唠摹,當ViewGroup的child(即子View)不為null時,子View的 dispatchTouchEvent 方法被調(diào)用奉瘤。

View的 dispatchTouchEvent 方法如下:

/**
 * View.java
 * Line 13395-13449
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    // ...
    boolean result = false;
    // ...
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        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;
}

li.mOnTouchListener 表示View的OnTouchListener勾拉,如果通過 setOnTouchListener 方法為View設置監(jiān)聽事件,則 li.mOnTouchListener 不為空盗温。(mViewFlags & ENABLED_MASK) == ENABLED 代表View enable藕赞。

當設置 onTouch監(jiān)聽事件并返回false時,14行的if語句判斷條件為false卖局,進入 if (!result && onTouchEvent(event)) 內(nèi)斧蜕,View的 onTouchEvent 方法被調(diào)用;如果 onTouch 返回true砚偶,進入第14行的if語句塊批销,result被置為true,因此20行的 onTouchEvent 方法不會被調(diào)用染坯。

onTouchEvent 方法如下:

/**
 * View.java
 * Line 14754-14962
 */
public boolean onTouchEvent(MotionEvent event) {
    // ...
    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) {
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }

                    if (prepressed) {
                        setPressed(true, x, y);
                    }

                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        removeLongPressCallback();
                        
                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClickInternal();
                            }
                        }
                    }
                    // ...
                }
                // ...
                break;
                // ...
        }

        return true;
    }

    return false;
}

如果View的 clickablelongClickable 有一個為true均芽,將會進入switch語句,并且在 action為MotionEvent.ACTION_UP 時单鹿,執(zhí)行36行的 performClickInternal 方法掀宋,該方法內(nèi)部又調(diào)用了 performClick 方法。

performClick 方法如下:

/**
 * View.java
 * Line 7131-7151
 */
public boolean performClick() {
    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;
}

如果View設置了 OnClickListener,則會執(zhí)行12行布朦,調(diào)用 onClick 方法囤萤。這印證了上面Demo演示中的分析:onClick 方法在 onTouchEvent 方法中被調(diào)用。因此是趴,當View設置了OnTouchListenerOnClickListener涛舍,事件分發(fā)的優(yōu)先級為 OnTouchListener.onTouch > onTouchEvent > OnClickListener.onClick

3. 分發(fā)流程圖

View事件分發(fā)

五唆途、滑動沖突

1. 常見場景

常見的產(chǎn)生滑動沖突的兩種場景如下:

滑動沖突

(1) 內(nèi)外滑動方向不一致

主要產(chǎn)生于ViewPager與Fragment組合富雅,F(xiàn)ragment內(nèi)又使用RecyclerView的場景。ViewPager內(nèi)部已經(jīng)處理了沖突肛搬,使用時無需處理没佑。而如果使用自定義可水平滑動的ViewGroup,則必須手動處理沖突温赔。

解決這種沖突蛤奢,一般根據(jù)滑動過程中兩點之間的水平和垂直距離差來判斷由誰攔截事件。

(2) 內(nèi)外滑動方向一致

主要產(chǎn)生于ScrollView嵌套的場景或ScrollView內(nèi)嵌RecyclerView的場景陶贼。例如兩個ScrollView嵌套時啤贩,只有外層可以滑動。

2. 解決方式

(1) 外部攔截法:事件先經(jīng)過父容器(ViewGroup)處理拜秧,如果父容器需要該事件則攔截痹屹。這種方式符合事件分發(fā)機制,可以通過重寫 onInterceptTouchEvent 方法進行處理枉氮。偽代碼如下:

override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
    var intercepted = false
    val x = event.x.toInt()
    val y = event.y.toInt()
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            intercepted = false
        }
        MotionEvent.ACTION_MOVE -> {
            intercepted = if (滿足父容器的攔截要求) {
                true
            } else {
                false
            }
        }
        MotionEvent.ACTION_UP -> {
            intercepted = false
        }
    }
    mLastXIntercept = x
    mLastYIntercept = y
    return intercepted
}

滑動沖突的處理邏輯主要表現(xiàn)為對ACTION_MOVE事件的處理志衍,如果滿足父容器的攔截條件則攔截該事件。而對于ACTION_DOWN事件聊替,必須返回false楼肪,不對其進行攔截。否則后續(xù)事件全部被父容器攔截佃牛,無法傳遞給子元素淹辞。ACTION_UP事件沒有太大意義医舆,也需返回false俘侠。

(2) 內(nèi)部攔截法:父容器不攔截任何事件,所有事件都傳遞給子元素(View)蔬将,如果需要該事件則直接消耗爷速,否則交給父容器處理。這種方式不符合事件分發(fā)機制霞怀,需要重寫 dispatchTouchEvent 方法并調(diào)用父容器的 requestDisallowInterceptTouchEvent 方法惫东,決定是否需要父容器對事件進行攔截。偽代碼如下:

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    val x = event.x.toInt()
    val y = event.y.toInt()
    when (event.action) {
        MotionEvent.ACTION_DOWN -> {
            parent.requestDisallowInterceptTouchEvent(true)
        }
        MotionEvent.ACTION_MOVE -> {
            val deltaX = x - mLastX
            val deltaY = y - mLastY
            if (父容器需要此類點擊事件) {
                parent.requestDisallowInterceptTouchEvent(false)
            }
        }
        MotionEvent.ACTION_UP -> {}
    }
    mLastX = x
    mLastY = y
    return super.dispatchTouchEvent(event)
}

父容器需要重寫 onInterceptTouchEvent 方法:

override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
    var intercepted = false
    val action = event.action
    if (action == MotionEvent.ACTION_DOWN) {
        super.onInterceptTouchEvent(ev)
        return false
    }
    return true
}

3. Demo演示

滑動方向一致

自定義MyScrollView繼承自ScrollView,嵌套使用時廉沮,將會產(chǎn)生只有外層ScrollView可以滑動的情況颓遏,產(chǎn)生了滑動沖突。此時MyScrollView既是父容器也是子元素滞时。

(1) 外部攔截法

將MyScrollView當作父容器叁幢,重寫 onInterceptTouchEvent 方法,返回false即可坪稽。

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    return false
}

(2) 內(nèi)部攔截法

將MyScrollView當作子元素曼玩,重寫 dispatchTouchEvent 方法。

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    when(ev?.action) {
        MotionEvent.ACTION_DOWN -> {
            parent.requestDisallowInterceptTouchEvent(true)
        }
    }
    return super.dispatchTouchEvent(ev)
}

父容器(同樣是MyScrollView)重寫 onInterceptTouchEvent 方法窒百。

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    if (ev?.action == MotionEvent.ACTION_DOWN) {
        super.onInterceptTouchEvent(ev)
        return false
    }
    return true
}

注:可以直接使用NestedScrollView代替ScrollView黍判,該組件支持嵌套使用,無需手動解決滑動沖突篙梢。

滑動方向不一致

自定義ConflictViewPager繼承自ViewPager顷帖,重寫 onInterceptTouchEvent 方法返回false。ConflictViewPager中的每個fragment中放有一個RecyclerView渤滞,此時RecyclerView可以正常上下滑動窟她;而如果左右滑動,ConflictViewPager中的fragment并不會進行切換蔼水,產(chǎn)生滑動沖突震糖。

(1) 外部攔截法

重寫 onInterceptTouchEvent 方法如下:

class OuterViewPager : ViewPager {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    private var mLastXIntercept = 0
    private var mLastYIntercept = 0

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        var intercepted = false
        val x = ev.x.toInt()
        val y = ev.y.toInt()
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                intercepted = false
                super.onInterceptTouchEvent(ev)
            }
            MotionEvent.ACTION_MOVE -> {
                val deltaX = x - mLastXIntercept
                val deltaY = y - mLastYIntercept
                intercepted = abs(deltaX) > abs(deltaY)
            }
            MotionEvent.ACTION_UP -> {
                intercepted = false
            }
        }
        mLastXIntercept =  x
        mLastYIntercept =  y
        return intercepted
    }
}

解決沖突的主要邏輯在 MotionEvent.ACTION_MOVE 中:如果水平距離大于豎直距離,表示產(chǎn)生了水平滑動趴腋,OuterViewPager攔截事件吊说;如果產(chǎn)生豎直滑動,OuterViewPager不攔截事件优炬,事件會傳遞給RecyclerView颁井。

(2) 內(nèi)部攔截法

自定義MyRecyclerView繼承自RecyclerView,重寫 dispatchTouchEvent 方法:

class MyRecyclerView: RecyclerView {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    private var mLastX = 0
    private var mLastY = 0

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        val x = ev.x.toInt()
        val y = ev.y.toInt()
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                parent.requestDisallowInterceptTouchEvent(true)
            }
            MotionEvent.ACTION_MOVE -> {
                val deltaX = x - mLastX
                val deltaY = y - mLastY
                if (abs(deltaX) > abs(deltaY)) {
                    parent.requestDisallowInterceptTouchEvent(false)
                }
            }
            MotionEvent.ACTION_UP -> {}
            else -> {}
        }
        mLastX = x
        mLastY = y
        return super.dispatchTouchEvent(ev)
    }
}

自定義InnerViewPager繼承自ViewPager蠢护,重寫 onInterceptTouchEvent 方法:

override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    if (ev.action == MotionEvent.ACTION_DOWN) {
        super.onInterceptTouchEvent(ev)
        return false
    }
    return true
}

解決沖突的主要邏輯同樣在 MotionEvent.ACTION_MOVE 中:如果產(chǎn)生水平滑動雅宾,InnerViewPager攔截事件;如果產(chǎn)生豎直滑動葵硕,MyRecyclerView攔截事件眉抬。

Demo鏈接

點擊查看

參考文章

Android事件分發(fā)機制詳解
Android事件分發(fā)機制完全解析,帶你從源碼的角度徹底理解(上)
Android事件分發(fā)機制完全解析懈凹,帶你從源碼的角度徹底理解(下)
Understanding Android touch flow control
Android開發(fā)藝術(shù)探索蜀变,任玉剛,電子工業(yè)出版社介评,2015.9

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末库北,一起剝皮案震驚了整個濱河市爬舰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌寒瓦,老刑警劉巖情屹,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異杂腰,居然都是意外死亡屁商,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門颈墅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蜡镶,“玉大人,你說我怎么就攤上這事恤筛」倩梗” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵毒坛,是天一觀的道長望伦。 經(jīng)常有香客問我,道長煎殷,這世上最難降的妖魔是什么屯伞? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮豪直,結(jié)果婚禮上劣摇,老公的妹妹穿的比我還像新娘。我一直安慰自己弓乙,他們只是感情好末融,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著暇韧,像睡著了一般勾习。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上懈玻,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天巧婶,我揣著相機與錄音,去河邊找鬼涂乌。 笑死艺栈,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的骂倘。 我是一名探鬼主播眼滤,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼巴席,長吁一口氣:“原來是場噩夢啊……” “哼历涝!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤荧库,失蹤者是張志新(化名)和其女友劉穎堰塌,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體分衫,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡场刑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蚪战。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片牵现。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖邀桑,靈堂內(nèi)的尸體忽然破棺而出瞎疼,到底是詐尸還是另有隱情,我是刑警寧澤壁畸,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布贼急,位于F島的核電站,受9級特大地震影響捏萍,放射性物質(zhì)發(fā)生泄漏太抓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一令杈、第九天 我趴在偏房一處隱蔽的房頂上張望走敌。 院中可真熱鬧,春花似錦逗噩、人聲如沸悔常。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽机打。三九已至,卻和暖如春片迅,著一層夾襖步出監(jiān)牢的瞬間残邀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工柑蛇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留芥挣,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓耻台,卻偏偏與公主長得像空免,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子盆耽,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355