事件分發機制 時間攔截 滑動衝突 MD
目錄
Markdown版本筆記 | 我的GitHub首頁 | 我的部落格 | 我的微信 | 我的郵箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | [email protected] |
事件分發機制 時間攔截 滑動衝突 MD
***
目錄
===
事件分發機制分析案例
預設行為
觸控事件由Action_Down
==0、Aciton_UP
Action_Move
==2組成,其中一次完整的觸控事件中,Down只有一個、Up有一個或0個、Move有若干個(包括0個),一旦UP發生,就表明此次觸控事件已經結束了。
試驗 0
1、當觸控根部的LinearLayout
時:
dispatchTouchEvent--root--按下
onInterceptTouchEvent--root--按下
onTouch--root--按下
onTouchEvent--root--按下
2、當觸控子TextView
時:
dispatchTouchEvent--root--按下 onInterceptTouchEvent--root--按下 dispatchTouchEvent--TextView--tv--按下 onTouch--tv--按下 onTouchEvent--tv--按下 onTouch--root--按下 onTouchEvent--root--按下
3、當觸控子LinearLayout
:
dispatchTouchEvent--root--按下
onInterceptTouchEvent--root--按下
dispatchTouchEvent--ll_child--按下
onInterceptTouchEvent--ll_child--按下
onTouch--ll_child--按下
onTouchEvent--ll_child--按下
onTouch--root--按下
onTouchEvent--root--按下
4、當觸控孫子TextView
時:
dispatchTouchEvent--root--按下
onInterceptTouchEvent--root--按下
dispatchTouchEvent--ll_child--按下
onInterceptTouchEvent--ll_child--按下
dispatchTouchEvent--TextView--tv_child--按下
onTouch--tv_child--按下
onTouchEvent--tv_child--按下
onTouch--ll_child--按下
onTouchEvent--ll_child--按下
onTouch--root--按下
onTouchEvent--root--按下
5、當觸控一個具有點選事件的普通的子TextView
時:
dispatchTouchEvent--root--按下
onInterceptTouchEvent--root--按下
dispatchTouchEvent--TextView--null--按下
onTouchEvent--null--按下
dispatchTouchEvent--root--移動
onInterceptTouchEvent--root--移動
dispatchTouchEvent--TextView--null--移動
onTouchEvent--null--移動
//...後續所有Move事件都會收到
dispatchTouchEvent--root--鬆開
onInterceptTouchEvent--root--鬆開
dispatchTouchEvent--TextView--null--鬆開
onTouchEvent--null--鬆開
結論
在不進行如何幹涉
的情況下:
即要滿足以下所有條件:
- 觸控點區域沒有一個View的
dispatchTouchEvent
方法的返回值為true - 觸控點區域沒有一個View的
onTouchEvent
方法和onTouch
方法的返回值為true - 觸控點區域沒有一個View有設定
點選事件、長點選事件
等會消耗觸控事件的方法
兩個基本規律:
- View的
onInterceptTouchEvent
方法是在dispatchTouchEvent
方法中被呼叫的(如果dispatchTouchEvent
方法返回true,則不會呼叫onInterceptTouchEvent
方法),所以可以把dispatchTouchEvent
方法和onInterceptTouchEvent
方法看做一對方法。 - View的
onTouchEvent
方法是在監聽器回撥方法onTouch
之後被呼叫的,所以也可以把onTouch
方法和onTouchEvent
方法看做一對方法。
這麼一劃分的話,事件的分發過程就可以簡化為對兩對方法進行分析了(事實上,這種劃分是合理的)。
對於 Down 事件
- Down事件首先會從root逐級【下發】到最底層的View,下發的事件是通過
dispatchTouchEvent
方法傳遞的(當然,因為dispatchTouchEvent
方法沒有返回true,所以還會將Down事件傳給自己的onInterceptTouchEvent
方法,下同)。 - 當Down事件從root逐級下發到最底層的那個View後,最底層的那個View的
dispatchTouchEvent
會將Down事件傳給自己的onTouch
方法(當然因為onTouch
沒有返回true,所以還會將Down事件傳給自己的onTouchEvent
方法,下同)。 - 由此便開始了Down事件的逐級【上傳】過程,即Down事件將從最底層的那個View開始,逐級【上傳】到root的
onTouch
(和onTouchEvent
方法)。
Down事件的傳遞過程:
dTE父(包含-->oIT) --> dTE子 --> ... --> dTE底 -->onTouch底(包含-->onTE)
--> ... --> onTouch子 --> onTouch父。
對於後續的 Move、UP 事件
- 在分發完 Down 事件後( Down 事件一定會分發),其餘的 Move、UP 事件將不再分發。
- 也即任何 View 都不會收到後續的 Move、UP 事件,因為沒有任何 View 需要處理本次觸控事件。
注意,上述案例中的第五種情況不滿足上述條件,因為那個TextView具有點選事件,所以會導致能收到後續的觸控事件
dispatchTouchEvent 返回 true
試驗 1
我們將子LinearLayout
的 dispatchTouchEvent
方法返回true
,當觸控子LinearLayout
、孫子TextView
時:
dispatchTouchEvent--root--按下
onInterceptTouchEvent--root--按下
dispatchTouchEvent--ll_child--按下
dispatchTouchEvent--root--移動
onInterceptTouchEvent--root--移動
dispatchTouchEvent--ll_child--移動
//...後續所有Move事件都會收到
dispatchTouchEvent--root--鬆開
onInterceptTouchEvent--root--鬆開
dispatchTouchEvent--ll_child--鬆開
試驗 2
我們將孫子TextView
的 dispatchTouchEvent
方法返回true
,當觸控孫子TextView
時:
dispatchTouchEvent--root--按下
onInterceptTouchEvent--root--按下
dispatchTouchEvent--ll_child--按下
onInterceptTouchEvent--ll_child--按下
dispatchTouchEvent--TextView--tv_child--按下
dispatchTouchEvent--root--移動
onInterceptTouchEvent--root--移動
dispatchTouchEvent--ll_child--移動
onInterceptTouchEvent--ll_child--移動
dispatchTouchEvent--TextView--tv_child--移動
//...後續所有Move事件都會收到
dispatchTouchEvent--root--鬆開
onInterceptTouchEvent--root--鬆開
dispatchTouchEvent--ll_child--鬆開
onInterceptTouchEvent--ll_child--鬆開
dispatchTouchEvent--TextView--tv_child--鬆開
結論
- 如果某個View的
dispatchTouchEvent
方法返回值為true(不管是普通View還是ViewGroup),則表明 這個具體的事件 的分發過程到此結束了。 - 所以此View的所有
子View
都不會獲取到這一個具體的事件。 - 但是此View的所有
父View
可以獲取到此View能獲取到的所有事件,因為此View獲取的任何事件都是由根View逐級下發過來的。 - 注意,分發結束只是針對當前這個具體的MotionEvent事件而言的,而不是針對整個事件鏈而言的,所以到後續所有事件仍會正常進行分發。
- 所以,即使你在
dispatchTouchEvent
中沒做任何判斷就直接返回了true,後續所有的事件仍會正常進行分發。 - 鑑於
dispatchTouchEvent
方法只是終止了某一具體事件的分發,而不是終止本次完整事件的分發,所以如果想在某種情況下終止接收後續的事件,用這個方法是不適合的。
onInterceptTouchEvent 返回 true
PS:只有 ViewGroup 才有 onInterceptTouchEvent 方法
試驗 3
我們將子LinearLayout
的 onInterceptTouchEvent
方法返回true
,當觸控子LinearLayout
、孫子TextView
時:
dispatchTouchEvent--root--按下
onInterceptTouchEvent--root--按下
dispatchTouchEvent--ll_child--按下
onInterceptTouchEvent--ll_child--按下
onTouch--ll_child--按下
onTouchEvent--ll_child--按下
onTouch--root--按下
onTouchEvent--root--按下
結論
- 如果一個
ViewGroup
的onInterceptTouchEvent
方法返回 true,則代表此 ViewGroup 會攔截後續所有事件,因此,其子View
後續將收不到任何Touch事件
。 - 注意,
ViewGroup
攔截事件後會將當前事件傳遞給自己的onTouch
和onTouchEvent
方法,而不會終止事件的分發。 - 因此,我們可以認為:如果一個
ViewGroup
的onInterceptTouchEvent
方法返回 true,則可以等價的認為這個ViewGroup
隱藏了其所有子View
(或者認為其沒有任何子View),而其他過程和正常的事件分發過程完全一致。 - 鑑於此,我們經常是在
onInterceptTouchEvent
方法中攔截子View
獲取事件(目的往往是為了讓自己能夠處理後續事件)。
onInterceptTouchEvent
和 dispatchTouchEvent
方法的注意區別:
- 返回 true 後,
onInterceptTouchEvent
則會攔截後續所有事件的分發,而dispatchTouchEvent
只是終止當前這一具體事件的分發。 onInterceptTouchEvent
會繼續正常分發這一具體的事件,而dispatchTouchEvent
會直接結束分發這一具體的事件。
onTouchEvent 返回 true
試驗 4
我們將子LinearLayout
的onTouchEvent
方法返回true
當觸控子LinearLayout
時:
dispatchTouchEvent--root--按下
onInterceptTouchEvent--root--按下
dispatchTouchEvent--ll_child--按下
onInterceptTouchEvent--ll_child--按下
onTouch--ll_child--按下
onTouchEvent--ll_child--按下
dispatchTouchEvent--root--移動
onInterceptTouchEvent--root--移動
dispatchTouchEvent--ll_child--移動
onTouch--ll_child--移動
onTouchEvent--ll_child--移動
//...後續所有Move事件都會收到
dispatchTouchEvent--root--鬆開
onInterceptTouchEvent--root--鬆開
dispatchTouchEvent--ll_child--鬆開
onTouch--ll_child--鬆開
onTouchEvent--ll_child--鬆開
當觸控孫子TextView
時:
dispatchTouchEvent--root--按下
onInterceptTouchEvent--root--按下
dispatchTouchEvent--ll_child--按下
onInterceptTouchEvent--ll_child--按下
dispatchTouchEvent--TextView--tv_child--按下
onTouch--tv_child--按下
onTouchEvent--tv_child--按下
onTouch--ll_child--按下
onTouchEvent--ll_child--按下
dispatchTouchEvent--root--移動
onInterceptTouchEvent--root--移動
dispatchTouchEvent--ll_child--移動
onTouch--ll_child--移動
onTouchEvent--ll_child--移動
//...後續所有Move事件都會收到
dispatchTouchEvent--root--鬆開
onInterceptTouchEvent--root--鬆開
dispatchTouchEvent--ll_child--鬆開
onTouch--ll_child--鬆開
onTouchEvent--ll_child--鬆開
結論
首先可以發現,當觸控子LinearLayout
和孫子TextView
時,兩者的過程基本都是一致的,所以下面的關鍵是分析對比著兩者的區別。
對於DOWN事件
- 【下發過程】:當觸控
孫子TextView
時,雖然子LinearLayout
的onTouchEvent
方法返回了true,但是孫子TextView
的dispatchTouchEvent
方法仍會被呼叫,所以,View的onTouchEvent
方法返回true並不會影響DOWN
事件的正常下發。 - 【上傳過程】:首先
DOWN
事件會從最底層的孫子TextView
的onTouchEvent
方法上傳給子LinearLayout
的onTouchEvent
方法,然後DOWN
事件的上傳過程就結束了,而不會繼續上傳給父View
。
結論:
onTouchEvent
方法的返回值並不會影響DOWN
事件的正常下發
過程。- 如果一個View的
onTouchEvent
方法返回true,那麼DOWN
事件的上傳
過程將會在這裡停止,所以此View所有父View
的onTouch
方法和onTouchEvent
方法都將不會被呼叫。 - 換句話說,如果一個View的
onTouchEvent
方法返回true,那麼DOWN
事件將會在傳遞到View的onTouchEvent
方法後被完全消耗掉。
對於後續的 MOVE、UP 事件
- 所觸控的那個View
及
所有父View
的dispatchTouchEvent
方法和onInterceptTouchEvent
方法【都會】被呼叫。 - 所觸控的那個View的所有
子View
的dispatchTouchEvent
方法和onInterceptTouchEvent
方法【都不會】被呼叫。 - 僅所觸控的那個View的
onTouch
方法和onTouchEvent
方法會被呼叫,換句話說就是,僅onTouchEvent
返回true的那個View的onTouchEvent
方法能夠處理MOVE、UP
事件。 - 所以,
onTouchEvent
方法最核心的作用是用來告訴View樹:後續的MOVE、UP
事件到底應該從根View【傳遞】到哪個View去處理(PS:"傳遞"這個詞用的非常好)。
完整的流程為
- 一旦某個View的【onTouchEvent】返回true,當【DOWN】事件按照【正常的分發流程】逐級【下傳】到【最底層的View】的【dispatchTouchEvent】後,【最底層的View】將通過其【onTouchEvent】逐級【上傳】,直到上傳到【此View】的【onTouchEvent】方法之後,DOWN事件將會被消耗掉;
- 然而,此後的【MOVE、UP】事件在逐級【下傳】到【此View】後將直接【結束】分發(而不會下傳到最底層的View),並且在呼叫【此View】的onTouchEvent方法後被完全【消耗】掉。
試驗 5
我們將孫子TextView
的onTouchEvent
方法返回true
當觸控孫子TextView
時:
dispatchTouchEvent--root--按下
onInterceptTouchEvent--root--按下
dispatchTouchEvent--ll_child--按下
onInterceptTouchEvent--ll_child--按下
dispatchTouchEvent--TextView--tv_child--按下
onTouch--tv_child--按下
onTouchEvent--tv_child--按下
dispatchTouchEvent--root--移動
onInterceptTouchEvent--root--移動
dispatchTouchEvent--ll_child--移動
onInterceptTouchEvent--ll_child--移動
dispatchTouchEvent--TextView--tv_child--移動
onTouch--tv_child--移動
onTouchEvent--tv_child--移動
dispatchTouchEvent--root--鬆開
onInterceptTouchEvent--root--鬆開
dispatchTouchEvent--ll_child--鬆開
onInterceptTouchEvent--ll_child--鬆開
dispatchTouchEvent--TextView--tv_child--鬆開
onTouch--tv_child--鬆開
onTouchEvent--tv_child--鬆開
可以發現和上面的情況完全一致,這裡之所以分開,是因為上面的篇幅太長了,放在一起的話會讓人比較難把握住重點資訊。
測試程式碼
Activity
public class TouchEventActivity extends Activity implements OnTouchListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout);
findViewById(R.id.root).setTag("root");
findViewById(R.id.ll_child).setTag("ll_child");
findViewById(R.id.tv).setTag("tv");
findViewById(R.id.tv_child).setTag("tv_child");
findViewById(R.id.root).setOnTouchListener(this);
findViewById(R.id.ll_child).setOnTouchListener(this);
findViewById(R.id.tv).setOnTouchListener(this);
findViewById(R.id.tv_child).setOnTouchListener(this);
findViewById(R.id.tv_alert).setOnClickListener(v -> {
Utils.TYPE = (Utils.TYPE + 1) % 6;
Toast.makeText(this, "值為 " + Utils.TYPE, Toast.LENGTH_SHORT).show();
});
}
@Override
protected void onResume() {
super.onResume();
Toast.makeText(this, "值為 " + Utils.TYPE, Toast.LENGTH_SHORT).show();
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("【bqt】", "onTouch--" + v.getTag() + "--" + Utils.getActionName(event));
return false;
}
}
自定義 LinearLayout
public class MyLinearLayout extends LinearLayout {
public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("【bqt】", "dispatchTouchEvent--" + getTag() + "--" + Utils.getActionName(event));
return (getId() == R.id.ll_child && Utils.TYPE == 1) || super.dispatchTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
Log.i("【bqt】", "onInterceptTouchEvent--" + getTag() + "--" + Utils.getActionName(event));
return (getId() == R.id.ll_child && Utils.TYPE == 3) || super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("【bqt】", "onTouchEvent--" + getTag() + "--" + Utils.getActionName(event));
return (getId() == R.id.ll_child && Utils.TYPE == 4) || super.onTouchEvent(event);
}
}
自定義 TextView
public class MyTextView extends AppCompatTextView {
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("【bqt】", "dispatchTouchEvent--TextView--" + getTag() + "--" + Utils.getActionName(event));
return (getId() == R.id.tv_child && Utils.TYPE == 2) || super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("【bqt】", "onTouchEvent--" + getTag() + "--" + Utils.getActionName(event));
return (getId() == R.id.tv_child && Utils.TYPE == 5) || super.onTouchEvent(event);
}
}
工具類
public class Utils {
public static int TYPE = 0;
public static String getActionName(MotionEvent event) {
switch (event.getAction()) {
case ACTION_DOWN:
return "按下";
case ACTION_UP:
return "鬆開";
case ACTION_MOVE:
return "移動";
default:
return "未知";
}
}
}
一些面試題
View的onTouchEvent方法,OnClickListerner的OnClick方法,OnTouchListener的onTouch方法,這三者的優先順序如何?
onTouch > onTouchEvent > OnClick
如果某個view(不包括VG)處理事件的時候沒有消耗down事件,會有什麼結果?
假如一個view,在down事件來的時候他的onTouchEvent
返回false, 那麼這個down事件所屬的事件序列
,就是他後續的move和up
都不會給他處理了。
注意:之所以不包括VG絕不是因為對於
後續的move和up
,此view的父View的onTouchEvent會回撥,而只是因為任何view所接收到的事件都是通過VG傳遞過來的。
一旦有事件傳遞給view(不包括VG),view的onTouchEvent一定會被呼叫嗎?
一定會(一定要三思,否則多半會回答"不一定會")。
因為view本身沒有onInterceptTouchEvent
方法,所以只要事件來到view這裡就一定會走onTouchEvent
方法,並且預設都是返回true的,除非這個view是不可點選的。
PS:所謂不可點選就是
clickable
和longClickable
同時為fale。
Button的clickable就是true,但是textview是false。
enable是否影響view的onTouchEvent返回值?
不影響
只要clickable和longClickable
有一個為真,那麼onTouchEvent就返回true
requestDisallowInterceptTouchEvent 的作用?
可以在子元素中干擾父元素的事件分發
但是down事件干擾不了。
onTouchEvent和GestureDetector在什麼時候用哪個比較好?
只有滑動需求的時候就用onTouchEvent
如果有雙擊、拋擲等行為的時候就用GestureDetector
滑動衝突問題如何解決?
要解決滑動衝突,其實主要的就是一個核心思想:你到底想讓哪個 view 來響應你的滑動?
比如,從上到下滑,是哪個view來處理這個事件,從左到右呢?
業務需求想明白以後,解決的方法就是2個:外部攔截(父親攔截)和內部攔截
,基本上所有的滑動衝突都是這2種的變種,而且核心程式碼思想都一樣。
- 外部攔截法:思路就是
重寫父容器的onInterceptTouchEvent方法
,子元素一般不需要做額外的處理。我們通常是對父View的onInterceptTouchEvent
接收到的Move事件做一個過濾
,當Move事件滿足適當條件時(如持續若干時間或移動若干距離)會攔截掉,並返回子View一個Action_Cancel
事件。這種方式比較簡單且很容易讓人理解。 - 內部攔截法:思路就是父容器不管,在子View中呼叫
getParent().requestDisallowInterceptTouchEvent(true)
,作用是告訴父view,這個觸控事件由我來處理,不要阻礙我。
2017-10-14