大領導給小明安排任務——Android觸控事件

Taylor發表於2019-03-03

這是Android觸控事件系列文章的第一篇。

  1. 大領導給小明安排任務——Android觸控事件
  2. 大領導又給小明安排任務——Android觸控事件

大領導安排任務會經歷一個“遞”的過程:大領導先把任務告訴小領導,小領導再把任務告訴小明。也可能會經歷一個“歸”的過程:小明告訴小領導做不了,小領導告訴大領導任務完不成。然後,就沒有然後了。。。。

Android觸控事件和領導安排任務的過程很相似,也會經歷“遞”和“歸”。這一篇會試著閱讀原始碼來分析ACTION_DOWN事件的這個遞迴過程。

(ps: 下文中的 粗斜體字 表示引導原始碼閱讀的內心戲)

分發觸控事件起點

寫一個包含ViewGroupViewActivity的demo,並在所有和touch有關的方法中打log。當觸控事件發生時,Activity.dispatchTouchEvent()總是第一個被呼叫,就以這個方法為切入點:

public class Activity{
    private Window mWindow;
    
    //分發觸控事件
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //讓PhoneWindow幫忙分發觸控事件
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    
    //獲得PhoneWindow物件
    public Window getWindow() {
        return mWindow;
    }
    
    //引數太長,省略了
    final void attach(...) {
        ...
        //構造PhoneWindow
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...
    }
}
複製程式碼

Activity將事件傳遞給PhoneWindow

public class PhoneWindow extends Window implements MenuBuilder.Callback {

    // This is the top-level view of the window, containing the window decor.
    //一個視窗的頂層檢視
    private DecorView mDecor;
    
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //將觸控事件交給DecorView分發
        return mDecor.superDispatchTouchEvent(event);
    }
}

//DecorView繼承自ViewGroup
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks{

    public boolean superDispatchTouchEvent(MotionEvent event) {
        //事件最終由ViewGroup.dispatchTouchEvent()分發觸控事件
        return super.dispatchTouchEvent(event);
    }
}
複製程式碼
  • PhoneWindow繼續將事件傳遞給DecorView,最終呼叫了ViewGroup.dispatchTouchEvent()
  • 至此可以做一個簡單的總結:觸控事件的傳遞從Activity開始,經過PhoneWindow,到達頂層檢視DecorViewDecorView呼叫了ViewGroup.dispatchTouchEvent()

觸控事件之“遞”

  • 在分析View繪製時,也遇到過“dispatchXXX”函式ViewGroup.dispatchDraw(),它用於遍歷孩子並觸發它們自己繪製自己。dispatchTouchEvent()會不會也遍歷孩子並將觸控事件傳遞給它們? 帶著這個疑問來看下原始碼:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!canceled && !intercepted) {
            ...
            //遍歷孩子
            for (int i = childrenCount - 1; i >= 0; i--) {
                //按照索引順序或者自定義繪製順序遍歷孩子
                final int childIndex = customOrder
                      ? getChildDrawingOrder(childrenCount, i) : I;
                final View child = (preorderedList == null)
                      ? children[childIndex] : preorderedList.get(childIndex);
                ...
                							
                //如果孩子不在觸控區域則直接跳過
                if (!canViewReceivePointerEvents(child)
                      || !isTransformedTouchPointInView(x, y, child, null)) {
                      ev.setTargetAccessibilityFocus(false);
                      continue;
                }
                ...
                //轉換觸控座標並分發給孩子(child引數不為null)
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    //這裡的程式碼也很關鍵,先埋伏筆1
                }
                 ...
            }
        }
        if (mFirstTouchTarget == null) {
                //這裡的程式碼也很關鍵,先埋伏筆2
        } else {
            //這裡的程式碼也很關鍵,先埋伏筆3
        }
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        ...
        // Perform any necessary transformations and dispatch.
        //進行必要的座標轉換然後分發觸控事件
        if (child == null) {
            //這裡的程式碼也很關鍵,先埋伏筆3
        } else {
            //將ViewGroup座標系轉換為它孩子的座標系(座標原點從ViewGroup左上角移動到孩子左上角)
            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);
        }
        ...
        return handled;
    }
}
複製程式碼

