1. 程式人生 > 實用技巧 >Android事件分發(一)

Android事件分發(一)

基於Android9.0,瞭解Android事件分發

還是那句話:點成線,線成面,切勿貪心,否則一臉懵逼

先記住這個事件分發的順序:

Activity->ViewGroup->View

以及三個重要的方法:

方法名 作用是什麼? 什麼時候呼叫?
dispatchTouchEvent() 傳遞(分發)事件 當前View能夠獲取點選事件時
onTouchEvent() 處理點選事件 在dispatchTouchEvent()內部呼叫
onInterceptTouchEvent() 判斷是否攔截事件
$\color{red}{注意:只存在於ViewGroup中}$
在ViewGroup的dispatchTouchEvent()中呼叫

腦海裡大概有了這個順序和概念,我們就從原始碼開始吧。

當觸發點選事件時,最先響應的是

Activity的dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {//按下事件
        onUserInteraction();//這是一個空方法。activity無論分發按鍵事件、觸控事件或者軌跡球事件都會呼叫Activity#onUserInteraction()。 
    }
    if (getWindow().superDispatchTouchEvent(ev)) {//獲取當前Window,Window是一個abstract類,這裡它的實現是PhoneWindow
         //Window的superDispatchTouchEvent方法,也是一個abstract方法,所以要去看PhoneWindow的superDispatchTouchEvent
        return true;
    }
    return onTouchEvent(ev);//沒有任何View 接收/處理事件,呼叫自身的onTouchEvent
}

PhoneWindow的superDispatchTouchEvent

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);//mDecor為DecorView例項,DecorView為PhoneWindow的頂層Window
}

DecorView繼承於FrameLayout,而FrameLayout繼承於ViewGroup,所以,DecorView是ViewGroup的子類。

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

這裡super.dispatchTouchEvent(event);其實就是ViewGroup的dispatchTouchEvent。這裡先不作深入分析,假設它返回false,先把第一條路走通,然後再來分析ViewGroup的dispatchTouchEvent。

最後呼叫onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

上面我們知道mWindow就是PhoneWindow,但是shouldCloseOnTouch不是abstract方法,它的實現在Window裡

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;//點選Window外面
    if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {//peekDecorView為DecorView
        return true;
    }
    return false;
}

private boolean isOutOfBounds(Context context, MotionEvent event) {
        final int x = (int) event.getX();
        final int y = (int) event.getY();
        final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop();//16畫素
        final View decorView = getDecorView();
        return (x < -slop) || (y < -slop)
                || (x > (decorView.getWidth()+slop))
                || (y > (decorView.getHeight()+slop));
    }

最後返回false,就代表沒有消費事件。

ViewGroup的dispatchTouchEvent

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
     ...
        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
             //disallowIntercept:是否禁用事件攔截的功能。預設為false,不禁用
            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;
        }
        ...
             if (!canceled && !intercepted) {//沒有取消也沒有攔截
                  ...
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        ...
                        if (!child.canReceivePointerEvents()//是否響應
                                || !isTransformedTouchPointInView(x, y, child, null)) {//是否在View範圍內
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        newTouchTarget = getTouchTarget(child);
                        ...
                         //注意dispatchTransformedTouchEvent方法,分發事件給子View的關鍵
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            ...
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                             //在addTouchTarget裡面,mFirstTouchTarget被賦值
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
。。。
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
...
            }
        }

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
             //注意dispatchTransformedTouchEvent方法,分發事件給子View的關鍵
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            ...
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    //注意dispatchTransformedTouchEvent方法,分發事件給子View的關鍵
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    ...
                }
               ...
            }
        }
		...
    }
...
    return handled;
}

先來看看onInterceptTouchEvent方法,最後再看dispatchTransformedTouchEvent這個方法

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;//預設返回false
}

既然ViewGroup預設不攔截,那就來看看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) {//子View為空,呼叫自己的dispatchTouchEvent
            handled = super.dispatchTouchEvent(event);
        } else {//子View不為空,呼叫子View的dispatchTouchEvent
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
...
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                ...
                handled = child.dispatchTouchEvent(event);
...
            }
            return handled;
        }
        ...
    } else {
        ...
    }

    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
         ...
        handled = child.dispatchTouchEvent(transformedEvent);
    }
...
    return handled;
}

其實邏輯不復雜,核心思想就是child==null,呼叫自身父類的dispatchTouchEvent,如果不為空,就呼叫子View的dispatchTouchEvent,接下來,我們就來看看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)) {//這裡OnTouchListener,就是平常我們設定的setOnTouchListener,onTouch自然就是重寫的onTouch
            result = true;
        }

        if (!result && onTouchEvent(event)) {//根據需求,可以重寫onTouchEvent。onTouchEvent預設返回true
            result = true;
        }
    }...

    return result;
}

到這裡,事件分發基本就結束了,是不是腦子有點糊?胖子第一次也是的,不過,加上自己寫個demo,會清楚很多。下面我們來敲敲demo,加深這條線的印象。


