Android事件傳遞、多點觸控及滑動衝突的處理

基本概念

  1. 全部Touch事件都會被封裝MotionEvent, 包括Touch的類型、位置(相對屏幕的絕對位置,相對View的相對位置)、時間、歷史記錄以及第幾個手指(多點觸控)等;
  2. 事件有多種類型,經常使用的事件類型有:ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_CANCEL 等;
  3. 對事件的處理包括三類: 事件傳遞,dispatchTouchEvent(); 攔截,onInterceptTouchEvent(); 消費,onTouchEvent()、OnTouchListener;

傳遞過程

網上有不少資料對事件的分發過程作了詳盡的代碼追蹤,好比 www.jianshu.com/p/38015afcd…bash

有興趣的同窗能夠參考並去詳細走一下,這裏我作一個文字性描述:ide

傳遞細節描述

  1. 事件從 Activity.dispatchTouchEvent() 開始傳遞, 依次經過getWindow().superDispatchTouchEvent(event)、mDecor.superDispatchTouchEvent(event) 傳遞,即從Activity-> PhoneWindow ->DecorView, DecorView 是整個 ViewTree 的頂層 ViewGroup ;
  2. 在整個 ViewGroup 中,事件從頂層開始,依次往子View傳遞;
  3. 父 ViewGroup 能夠經過 onInterceptTouchEvent() 對事件作攔截,阻止其往下傳遞;
  4. 若是未被攔截,則子 View 能夠經過 onTouchEvent() 消費(處理)事件;
  5. 若是事件從上往下傳遞過程當中一直沒有被攔截,且最底層子 View 沒有消費事件,事件會反向往上傳遞,這時父 ViewGroup 能夠在 onTouchEvent() 中消費該事件,若是仍是沒有被消費的話,最後會到 Activity 的 onTouchEvent() 函數;
  6. 底層View是具備事件的優先消費權的;
  7. 若是View 沒有對 ACTION_DOWN 進行消費,這次點擊的後續事件不會傳遞過來;
  8. 若是 View 消費了 ACTION_DOWN ,這次點擊的後續事件會直接給這個 View,這裏的後續事件指的是 ACTION_MOVE 和 ACTION_UP 事件;此時,其父 ViewGroup 的 onIntercept 函數仍會被調用,仍能進行攔截,但它本身的 onIntercept 不會被調用了;
  9. 子 View 能夠在 onTouchEvent 中調用 getParent().requestDisallowInterceptTouchEvent(true),這樣父 ViewGroup 的 onIntercept 在後續的事件中就不會被調用了;
  10. 若是第一個事件即 ACTION_DOWN 就被父 ViewGroup 攔截了,子 View 將不會獲取到消費事件的機會;
  11. OnTouchListener 優先於 onTouchEvent() 對事件進行消費;
  12. 消費指的是相應的函數返回 true ;
  13. ViewGroup 纔有 onIntercept 方法,View 是沒有的,即View不能夠攔截事件;
  14. 全部的事件處理過程都是以 ACTION_DOWN 開始,ACTION_UP 或者 ACTION_CANCEL 結束,ACTION_UP 是事件正常處理邏輯的結束標誌,ACTION_CANCEL 是由父 ViewGroup 主動發出,當父 ViewGroup 攔截了除 ACTION_DOWN 以外的事件,會給正在消費 ACTION_DOWN 並等待後續事件的子 View 發送一個 ACTION_CANCEL 事件,通知子 View 結束本身的事件等待;

TouchTarget

關於第七、8兩點,ViewGroup是如何在 dispatchTouchEvent 過程當中快速命中並分發到對應子 View 的呢?這裏是經過 TouchTarget 這個結構來實現的。函數

private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        
        // 用於控制同步的鎖
        private static final Object sRecycleLock = new Object[0];
        
        // 注意這是static類型的,內部可複用實例鏈表表頭
        private static TouchTarget sRecycleBin;
        
        // 內部可複用的實例鏈表的長度
        private static int sRecycledCount;

        public static final int ALL_POINTER_IDS = -1; // all ones

        // 當前被觸摸的 View
        public View child;
        
        // 對目標捕獲的全部指針的指針id的組合位掩碼
        public int pointerIdBits;

        // 鏈表中指向的下一個目標
        public TouchTarget next;

        private TouchTarget() {
        }

        ...
}
複製代碼