果然沒猜錯!父控制元件在ViewGroup.dispatchTouchEvent()中會遍歷孩子並將觸控事件分發給被點中的子控制元件,如果子控制元件還有孩子,觸控事件的“遞”將不斷持續,直到葉子結點。 最終View型別的葉子結點呼叫的是View.dispatchTouchEvent()

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            //1.通知觸控監聽器OnTouchListener
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            //2.呼叫onTouchEvent()
            //只有當OnTouchListener.onTouch()返回false時,onTouchEvent()才有機會被呼叫
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ...
        //返回值就是onTouch()或者onTouchEvent()的返回值
        return result;
    }
    
    ListenerInfo mListenerInfo;
    
    //監聽器容器類
    static class ListenerInfo {
        ...
        private OnTouchListener mOnTouchListener;
        ...
    }
    
    //設定觸控監聽器
    public void setOnTouchListener(OnTouchListener l) {
        //將監聽器儲存在監聽器容器中
        getListenerInfo().mOnTouchListener = l;
    }
    
    //獲得監聽器管理例項
    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
}
    
複製程式碼
  • View.dispatchTouchEvent()是傳遞觸控事件的終點,消費觸控事件的起點。
  • 消費觸控事件的標誌是呼叫OnTouchListener.onTouch()View.onTouchEvent(),前者優先順序高於後者。只有當沒有設定OnTouchListener或者onTouch()返回false時,View.onTouchEvent()才會被呼叫。
  • 讀到這裡,畫一張圖總結一下觸控事件之“遞”:
    圖1
  • 圖中ViewGroup層後面的N表示在Activity層和View層之間可能有多個ViewGroup層。
  • 圖中自上而下一共有三類層次,觸控事件會從最高層次開始沿著箭頭往下層傳遞。
  • 為簡單起見,圖中省略了另一種觸控事件的處理方式:OnTouchListener.onTouch
  • 圖示觸控事件的傳遞只是眾多傳遞場景中的一種:被點選的View巢狀在ViewGroup中,ViewGroup在Activity中。

觸控事件之“歸”

觸控事件之所以在“遞”之後還會發生“歸”是因為:分發觸控事件的函式還沒有執行完。沿著剛才呼叫鏈相反的方向重新看一遍原始碼:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
   /**
     * Implement this method to handle touch screen motion events.
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     * 返回true表示觸控事件被消費,否則表示未被消費
     */
    public boolean onTouchEvent(MotionEvent event) {
       ...
       if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            //省略了對不同觸控事件的預設處理
            ...
            //只要控制元件是可點選的,就表示觸控事件已被消費
            return true;
        }
        //若控制元件不可點選則不消費觸控事件
        return false;
    }
}
複製程式碼

View.dispatchTouchEvent()呼叫了View.onTouchEvent()後並沒有執行完。View.onTouchEvent()的返回值會影響View.dispatchTouchEvent()的返回值:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        boolean result = false;
        ...
        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)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        //返回當前View是否消費觸控事件的布林值
        return result;
    }
複製程式碼

同樣的,ViewGroup.dispatchTouchEvent()呼叫了View.dispatchTouchEvent()後也沒有執行完,View.dispatchTouchEvent()的返回值會影響ViewGroup.dispatchTouchEvent()的返回值:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    //觸控鏈頭結點
    private TouchTarget mFirstTouchTarget;
    ...
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!canceled && !intercepted) {
            ...
            //遍歷孩子
            for (int i = childrenCount - 1; i >= 0; i--) {
                ...
                //轉換觸控座標並分發給孩子(child引數不為null)
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                      ...
                      //有孩子願意消費觸控事件,將其插入“觸控鏈”
                      newTouchTarget = addTouchTarget(child, idBitsToAssign);
                      //表示已經將觸控事件分發給新的觸控目標
                      alreadyDispatchedToNewTouchTarget = true;
                      break;
                }
                 ...
            }
        }
        if (mFirstTouchTarget == null) {
                //如果沒有孩子願意消費觸控事件,則自己消費(child引數為null)
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                //遍歷觸控鏈分發觸控事件給所有想接收的孩子
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        //如果已經將觸控事件分發給新的觸控目標,則返回true
                        handled = true;
                    } else {
                        //這裡的程式碼很重要,繼續埋伏筆,待下一篇分析。
                    }
                    predecessor = target;
                    target = next;
                }
        }
        ...
        //返回觸控事件是否被孩子或者自己消費的布林值
        return handled;
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        ...
        // Perform any necessary transformations and dispatch.
        //進行必要的座標轉換然後分發觸控事件
        if (child == null) {
            //ViewGroup孩子都不願意消費觸控事件 則其將自己當成View處理(呼叫View.dispatchTouchEvent())
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            //將觸控事件分發給孩子
        }
        ...
        return handled;
    }
    
    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     * 新增View到觸控鏈頭部
     * @param child  View
     * @param pointerIdBits
     * @return 新觸控目標
     */
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
}
複製程式碼
  • 上面這段程式碼補全了上一節中買下的伏筆。原來當孩子願意消費觸控事件時,ViewGroup會將其接入“觸控鏈”,如果觸控鏈中沒有結點則表示沒有孩子願意消費事件,此時ViewGroup只能自己消費事件。ViewGroupView的子類,他們消費觸控事件的方式一摸一樣,都是通過View.dispatchTouchEvent()呼叫View.onTouchEvent()OnTouchListener.onTouch()
  • 沿著回溯鏈,再向上“歸”一步:
