深入理解Android事件分發機制
在理解事件分發機制之前,我們先要明白,事件分發機制是為View服務的,而View是Android中所有控制元件的基類,View可以是單個的,而多個View組成可以叫做ViewGroup。不管什麼View控制元件,他們基類都是View,在Android多個View的疊加則Web中的DOM樹形結構,所以當我們點選一個區域有多個View的情況下, 到底這時候該哪個View來響應我們的點選事件呢?事件分發機制就是為了解決這個問題而產生的。

ViewGroup官方文件繼承關係.png
-
事件
- 理解事件分發機制,首先我們要了解事件是什麼,這裡事件主要指我們操作手機的觸控事件。在Android中所有的輸入事件都放在了MotionEvent中。
- MotionEvent是個很龐大的東西,有單點觸控、多點觸控、滑鼠事件等等,這裡簡單列出基本的單點事件,不做更多深入討論。
事件 簡介 ACTION_DOWN 手指 初次接觸到螢幕 時觸發 ACTION_MOVE 手指在 螢幕上滑動 時觸發,會會多次觸發 ACTION_UP 手指 離開螢幕 時觸發 ACTION_CANCEL 事件 被上層攔截 時觸發 - 正常情況下觸控一次螢幕觸發事件序列為ACTION_DOWN-->ACTION_UP
- 有滑動動作的單點序列為ACTION_DOWN-->ACTION_MOVE ..... ACTION_MOVE-->ACTION_UP
-
點選事件分發流程
-
首先我們來看一個比較有意思的例子來帶入,我們定義一個公司的幾個角色
-
老闆(Activity)
/** * Created by maoqitian on 2018/5/10 0010. * 事件分發機制測試 老闆 */ public class DispatchTouchEventTestActivity extends AppCompatActivity { private static final String TAG = Action.TAG1; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_dispatch_touch_event_test); } //Actiivty 只有 dispatchTouchEvent 和 onTouchEvent 方法 @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN){ Log.i(TAG,Action.dispatchTouchEvent+"經理,現在專案做到什麼程度了?"); } return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN){ Log.i(TAG, Action.onTouchEvent); } return super.onTouchEvent(event); }
-
經理(RootView)
/** * 經理 */ public class RootView extends RelativeLayout { private static final String TAG = Action.TAG2; public RootView(Context context) { super(context); } public RootView(Context context, AttributeSet attrs) { super(context, attrs); } public RootView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { Log.i(TAG, Action.dispatchTouchEvent + "技術部,你們的app快做完了麼?"); } return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { Action.onInterceptTouchEvent+"老闆問專案進度" ); } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { Log.i(TAG, Action.onTouchEvent +"....."); } return super.onTouchEvent(event); } }
-
組長(ViewGroup)
/** * 組長 */ public class ViewGroupA extends RelativeLayout { private static final String TAG = Action.TAG3; public ViewGroupA(Context context) { super(context); } public ViewGroupA(Context context, AttributeSet attrs) { super(context, attrs); } public ViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { Log.i(TAG, Action.dispatchTouchEvent + "專案進度?"); } return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { Log.i(TAG, Action.onInterceptTouchEvent + "我問問程式員"); } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { Log.i(TAG, Action.onTouchEvent); } return super.onTouchEvent(event); } }
-
程式設計師(View1)
/** * 碼農 */ public class View1 extends View { private static final String TAG = Action.TAG4; public View1(Context context) { super(context); } public View1(Context context, AttributeSet attrs) { super(context, attrs); } public View1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } //View最為事件傳遞的最末端,要麼消費掉事件,要麼不處理進行回傳,根本沒必要進行事件攔截 @Override public boolean dispatchTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { Log.i(TAG, Action.dispatchTouchEvent+"app完成進度麼?"); } return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { Log.i(TAG, Action.onTouchEvent+"做好了."); } return true; } }
-
掃地阿姨(View2)
/** * 掃地阿姨 */ public class View2 extends View { private static final String TAG = Action.TAG5; public View2(Context context) { super(context); } public View2(Context context, AttributeSet attrs) { super(context, attrs); } public View2(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean dispatchTouchEvent(MotionEvent event) { if(event.getAction() == MotionEvent.ACTION_DOWN){ Log.i(TAG, Action.dispatchTouchEvent+"我只是個掃地阿姨,我不懂你說什麼"); } return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { Log.i(TAG, Action.onTouchEvent+"經理你問錯人了,去問老闆吧"); } return super.onTouchEvent(event); } }
Demo截圖.png
[圖片上傳中...(場景一執行結果.png-722662-1540099795552-0)]
-
-
場景一:老闆詢問App專案進度,事件經過每個領導傳遞到達程式設計師處,程式設計師完成了專案(點選事件被View1消費了)
場景一執行結果.png
-
場景二 :老闆異想天開,想造宇宙飛船,事件經過每個領導傳遞到達程式設計師處,程式設計師表示做不了,反饋給老闆(事件沒有被消費)
@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { Log.i(TAG, Action.onTouchEvent+"這個真心做不了啊,把我做了吧"); } return super.onTouchEvent(event); }
場景二執行結果.png
-
場景三:老闆詢問技術部本月表現,只需要組長彙報就行,不需要通知程式設計師(ViewGroup 攔截並消費了事件)
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { Log.i(TAG, Action.onInterceptTouchEvent + "我看看組員績效情況"); } //return super.onInterceptTouchEvent(ev); return true;//攔截事件 onTouchEvent 中進行處理 } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { Action.onTouchEvent+"技術部組員最近表現都很好,專案按時完成,沒有遲到早退"); } return true;//消費事件 }
場景三執行結果.png
-
從這三個場景我們可看出事件分發機制主要有三個方法來處理
-
public boolean dispatchTouchEvent(MotionEvent ev) {}
- 該方法的作用是事件的分發,返回結果表示是否消耗事件,消耗則會呼叫當前View的onTouchEvent,否則傳遞事件,呼叫子View的dispatchTouchEvent方法,只要時間傳遞到該View,dispatchTouchEvent方法必定是會被首先呼叫的。
-
public boolean onInterceptTouchEvent(MotionEvent ev) {}
- 該方法表示是否對分發的事件進行攔截,如果進行了攔截,則該方法在這一次的時間傳遞序列中獎不會被再呼叫,該方法在dispatchTouchEvent被呼叫,我們需要注意一點,View是沒有該方法的,View是單個的,我們可以理解它為事件傳遞的終點,終點要麼消費事件,要麼不消費事件把事件進行回傳,而ViewGroup則包含不止一個View,所以他可以把時間傳遞給子View,也可以攔截事件自己處理不傳遞給子View。
-
public boolean onTouchEvent(MotionEvent event) {}
- 該方法表示處理攔截的事件,如果不進行處理(事件消耗),也就是不反回true,則當前View不會再次接收到該事件
-
-
三個方法之間的關係
public boolean dispatchTouchEvent(MotionEvent ev) { boolean isDispatch; if(onInterceptTouchEvent(ev)){ isDispatch=onTouchEvent(ev); }else { isDispatch=childView.dispatchTouchEvent(ev); } return isDispatch; }
- 結合這段虛擬碼和前面的例子的場景三,我們可以發現ViewGroup的事件分發規則是這樣的,時間傳遞到ViewGroup首先呼叫它的dispatchTouchEvent方法,接下來是呼叫onInterceptTouchEvent方法,如果該方法但會true,則說明當前ViewGroup要攔截該事件,攔截之後則呼叫當前ViewGroup的onTouchEvent方法,如果不進行攔截則呼叫子View的dispatchTouchEvent方法,結合場景二,如果到最後事件都沒有被消費掉,則最後返回Activity,Activity不處理則事件消失。
- 結合場景一、場景二,View接收到事件,如果進行處理,則直接在onTouchEvent進行處理返回true就表示事件被消費了,不進行處理則呼叫父類onTouchEvent方法或者返回false表示不消費該事件,然後事件再原路返回向上傳遞。
-
前面我們只是描述了ViewGroup和View之間的時間傳遞,我們看到例子中的場景事件都是從老闆(Activity)開始的,而Activity本身並不是繼承View,所以我們需要了解Activity是如何把事件傳遞到View的,從原始碼的角度來看是比較清晰的,下面一起來看看。
-
Activity 本身並不是View,那他去哪裡載入View呢?setContentView()這個方法相信大家都不陌生,他載入我們的佈局,佈局中包括控制元件,也就是載入我們的View,
/** * Set the activity content from a layout resource.The resource will be * inflated, adding all top-level views to the activity. * * @param layoutResID Resource ID to be inflated. * * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */ public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
我們可以看到呼叫的是 getWindow().setContentView(layoutResID)這個方法,繼續找getWindow()
/** * Retrieve the current {@link android.view.Window} for the activity. * This can be used to directly access parts of the Window API that * are not available through Activity/Screen. * * @return Window The current window, or null if the activity is not *visual. */ public Window getWindow() { return mWindow; }
getWindow()方法返回的是mWindow,繼續找mWindow物件,發現在Activity中定義的是Window物件
private Window mWindow;
- 檢視Window原始碼,註釋說得非常清楚,Window的唯一實現類是PhoneWindow
/** * <p>The only existing implementation of this abstract class is * android.view.PhoneWindow, which you should instantiate when needing a * Window. */ public abstract class Window {...}
- 在Activity原始碼的attch()方法中我們也看到 mWindow 的例項物件確實是PhoneWindow
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window, ActivityConfigCallback activityConfigCallback) { ..... mWindow = new PhoneWindow(this, window, activityConfigCallback); ......}
所以我們繼續看PhoneWindow,這時必須要記住,我們還在找setContentView()方法,PhoneWindow的setContentView()方法
@Override public void setContentView(int layoutResID) { // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { mLayoutInflater.inflate(layoutResID, mContentParent); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true; }
該方法中我們重點看installDecor()方法
private void installDecor() { mForceDecorInstall = false; if (mDecor == null) { mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } else { mDecor.setWindow(this); } if (mContentParent == null) { mContentParent = generateLayout(mDecor); ....... } ....... }
好像沒發現什麼,繼續看generateDecor(int featureId)方法
protected DecorView generateDecor(int featureId) { // System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity. Context context; ...... return new DecorView(context, featureId, this, getAttributes()); }
到此我們發現,他返回的是DecorView,DecorView是PhoneWindow的內部類,我們再看generateLayout(mDecor)方法
protected ViewGroup generateLayout(DecorView decor){ .... // Inflate the window decor. int layoutResource; int features = getLocalFeatures(); // System.out.println("Features: 0x" + Integer.toHexString(features)); if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { layoutResource = R.layout.screen_swipe_dismiss; setCloseOnSwipeEnabled(true); } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) { if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleIconsDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = R.layout.screen_title_icons; } // XXX Remove this once action bar supports these features. removeFeature(FEATURE_ACTION_BAR); // System.out.println("Title Icons!"); } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 && (features & (1 << FEATURE_ACTION_BAR)) == 0) { // Special case for a window with only a progress bar (and title). // XXX Need to have a no-title version of embedded windows. layoutResource = R.layout.screen_progress; // System.out.println("Progress!"); } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) { // Special case for a window with a custom title. // If the window is floating, we need a dialog layout if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogCustomTitleDecorLayout, res, true); layoutResource = res.resourceId; } else { layoutResource = R.layout.screen_custom_title; } // XXX Remove this once action bar supports these features. removeFeature(FEATURE_ACTION_BAR); } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { // If no other features and not embedded, only need a title. // If the window is floating, we need a dialog layout if (mIsFloating) { TypedValue res = new TypedValue(); getContext().getTheme().resolveAttribute( R.attr.dialogTitleDecorLayout, res, true); layoutResource = res.resourceId; } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) { layoutResource = a.getResourceId( R.styleable.Window_windowActionBarFullscreenDecorLayout, R.layout.screen_action_bar); } else { layoutResource = R.layout.screen_title; } // System.out.println("Title!"); } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) { layoutResource = R.layout.screen_simple_overlay_action_mode; } else { // Embedded, so no decoration is needed. layoutResource = R.layout.screen_simple; // System.out.println("Simple!"); } ....... }
該方法比較長,只擷取一部分,方法根據不同的情況載入不同的佈局給layoutResource,看其中一個layout.screen_title佈局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:fitsSystemWindows="true"> <!-- Popout bar for action modes --> <ViewStub android:id="@+id/action_mode_bar_stub" android:inflatedId="@+id/action_mode_bar" android:layout="@layout/action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="?attr/actionBarTheme" /> <FrameLayout android:layout_width="match_parent" android:layout_height="?android:attr/windowTitleSize" style="?android:attr/windowTitleBackgroundStyle"> <TextView android:id="@android:id/title" style="?android:attr/windowTitleStyle" android:background="@null" android:fadingEdge="horizontal" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout> <FrameLayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="1" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" /> </LinearLayout>
這時我們只是瞭解了Activity的setContentView方法,我們看看Activity的dispatchTouchEvent方法
/** * Called to process touch screen events.You can override this to * intercept all touch screen events before they are dispatched to the * window.Be sure to call this implementation for touch screen events * that should be handled normally. * * @param ev The touch screen event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
顯然呼叫getWindow().superDispatchTouchEvent(ev),根據前面的分析也就是PhoneWindow的dispatchTouchEvent方法
// This is the top-level view of the window, containing the window decor. private DecorView mDecor; @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } // This is the view in which the window contents are placed. It is either // mDecor itself, or a child of mDecor where the contents go. ViewGroup mContentParent;
可以看到PhoneWindow的superDispatchTouchEvent呼叫的是DecorView的superDispatchTouchEvent方法,前面我們知道DecorView其實是ViewGroup(上述generateLayout(mDecor)返回值),到此我們可以串聯起來, Activity的setContentView其實是Window物件的實現是其唯一實現類PhoneWindown的內部類DecorView來作為Activity的根View,也就是說從Activity開始傳遞的是從PhoneWindow開始 ,也就是原始碼中的installDecor得到的DecorView充當了Activity傳遞事件的View,DecorView可以理解為當前頁面的底層容器,底層容器DecorView在根據自己是ViewGroup把事件再向他的子View傳遞,也就是我們平時寫的介面最上層View,也就是setContentView載入的佈局根佈局View,下圖結合例項很清晰的可以表示出Activity的構成。
Activity構成對比圖.png
- 到此我們可以寫出一個事件傳遞的流程為
Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View
- 總結一下每個傳遞者具有的方法,我們注意到Activity沒有onInterceptTouchEvent方法,其實很容易理解,Activity作為事件的初始傳遞者如果攔截了事件,也就是我們點選介面無響應,這也就使得我們使用者的點選沒什麼意義,肯定是我們點選介面中的某個view響應才符合操作。(PhoneWindow在Android都是隱藏的,不做記錄)
型別 相關方法 Activity ViewGroup View 事件分發 dispatchTouchEvent 有 有 有 事件攔截 onInterceptTouchEvent 無 有 無 事件消費 onTouchEvent 有 有 有 -
點選事件分發原則
- onInterceptTouchEvent攔截事件,該View的onTouchEvent方法才會被呼叫,只有onTouchEvent返回true才表示該事件被消費,否則回傳到上層View的onTouchEvent方法。
- 如果事件一直不被消費,則最終回傳給Activity,Activity不消費則事件消失。
- 事件是否被消費是根據返回值,true表示消費,false表示不消費。
-
-
從原始碼角度繼續分析ViewGroup和View事件傳遞流程
經過前面的研究,我們回顧一下,一個點選事件用MotionEvent表示,事件最先傳遞到Activity,呼叫Activity的dispatchTouchEvent方法,事件處理工作交給PhoneWindow,PhoneWindow在把事件傳遞給DecorView,最後DecorView作為我們介面底層容器裝載我們setContentView的佈局,我們寫佈局一般都是啥layout作為根佈局,也就是ViewGroup,DecorView把事件傳遞到ViewGroup的dispatchTouchEvent方法,我們就從ViewGroup的dispatchTouchEvent原始碼開始分析
-
ViewGroup事件傳遞流程
-
ViewGroup方法比較長,我們一段一段來
/** * When set, this ViewGroup should not intercept touch events. * {@hide} */ @UnsupportedAppUsage protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000; @Override public boolean dispatchTouchEvent(MotionEvent ev) { ...... // Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); } // Check for interception. 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 { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } .....} /** * Resets all touch state in preparation for a new cycle. */ private void resetTouchState() { clearTouchTargets(); ..... mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; ...... } /** * Clears all touch targets. */ private void clearTouchTargets() { ...... mFirstTouchTarget = null; } }
-
一上來首先判斷了事件是否為ACTION_DOWN,如果是ACTION_DOWN事件,則呼叫resetTouchState()方法,resetTouchState()鐘調用了clearTouchTargets()使mFirstTouchTarget=null,而前面我們瞭解事件的時候也說過一個事件是ACTION_DOWN開始到ACTION_UP結束,也就是說ACTION_DOWN出現表示一個新的事件的開始;接下來再次判斷為ACTION_DOWN和mFirstTouchTarget!=null,我們看到條件成立之後才能呼叫onInterceptTouchEvent方法,也就是說mFirstTouchTarget!=null成立說明此時不攔截事件,而mFirstTouchTarget==null成立則說明事件已經被攔截,並且不會再有ACTION_DOWN,因為此時這個一個事件還沒結束,此時不管ACTION_MOVE還是ACTION_UP動作,都交由現在攔截了事件的ViewGroup來處理, 並且不會再次呼叫onInterceptTouchEvent方法 (說明該方法並不是每次都會呼叫的)。
-
我們還看到一個標記位FLAG_DISALLOW_INTERCEPT,它一般是由子View的requestDisallowInterceptTouchEvent方法設定的,表示ViewGroup無法攔截除了ACTION_DOWN以外的其他動作,我們看到原始碼第一個判斷就會明白,只要是ACTION_DOWN動作,這個標記位都會被重置,並且ViewGroup會呼叫自己onInterceptTouchEvent方法表達是否需要攔截這新一輪的點選事件。
-
-
接著看dispatchTouchEvent方法剩下的其他程式碼段
public boolean dispatchTouchEvent(MotionEvent ev) { ....... if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // Find a child that can receive the event. // Scan children from front to back. final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } ...... } /** * Transforms a motion event into the coordinate space of a particular child view, * filters out irrelevant pointer ids, and overrides its action if necessary. * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead. */ private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { ....... if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); ....... }
- 這裡顯示的邏輯還是非常清晰的,如果ViewGroup不攔截點選事件,則首先遍歷子View的最外層,獲取點選事件的X座標和Y座標判斷是否和當前子View的座標相匹配,而dispatchTransformedTouchEvent方法實際上就是呼叫子View的dispatchTouchEvent方法,這樣就完成了ViewGroup到子View的事件分發。
- ViewGroup預設不攔截任何事件,他的onInterceptTouchEvent方法預設返回false
public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.isFromSource(InputDevice.SOURCE_MOUSE) //滑鼠點選處理 && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { return true; } return false; }
-
- 如下原始碼,如果ViewGroup將事件傳遞到子View,則會呼叫addTouchTarget(child, idBitsToAssign)方法,並退出遍歷子View的迴圈 ``` public boolean dispatchTouchEvent(MotionEvent ev) { ....... newTouchTarget = addTouchTarget(child, idBitsToAssign); break; ..... // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } ..... } /** * Adds a touch target for specified child to the beginning of the list. * Assumes the target child is not already present. */ private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; } ``` - 如上原始碼,呼叫addTouchTarget方法,會給mFirstTouchTarget賦值,也就是說mFirstTouchTarget!=null,前面我們已經討論過,mFirstTouchTarget==null則攔截所有的事件給該ViewGroup處理,可見mFirstTouchTarget是否賦值對於ViewGroup的事件攔截起了關鍵的作用。 - 接著往下看,如果子View遍歷結束後事件還是沒有進行處理,這樣的情況有兩種可能,一個就是上面提到的例子場景二,ViewGroup的子View沒有消費事件,也就是子View的onTouchEvent返回了false,另一個情況則是則是ViewGroup子View,也就不存在事件傳遞子View的情況。我們看如下程式碼,是在上面分析的程式碼之後出現,第三個引數子View為null,也就是呼叫super.dispatchTouchEvent(event)方法,ViewGroup是繼承View,也就是說不管是否攔截,ViewGropu最終還是將點選事件交由到View來處理了,只是child.dispatchTouchEvent還是super.dispatchTouchEvent的問題。 ViewGroup的原始碼事件分發就到這裡,接下來我們分析一下View的事件分發流程。 ``` public boolean dispatchTouchEvent(MotionEvent ev) { ..... // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } ..... } ```
-
View的事件分發流程
- 首先我們看View的dispatchTouchEvent方法
`` /** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ 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; } } ..... return result; } ``` - 我們看到上面的原始碼中,View對於點選的事件的處理首先是判斷是註冊OnTouchListener,並且如果OnTouchListener的onTouch放回true,則整個dispatchTouchEvent返回true,已經攔截了事件,則不會執行下面的onTouchEvent方法的呼叫,也就是說事件攔截了,但是不呼叫onTouchEvent方法,這裡其實很好理解,如果開發者註冊了OnTouchListener並在onTouch放回true,說明開發者是想自己來處理觸控事件,而onTouchEvent是屬於Android的事件傳遞機制方法,是系統幫我們處理的,所以當我們自己處理了點選事件,就不需要系統來再次處理了。所以OnTouchListener的呼叫有先級高於onTouchEvent。 - 如果Ciew沒有註冊OnTouchListener方法,接下來事件傳遞到onTouchEvent方法,我們接著看onTouchEvent原始碼 ``` /** * Implement this method to handle touch screen motion events. * <p> * If this method is used to detect click actions, it is recommended that * the actions be performed by implementing and calling * {@link #performClick()}. This will ensure consistent system behavior, * including: * <ul> * <li>obeying click sound preferences * <li>dispatching OnClickListener calls * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when * accessibility features are enabled * </ul> * * @param event The motion event. * @return True if the event was handled, false otherwise. */ public boolean onTouchEvent(MotionEvent event) { .... final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return clickable; } .... } ``` - 通過上面原始碼和註釋,我們可以知道,View即使是處於不可用狀態,他還是會消費(consumes)點選事件,只是他不會響應點選事件,也就是返回各種點選的狀態(點選,長按)。 - 接著看看剩下原始碼對點選事件的處理 ``` public boolean onTouchEvent(MotionEvent event) { if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: ...... if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { ....... if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClickInternal(); } } } ..... } break; } .... return true; } return false; } ``` - 觸控事件結束,也就是ACTION_UP,所以這裡我們看對於ACTION_UP的處理就可以了。我們看到對於ACTION_UP,,如果沒有!clickable,也就是沒有View的CLICKABLE、LONG_CLICKABLE和CONTEXT_CLICKABLE都不存在,則清除所有的狀態回撥等,如果其中一個存在,則直接消費這個時間,我們看到方法後面有個retrun true存在,也證實事件被消費了,也就是onTouchEvent方法返回了true。而如果ACTION_UP沒有消費事件,最終onTouchEvent方法是返回false。 - 到這裡,我們還看到ACTION_UP事件會觸發performClickInternal();方法,我們看看他做了什麼 ``` private boolean performClickInternal() { // Must notify autofill manager before performing the click actions to avoid scenarios where // the app has a click listener that changes the state of views the autofill service might // be interested on. notifyAutofillManagerOnClick(); return performClick(); } public boolean performClick() { // We still need to call this method to handle the cases where performClick() was called // externally, instead of through performClickInternal() notifyAutofillManagerOnClick(); final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result; } ``` - 可以從原始碼看到他最終呼叫的是performClick()方法,如果View設定了OnClickListener,則會呼叫onClick方法。我們知道View預設的LONG_CLICKABLE是false,而CLICKABLE需要根據具體View才能知道,比如Button是可點選的,則CLICKABLE為true,而ImageView預設是不可點選的,所以CLICKABLE為false,但是開發中我們也發現,不管View是否可以點選,只要我們設定setOnClickListener()或者setOnLongClickListener()方法,則該View就是可以被點選或者長按的,也就是LONG_CLICKABLE或者CLICKABLE為true。我們從原始碼可以看出。到此,從原始碼角度分析事件分發機制的流程我們已經走完。 ``` /** * Register a callback to be invoked when this view is clicked. If this view is not * clickable, it becomes clickable. * * @param l The callback that will run * * @see #setClickable(boolean) */ public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l; } public void setOnLongClickListener(@Nullable OnLongClickListener l) { if (!isLongClickable()) { setLongClickable(true); } getListenerInfo().mOnLongClickListener = l; } ```
- 經過前面的分析,我們還可以排出與View相關的事件排程優先順序為onTouchListener>onTouchEvent > onLongClickListener > onClickListener
最後,總結事件分發機制的核心知識點
- 正常情況下觸控一次螢幕觸發事件序列為ACTION_DOWN-->ACTION_UP
- 當一個View決定攔截,那麼這一個事件序列只能由這個View來處理,onInterceptTouchEvent方法並不是每次產生動作都會被呼叫到,當我們需要提前出來想要攔截的動作需要在事件必須傳遞到該ViewGroup的前提下在dispatchTouchEvent方法中程序操作。
- 一個View開始處理事件,但是它不消耗ACTION_DOWN,也就是onTouchEvent返回false,則這個事件會交由他的父元素的onTouchEvent方法來進行處理,而這個事件序列的其他剩餘ACACTION_MOVE,ACTION_UP也不會再給該View來處理。
- View沒有onInterceptTouchEvent方法,View一旦接收到事件就呼叫onTouchEvent方法
- ViewGroup預設不攔截任何事件(onInterceptTouchEvent方法預設返回false)。
- View的onTouchEvent方法預設是處理點選事件的,除非他是不可點選的(clickable和longClickable同時為false)
- 事件分發機制的核心原理就是責任鏈模式,事件層層傳遞,直到被消費。
終於,把事件分發機制給回顧了一遍,其實五月份的時候我就複習過一次事件分發機制,但是當時沒有記錄,所以這次在回頭看以前記得有些知識點感覺還是模糊,所以記錄下來才能在以後忘記的時候去回顧再總結。如果文章中有寫得不對的地方,請給我留言指出,大家一起學習進步。如果覺得我的文章給予你幫助,也請給我一個喜歡和關注。
-
參考連結
- ofollow,noindex">Android 原始碼 Activity
- Android 原始碼 Window
- Android 原始碼 PhoneWindow
- Android 原始碼 screen_title.xml
- Android 原始碼 ViewGroup
- Android 原始碼 View
- Android事件傳遞機制分析
-
參考書籍
- 《Android開發藝術探索》
- 《Android進階之光》