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

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

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

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

Android觸摸事件和領導安排任務的過程很類似,也會經歷「遞」和「歸」。這一篇會試着閱讀源碼來分析ACTION_DOWN事件的這個遞歸過程。函數

(ps: 下文中的 粗斜體字 表示引導源碼閱讀的心裏戲)佈局

分發觸摸事件起點

寫一個包含ViewGroupViewActivity的demo,並在全部和touch有關的方法中打log。當觸摸事件發生時,Activity.dispatchTouchEvent()老是第一個被調用,就以這個方法爲切入點:post

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將事件傳遞給PhoneWindowui

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()this

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中。

觸摸事件之「歸」

觸摸事件之因此在「遞」以後還會發生「歸」是由於:分發觸摸事件的函數尚未執行完。沿着剛纔調用鏈相反的方向從新看一遍源碼:spa

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()的返回值:3d

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()的返回值:code

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層層傳遞下去,最終到達ViewView.dispatchTouchEvent()被調用。
  • View.dispatchTouchEvent()是傳遞事件的終點,消費事件的起點。它會調用onTouchEvent()OnTouchListener.onTouch()來消費事件。
  • 每一個層次均可以經過在onTouchEvent()OnTouchListener.onTouch()返回true,來告訴本身的父控件觸摸事件被消費。只有當下層控件不消費觸摸事件時,其父控件纔有機會本身消費。
  • 觸摸事件的傳遞是從根視圖自頂向下「遞」的過程,觸摸事件的消費是自下而上「歸」的過程。

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

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

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

相關文章
相關標籤/搜索