在ViewGroup中維護了一個變量:mFirstTouchTarget,這是在 ViewGroup 中維護的鏈表, 用於記錄當前響應事件序列的子 View (一個事件序列對應一個響應它的子View),mFirstTouchTarget 指向鏈表首部。ui

先看一下 mFirstTouchTarget 的賦值:this

// 這是發生在ViewGroup中的dispatchTouchEvent方法中
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    ...
    mLastTouchDownX = ev.getX();
    mLastTouchDownY = ev.getY();
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
}

// 當響應事件的目標child View添加到鏈表中,同時讓 mFirstTouchTarget 指向鏈表的表頭
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
複製代碼

再看 mFirstTouchTarget 在 dispatchTouchEvent 方法中的使用:spa

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            一、若是事件是 ACTION_DOWN 事件,重置 touchTargets 狀態,在 cancelAndClearTouchTargets 方法中會發出 ACTION_CANCEL 事件
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            二、對於一個事件序列,當其中某一個事件成功攔截時,那麼對於剩下的一系列事件也會被攔截,而且不會再次執行onInterceptTouchEvent方法。若是 ACTION_DOWN 事件被攔截了,即當前ViewGroup的 onInterceptTouchEvent(ev) return true;此時 mFirstTouchTarget 必然爲null,後續的事件都會當前 ViewGroup 攔截再也不傳遞
            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); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }

            三、若是事件既沒有cancel,也沒有被 intercept,遍歷子View進行事件分發
            if (!canceled && !intercepted) {
                ...
            }

            四、事件分發過程當中,若是dispatchTouchEvent返回了false,或者說當前的ViewGroup沒有子元素的話,會走到這個邏輯。mFirstTouchTarget == null說明子View並無消費事件,因此沒有對mFirstTouchTarget進行賦值。這裏child == null,代碼會進一步執行super.dispatchTouchEvent(event),即 View 中的 dispatchTouchEvent 方法
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {

            五、mFirstTouchTarget != null, 說明事件被子View消費,此時會依次將事件分發到 mFirstTouchTarget 保存的鏈表 View中
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    ...
                    target = next;
                }
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }
複製代碼

這個地方重點關注一下一、二、三、四、5幾個註釋點。如今咱們回到7 8兩點。代理

若是View 沒有對 ACTION_DOWN 進行消費,這次點擊的後續事件不會傳遞過來。這個很顯然,若是沒有對 ACTION_DOWN 進行消費,就不會被保存到 TouchTarget 鏈表中,後續事件的分發是直接往這個鏈表中進行分發的。指針

若是 View 消費了 ACTION_DOWN ,這次點擊的後續事件會直接給這個 View,這裏的後續事件指的是 ACTION_MOVE 和 ACTION_UP 事件;此時,其父 ViewGroup 的 onIntercept 函數仍會被調用,仍能進行攔截,但它本身的 onIntercept 不會被調用了。這個能夠從第2點註釋中找到答案,若是事件被消費了,mFirstTouchTarget != null, 後續事件能夠從mFirstTouchTarget 鏈表中直接分發,同時後續事件過來的時候會跳過intercepted 的判斷,因此本身的 onIntercept 就不會調用了。rest

RecyclerView 的事件傳遞

這裏以點擊 RecyclerView 中的某個Item中的 Button 爲例:code

點下Button

  1. 產生了一個down事件,activity-->phoneWindow-->ViewGroup-->ListView-->botton,中間若是有重寫了攔截方法,則事件被該view攔截可能消耗;
  2. 沒攔截,事件到達了button,這個過程當中創建了一條事件傳遞的view鏈表;
  3. 到button的dispatch方法-->onTouch-->view是否可用-->Touch代理;