首頁建立MainActivity、CustomRelativeLayout、CustomView,然後分別重新他們的dispatchTouchEvent、onInterceptTouchEvent(CustomRelativeLayout獨有)、onTouchEvent,加上日誌,最後看看xml和CustomView的監聽

<com.example.test.CustomRelativeLayout
    android:id="@+id/relativeLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.test.CustomView
        android:id="@+id/textTv"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_centerInParent="true"
        android:background="@color/colorAccent" />

第一種情況:事件不消費

activityMainBinding.textTv.setOnTouchListener((v,event)->{
    Log.e(TAG,"textTv --->> setOnTouchListener");
    return false;//注意這裡,預設是返回false。表示不消費
});
//        activityMainBinding.textTv.setOnTouchListener(new View.OnTouchListener() {
//            @Override
//            public boolean onTouch(View v, MotionEvent event) {
//                return false;
//            }
//        });

點選CustomView看看日誌

E/MainActivity: 呼叫dispatchTouchEvent第0次 //點選,從上往下傳遞
E/MainActivity: dispatchTouchEvent   //先呼叫Activity的事件分發
E/CustomRelativeLayout: dispatchTouchEvent //再呼叫ViewGroup的事件分發
E/CustomRelativeLayout: onInterceptTouchEvent//判斷ViewGroup是否攔截事件
E/CustomView: dispatchTouchEvent//View的dispatchTouchEvent
E/MainActivity: textTv --->> setOnTouchListener//View的setOnTouchListener,onTouch返回false
E/CustomView: onTouchEvent//從下往上返回
E/CustomView: onTouchEvent ACTION_DOWN
E/CustomRelativeLayout: onTouchEvent
E/CustomRelativeLayout: onTouchEvent ACTION_DOWN
E/MainActivity: 呼叫onTouchEvent第0次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_DOWN//最後到Activity的onTouchEvent,結束
E/MainActivity: 呼叫dispatchTouchEvent第1次//這裡是抬起
E/MainActivity: dispatchTouchEvent
E/MainActivity: 呼叫onTouchEvent第1次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_UP

第二種情況:View消費

activityMainBinding.textTv.setOnTouchListener((v,event)->{
    Log.e(TAG,"textTv --->> setOnTouchListener");
    return true;
});
E/MainActivity:呼叫dispatchTouchEvent第0次//點選,從上往下傳遞
E/MainActivity:dispatchTouchEvent
E/CustomRelativeLayout:dispatchTouchEvent
E/CustomRelativeLayout:onInterceptTouchEvent
E/CustomView:dispatchTouchEvent
E/MainActivity:textTv--->>setOnTouchListener//這裡返回true,就不往上返回了
E/MainActivity:呼叫dispatchTouchEvent第1次//這裡是抬起
E/MainActivity:dispatchTouchEvent
E/CustomRelativeLayout:dispatchTouchEvent
E/CustomRelativeLayout:onInterceptTouchEvent
E/CustomView:dispatchTouchEvent
E/MainActivity:textTv--->>setOnTouchListener

第三種情況:ViewGroup攔截

CustomRelativeLayout
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    Log.e(TAG,"onInterceptTouchEvent");
    return true;//注意這裡,返回true,攔截事件
}
E/MainActivity: 呼叫dispatchTouchEvent第0次
E/MainActivity: dispatchTouchEvent
E/CustomRelativeLayout: dispatchTouchEvent
E/CustomRelativeLayout: onInterceptTouchEvent//這裡返回true,攔截事件
E/CustomRelativeLayout: onTouchEvent//直接呼叫自己的onTouchEvent
E/CustomRelativeLayout: onTouchEvent ACTION_DOWN
E/MainActivity: 呼叫onTouchEvent第0次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_DOWN
E/MainActivity: 呼叫dispatchTouchEvent第1次
E/MainActivity: dispatchTouchEvent
E/MainActivity: 呼叫onTouchEvent第1次
E/MainActivity: onTouchEvent
E/MainActivity: onTouchEvent ACTION_UP

第四種情況:ViewGroup消費

activityMainBinding.relativeLayout.setOnTouchListener((v,event)->{
    Log.e(TAG,"relativeLayout --->> setOnTouchListener");
    return true;
});

與情況二類似,onTouch返回true,就不再往上傳遞了。


關於總結:還是不借鑑各路大神的blog總結了。胖子覺得這樣會造成部分朋友只看總結,不看內容,最後變成知其然不知其所以然(胖子就吃過很多這樣的虧),還是交給朋友們自行總結吧。

胖子總結

  • 先清楚傳遞Activity->ViewGroup->View和返回View->ViewGroup->Activity順序
  • 再搞清楚dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()的基本作用
  • 開啟原始碼,一步一步跟下去,自己用工具畫一畫它們之間的關係,之後腦袋裡就會有大致概念了
  • 溫馨提示:點成線,線成面,切勿貪心,否則一臉懵逼
  • 胖子有什麼理解錯誤的,歡迎大家指出來,一起討論、學習、進步
  • 期待胖子的第三篇《Android事件分發(二)》

參考文獻

圖解 Android 事件分發機制

Android事件分發機制詳解:史上最全面、最易懂