public class Activity {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            //如果佈局中有控制元件願意消費觸控事件,則返回true,onTouchEvent()不會被呼叫
            return true;
        }
        return onTouchEvent(ev);
    }
}
複製程式碼

ViewViewGroupActivity,雖然它們分發觸控事件的邏輯不太一樣,但基本結構都和上面這段程式碼神似,用虛擬碼可以寫成:

//“遞”
if(分發事件給孩子){
    如果孩子消費了事件 直接返回(將觸控事件被消費這一事實往上傳遞)
}
//“歸”
如果孩子沒有消費事件,則自己消費事件
複製程式碼

“分發事件給孩子”這個函式的呼叫表示“遞”,即將觸控事件傳遞給下層。“分發事件給孩子”這個函式的返回表示“歸”,即將觸控事件的消費結果回溯給上層,以便上層採取進一步的行動。

同樣的套路,用圖片總結下觸控事件之“歸”:

圖2

  • 這張圖是對圖1描述場景的補全。圖中黑色的線表示觸控事件的傳遞路徑,灰色的線表示觸控事件回溯的路徑。
  • 因為View.onTouchEvent()返回true,表示消費觸控事件,所以ViewGroup.onTouchEvent()以及Activity.onTouchEvent()都不會被呼叫。

圖3

  • 這張圖是對圖1描述場景的擴充套件。圖中黑色的線表示觸控事件的傳遞路徑,灰色的線表示觸控事件回溯的路徑。
  • 圖示所對應的場景是:被點選的View不消費觸控事件,而ViewGrouponTouchEvent()中返回true自己消費觸控事件。

圖4

  • 這張圖是對圖1描述場景的擴充套件。圖中黑色的線表示觸控事件的傳遞路徑,灰色的線表示觸控事件回溯的路徑。
  • 圖示所對應的場景是:被點選的ViewViewGroup都不消費觸控事件,最後只能由Activity來消費觸控事件。

總結

  • Activity接收到觸控事件後,會傳遞給PhoneWindow,再傳遞給DecorView,由DecorView呼叫ViewGroup.dispatchTouchEvent()自頂向下分發ACTION_DOWN觸控事件。
  • ACTION_DOWN事件通過ViewGroup.dispatchTouchEvent()DecorView經過若干個ViewGroup層層傳遞下去,最終到達View
  • 每個層次都可以通過在onTouchEvent()OnTouchListener.onTouch()返回true,來告訴自己的父控制元件觸控事件被消費。只有當下層控制元件不消費觸控事件時,其父控制元件才有機會自己消費。
  • 觸控事件的傳遞是從根檢視自頂向下“遞”的過程,觸控事件的消費是自下而上“歸”的過程。

讀到這裡可能對於觸控事件還充滿諸多疑問:

  1. ViewGroup層是否有辦法攔截觸控事件?
  2. ACTION_DOWN只是觸控序列的起點,後序的ACTION_MOVEACTION_UPACTION_CANCEL是如何傳遞的?

這些問題會在下一篇繼續分析。

相關文章