移動點擊按鈕的時候

  1. 產生move事件,RecyclerView 中會對move事件作攔截;
  2. 此時 RecyclerView 會將該滑動事件消費掉;
  3. 後續的滑動事件都會被 RecyclerView 消費掉;
  4. Button以前已經處理了 down 事件,如今還在等着後續事件,這個時候 RecyclerView 就會發出 cancel 事件通知Button不要再等了

手指擡起 前面創建了一個view鏈表,RecyclerView 的父view在獲取事件的時候,會直接取鏈表中的RecyclerView 讓其進行事件消耗

有興趣的同窗能夠帶着這個步驟去追蹤 RecyclerView 的源代碼。

多點觸控

多點觸控涉及到了多個手指點擊事件的處理,這裏要增長兩個額外的事件

  1. ACTION_POINTER_DOWN:額外⼿手指按下(按下以前已經有別的⼿手指觸摸到 View)
  2. ACTION_POINTER_UP:有⼿手指擡起,但不不是最後⼀一個(擡起以後,仍然還有別的⼿手指在觸摸着 View)

事件類型: ACTION_POINTER_UP; active pointer index: 0; pointer: x: 200, y: 300, index: 0, id: 1; pointer: x: 300, y: 500, index: 1, id: 2

多點觸控觸摸事件的結構

  1. 觸摸事件是按序列列來分組的,每⼀一組事件必然以 ACTION_DOWN 開頭,以 ACTION_UP 或 ACTION_CANCEL 結束;
  2. ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 和 ACTION_MOVE ⼀同樣,只是事件序列列中 的組成部分,並不不會單獨分出新的事件序列列;
  3. 同⼀一時刻,⼀一個 View 要麼沒有事件序列列,要麼只有⼀一個事件序列列;
  4. 多點觸控要解決的問題之一是:手指觸摸的順序,手指的區分,這兩個問題經過 index 和 id 來區分;
  5. 多點觸控要解決的問題二:多點觸控時滑動了一個手指,這時候要知道動的是哪一個

多點觸控的三種類型

  • 接⼒力力型 同⼀一時刻只有⼀一個 pointer 起做⽤用,即最新的 pointer。 典型:ListView、 RecyclerView。 實現⽅方式:在 ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 時記錄下最 新的 pointer,在以後的 ACTION_MOVE 事件中使⽤用這個 pointer 來判斷位置。
  • 配合型 全部觸摸到 View 的 pointer 共同起做⽤用。 典型:ScaleGestureDetector,以及 GestureDetector 的 onScroll() ⽅方法判斷。 實現⽅方式:在 每一個 DOWN、POINTER_DOWN、POINTER_UP、UP 事件中使⽤用全部 pointer 的座標來共同更更新焦點座標,並在 MOVE 事件中使⽤用全部 pointer 的座標來判斷位置。
  • 各⾃自爲戰型 各個 pointer 作不不同的事,互不不影響。 典型:⽀支持多畫筆的畫板應⽤用。 實現⽅方式: 在每一個 DOWN、POINTER_DOWN 事件中記錄下每一個 pointer 的 id,在 MOVE 事件中使⽤用 id 對 它們進⾏行行跟蹤。

滑動衝突處理

什麼是滑動衝突?就是父 View 和子 View 都須要處理滑動,例如父 View 須要左右滑動,子 View 須要上下滑動(ViewPager 嵌套 RecyclerView),一個點擊事件,到底交給誰處理?

首先咱們須要定義好處理規則,而後咱們在父 View 的 onIntercept、子 View 的 onTouchEvent 以及父 View 的 onTouchEvent 函數中實現咱們定義的規則便可。例如父 View 的 onIntercept 中,若是發現是左右滑動,那就攔截,不然不攔截。

NestedScrollView 嵌套 RecyclerView 也是同樣的道理,NestedScrollView 發現是上下滑動,就直接攔截並處理,RecyclerView 就沒有處理的機會了。

參考文章

Piasy:事件傳遞及滑動衝突的處理

簡書連接

相關文章
相關標籤/搜索