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ā)流程
如上圖所示,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的 dispatchTouchEvent
和 onTouchEvent
方法搬卒。
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ā)流程圖
三拳缠、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_OBSCURED
與 android: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ā)流程圖
四罕袋、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添加 OnTouchListener
和 OnClickListener
炫贤。
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時,onTouchEvent
和 onClick
都被調(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的 clickable
或 longClickable
有一個為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設置了OnTouchListener
和 OnClickListener
涛舍,事件分發(fā)的優(yōu)先級為 OnTouchListener.onTouch
> onTouchEvent
> OnClickListener.onClick
。
3. 分發(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