1. 程式人生 > >事件分發機制 時間攔截 滑動衝突 MD

事件分發機制 時間攔截 滑動衝突 MD

目錄

Markdown版本筆記 我的GitHub首頁 我的部落格 我的微信 我的郵箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 [email protected]

事件分發機制 時間攔截 滑動衝突 MD
***
目錄
===

事件分發機制分析案例

預設行為

觸控事件由Action_Down==0、Aciton_UP

==1、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

我們將子LinearLayoutdispatchTouchEvent 方法返回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

我們將孫子TextViewdispatchTouchEvent 方法返回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

我們將子LinearLayoutonInterceptTouchEvent 方法返回true,當觸控子LinearLayout孫子TextView時:

dispatchTouchEvent--root--按下
onInterceptTouchEvent--root--按下
dispatchTouchEvent--ll_child--按下
onInterceptTouchEvent--ll_child--按下

onTouch--ll_child--按下
onTouchEvent--ll_child--按下
onTouch--root--按下
onTouchEvent--root--按下

結論

  • 如果一個 ViewGrouponInterceptTouchEvent方法返回 true,則代表此 ViewGroup 會攔截後續所有事件,因此,其子View後續將收不到任何Touch事件
  • 注意,ViewGroup攔截事件後會將當前事件傳遞給自己的 onTouchonTouchEvent 方法,而不會終止事件的分發
  • 因此,我們可以認為:如果一個 ViewGrouponInterceptTouchEvent方法返回 true,則可以等價的認為這個ViewGroup隱藏了其所有子View(或者認為其沒有任何子View),而其他過程和正常的事件分發過程完全一致。
  • 鑑於此,我們經常是在onInterceptTouchEvent方法中攔截子View獲取事件(目的往往是為了讓自己能夠處理後續事件)。

onInterceptTouchEventdispatchTouchEvent 方法的注意區別:

  • 返回 true 後,onInterceptTouchEvent 則會攔截後續所有事件的分發,而 dispatchTouchEvent 只是終止當前這一具體事件的分發。
  • onInterceptTouchEvent 會繼續正常分發這一具體的事件,而 dispatchTouchEvent會直接結束分發這一具體的事件。

onTouchEvent 返回 true

試驗 4

我們將子LinearLayoutonTouchEvent方法返回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時,雖然子LinearLayoutonTouchEvent方法返回了true,但是孫子TextViewdispatchTouchEvent方法仍會被呼叫,所以,View的onTouchEvent方法返回true並不會影響DOWN事件的正常下發。
  • 【上傳過程】:首先DOWN事件會從最底層的孫子TextViewonTouchEvent方法上傳給子LinearLayoutonTouchEvent方法,然後DOWN事件的上傳過程就結束了,而不會繼續上傳給父View

結論:

  • onTouchEvent方法的返回值並不會影響DOWN事件的正常下發過程。
  • 如果一個View的onTouchEvent方法返回true,那麼DOWN事件的上傳過程將會在這裡停止,所以此View所有父ViewonTouch方法和onTouchEvent方法都將不會被呼叫。
  • 換句話說,如果一個View的onTouchEvent方法返回true,那麼DOWN事件將會在傳遞到View的onTouchEvent方法後被完全消耗掉。

對於後續的 MOVE、UP 事件

  • 所觸控的那個View所有父ViewdispatchTouchEvent方法和onInterceptTouchEvent方法【都會】被呼叫。
  • 所觸控的那個View的所有子ViewdispatchTouchEvent方法和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

我們將孫子TextViewonTouchEvent方法返回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:所謂不可點選就是clickablelongClickable同時為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