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事件分發(二)》