在理解事件分發(fā)機制之前慕的,我們先要明白箩张,事件分發(fā)機制是為View服務(wù)的甩骏,而View是Android中所有控件的基類,View可以是單個的伏钠,而多個View組成可以叫做ViewGroup横漏。不管什么View控件,他們基類都是View熟掂,在Android中多個View的疊加像Web中的DOM樹形結(jié)構(gòu),所以當(dāng)我們點擊一個區(qū)域有多個View的情況下扎拣,到底這時候該哪個View來響應(yīng)我們的點擊事件呢赴肚?事件分發(fā)機制就是為了解決這個問題而產(chǎn)生的。
-
事件
- 理解事件分發(fā)機制二蓝,首先我們要了解事件是什么誉券,這里事件主要指我們操作手機的觸摸事件。在Android中所有的輸入事件都放在了MotionEvent中刊愚。
- MotionEvent是個很龐大的東西踊跟,有單點觸控、多點觸控鸥诽、鼠標(biāo)事件等等商玫,這里簡單列出基本的單點事件,不做更多深入討論牡借。
事件 簡介 ACTION_DOWN 手指初次接觸到屏幕時觸發(fā) ACTION_MOVE 手指在屏幕上滑動時觸發(fā)拳昌,會會多次觸發(fā) ACTION_UP 手指離開屏幕時觸發(fā) ACTION_CANCEL 事件被上層攔截時觸發(fā) - 正常情況下觸摸一次屏幕觸發(fā)事件序列為ACTION_DOWN-->ACTION_UP
- 有滑動動作的單點序列為ACTION_DOWN-->ACTION_MOVE ..... ACTION_MOVE-->ACTION_UP
-
點擊事件分發(fā)流程
-
首先我們來看一個比較有意思的例子來帶入,我們定義一個公司的幾個角色
-
老板(Activity)
/** * Created by maoqitian on 2018/5/10 0010. * 事件分發(fā)機制測試 老板 */ 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+"經(jīng)理,現(xiàn)在項目做到什么程度了?"); } 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); }
-
經(jīng)理(RootView)
/** * 經(jīng)理 */ 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 + "技術(shù)部,你們的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)
/** * 碼農(nóng) */ 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+"經(jīng)理你問錯人了上真,去問老板吧"); } return super.onTouchEvent(event); } }
-
-
場景一:老板詢問App項目進度,事件經(jīng)過每個領(lǐng)導(dǎo)傳遞到達程序員處羹膳,程序員完成了項目(點擊事件被View1消費了)
-
場景二 :老板異想天開谷羞,想造宇宙飛船,事件經(jīng)過每個領(lǐng)導(dǎo)傳遞到達程序員處溜徙,程序員表示做不了湃缎,反饋給老板(事件沒有被消費)
@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { Log.i(TAG, Action.onTouchEvent+"這個真心做不了啊,把我做了吧"); } return super.onTouchEvent(event); }
-
場景三:老板詢問技術(shù)部本月表現(xiàn)蠢壹,只需要組長匯報就行嗓违,不需要通知程序員(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+"技術(shù)部組員最近表現(xiàn)都很好,項目按時完成,沒有遲到早退"); } return true;//消費事件 }
-
從這三個場景我們可看出事件分發(fā)機制主要有三個方法來處理
-
public boolean dispatchTouchEvent(MotionEvent ev) {}
- 該方法的作用是事件的分發(fā)图贸,返回結(jié)果表示是否消耗事件蹂季,消耗則會調(diào)用當(dāng)前View的onTouchEvent,否則傳遞事件疏日,調(diào)用子View的dispatchTouchEvent方法偿洁,只要時間傳遞到該View,dispatchTouchEvent方法必定是會被首先調(diào)用的沟优。
-
public boolean onInterceptTouchEvent(MotionEvent ev) {}
- 該方法表示是否對分發(fā)的事件進行攔截涕滋,如果進行了攔截,則該方法在這一次的時間傳遞序列中獎不會被再調(diào)用挠阁,該方法在dispatchTouchEvent被調(diào)用宾肺,我們需要注意一點,View是沒有該方法的侵俗,View是單個的锨用,我們可以理解它為事件傳遞的終點,終點要么消費事件隘谣,要么不消費事件把事件進行回傳增拥,而ViewGroup則包含不止一個View,所以他可以把時間傳遞給子View寻歧,也可以攔截事件自己處理不傳遞給子View掌栅。
-
public boolean onTouchEvent(MotionEvent event) {}
- 該方法表示處理攔截的事件,如果不進行處理(事件消耗)熄求,也就是不反回true渣玲,則當(dāng)前View不會再次接收到該事件
-
-
三個方法之間的關(guān)系
public boolean dispatchTouchEvent(MotionEvent ev) { boolean isDispatch; if(onInterceptTouchEvent(ev)){ isDispatch=onTouchEvent(ev); }else { isDispatch=childView.dispatchTouchEvent(ev); } return isDispatch; }
- 結(jié)合這段偽代碼和前面的例子的場景三,我們可以發(fā)現(xiàn)ViewGroup的事件分發(fā)規(guī)則是這樣的弟晚,時間傳遞到ViewGroup首先調(diào)用它的dispatchTouchEvent方法忘衍,接下來是調(diào)用onInterceptTouchEvent方法逾苫,如果該方法但會true,則說明當(dāng)前ViewGroup要攔截該事件枚钓,攔截之后則調(diào)用當(dāng)前ViewGroup的onTouchEvent方法铅搓,如果不進行攔截則調(diào)用子View的dispatchTouchEvent方法,結(jié)合場景二搀捷,如果到最后事件都沒有被消費掉星掰,則最后返回Activity,Activity不處理則事件消失嫩舟。
- 結(jié)合場景一氢烘、場景二,View接收到事件家厌,如果進行處理播玖,則直接在onTouchEvent進行處理返回true就表示事件被消費了,不進行處理則調(diào)用父類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(); }
我們可以看到調(diào)用的是 getWindow().setContentView(layoutResID)這個方法沸停,繼續(xù)找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膜毁,繼續(xù)找mWindow對象,發(fā)現(xiàn)在Activity中定義的是Window對象
private Window mWindow;
- 查看Window源碼愤钾,注釋說得非常清楚瘟滨,Window的唯一實現(xiàn)類是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); ......}
所以我們繼續(xù)看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); ....... } ....... }
好像沒發(fā)現(xiàn)什么,繼續(xù)看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()); }
到此我們發(fā)現(xiàn)伙菊,他返回的是DecorView败玉,DecorView是PhoneWindow的內(nèi)部類敌土,我們再看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!"); } ....... }
該方法比較長,只截取一部分运翼,方法根據(jù)不同的情況加載不同的布局給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); }
顯然調(diào)用getWindow().superDispatchTouchEvent(ev)血淌,根據(jù)前面的分析也就是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調(diào)用的是DecorView的superDispatchTouchEvent方法矩欠,前面我們知道DecorView其實是ViewGroup(上述generateLayout(mDecor)返回值),到此我們可以串聯(lián)起來悠夯,Activity的setContentView其實是Window對象的實現(xiàn)是其唯一實現(xiàn)類PhoneWindown的內(nèi)部類DecorView來作為Activity的根View癌淮,也就是說從Activity開始傳遞的是從PhoneWindow開始,也就是源碼中的installDecor得到的DecorView充當(dāng)了Activity傳遞事件的View沦补,DecorView可以理解為當(dāng)前頁面的底層容器乳蓄,底層容器DecorView在根據(jù)自己是ViewGroup把事件再向他的子View傳遞,也就是我們平時寫的界面最上層View策彤,也就是setContentView加載的布局根布局View栓袖,下圖結(jié)合實例很清晰的可以表示出Activity的構(gòu)成。
- 到此我們可以寫出一個事件傳遞的流程為
Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View
- 總結(jié)一下每個傳遞者具有的方法店诗,我們注意到Activity沒有onInterceptTouchEvent方法裹刮,其實很容易理解,Activity作為事件的初始傳遞者如果攔截了事件庞瘸,也就是我們點擊界面無響應(yīng)捧弃,這也就使得我們用戶的點擊沒什么意義,肯定是我們點擊界面中的某個view響應(yīng)才符合操作擦囊。(PhoneWindow在Android都是隱藏的违霞,不做記錄)
類型 相關(guān)方法 Activity ViewGroup View 事件分發(fā) dispatchTouchEvent 有 有 有 事件攔截 onInterceptTouchEvent 無 有 無 事件消費 onTouchEvent 有 有 有 -
點擊事件分發(fā)原則
- onInterceptTouchEvent攔截事件,該View的onTouchEvent方法才會被調(diào)用瞬场,只有onTouchEvent返回true才表示該事件被消費买鸽,否則回傳到上層View的onTouchEvent方法。
- 如果事件一直不被消費贯被,則最終回傳給Activity眼五,Activity不消費則事件消失。
- 事件是否被消費是根據(jù)返回值彤灶,true表示消費看幼,false表示不消費。
-
-
從源碼角度繼續(xù)分析ViewGroup和View事件傳遞流程
經(jīng)過前面的研究幌陕,我們回顧一下,一個點擊事件用MotionEvent表示诵姜,事件最先傳遞到Activity,調(diào)用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事件,則調(diào)用resetTouchState()方法微饥,resetTouchState()鐘調(diào)用了clearTouchTargets()使mFirstTouchTarget=null逗扒,而前面我們了解事件的時候也說過一個事件是ACTION_DOWN開始到ACTION_UP結(jié)束,也就是說ACTION_DOWN出現(xiàn)表示一個新的事件的開始欠橘;接下來再次判斷為ACTION_DOWN和mFirstTouchTarget矩肩!=null,我們看到條件成立之后才能調(diào)用onInterceptTouchEvent方法肃续,也就是說mFirstTouchTarget黍檩!=null成立說明此時不攔截事件,而mFirstTouchTarget==null成立則說明事件已經(jīng)被攔截始锚,并且不會再有ACTION_DOWN刽酱,因為此時這個一個事件還沒結(jié)束,此時不管ACTION_MOVE還是ACTION_UP動作瞧捌,都交由現(xiàn)在攔截了事件的ViewGroup來處理棵里,并且不會再次調(diào)用onInterceptTouchEvent方法(說明該方法并不是每次都會調(diào)用的)。
我們還看到一個標(biāo)記位FLAG_DISALLOW_INTERCEPT姐呐,它一般是由子View的requestDisallowInterceptTouchEvent方法設(shè)置的殿怜,表示ViewGroup無法攔截除了ACTION_DOWN以外的其他動作,我們看到源碼第一個判斷就會明白曙砂,只要是ACTION_DOWN動作头谜,這個標(biāo)記位都會被重置,并且ViewGroup會調(diào)用自己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坐標(biāo)和Y坐標(biāo)判斷是否和當(dāng)前子View的坐標(biāo)相匹配,而dispatchTransformedTouchEvent方法實際上就是調(diào)用子View的dispatchTouchEvent方法侧纯,這樣就完成了ViewGroup到子View的事件分發(fā)新锈。
- ViewGroup默認(rèn)不攔截任何事件,他的onInterceptTouchEvent方法默認(rèn)返回false
public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.isFromSource(InputDevice.SOURCE_MOUSE) //鼠標(biāo)點擊處理 && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { return true; } return false; }
-
- 如下源碼眶熬,如果ViewGroup將事件傳遞到子View妹笆,則會調(diào)用addTouchTarget(child, idBitsToAssign)方法块请,并退出遍歷子View的循環(huán)
```
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;
}
```
- 如上源碼,調(diào)用addTouchTarget方法拳缠,會給mFirstTouchTarget賦值墩新,也就是說mFirstTouchTarget!=null窟坐,前面我們已經(jīng)討論過海渊,mFirstTouchTarget==null則攔截所有的事件給該ViewGroup處理,可見mFirstTouchTarget是否賦值對于ViewGroup的事件攔截起了關(guān)鍵的作用哲鸳。
- 接著往下看臣疑,如果子View遍歷結(jié)束后事件還是沒有進行處理,這樣的情況有兩種可能徙菠,一個就是上面提到的例子場景二讯沈,ViewGroup的子View沒有消費事件,也就是子View的onTouchEvent返回了false婿奔,另一個情況則是則是ViewGroup子View国觉,也就不存在事件傳遞子View的情況。我們看如下代碼奕锌,是在上面分析的代碼之后出現(xiàn)掌动,第三個參數(shù)子View為null,也就是調(diào)用super.dispatchTouchEvent(event)方法记餐,ViewGroup是繼承View驮樊,也就是說不管是否攔截,ViewGropu最終還是將點擊事件交由到View來處理了片酝,只是child.dispatchTouchEvent還是super.dispatchTouchEvent的問題囚衔。 ViewGroup的源碼事件分發(fā)就到這里,接下來我們分析一下View的事件分發(fā)流程雕沿。
```
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的事件分發(fā)流程
- 首先我們看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對于點擊的事件的處理首先是判斷是注冊O(shè)nTouchListener,并且如果OnTouchListener的onTouch放回true审轮,則整個dispatchTouchEvent返回true肥哎,已經(jīng)攔截了事件,則不會執(zhí)行下面的onTouchEvent方法的調(diào)用疾渣,也就是說事件攔截了篡诽,但是不調(diào)用onTouchEvent方法,這里其實很好理解榴捡,如果開發(fā)者注冊了OnTouchListener并在onTouch放回true杈女,說明開發(fā)者是想自己來處理觸摸事件,而onTouchEvent是屬于Android的事件傳遞機制方法,是系統(tǒng)幫我們處理的达椰,所以當(dāng)我們自己處理了點擊事件翰蠢,就不需要系統(tǒng)來再次處理了。所以O(shè)nTouchListener的調(diào)用有先級高于onTouchEvent啰劲。
- 如果Ciew沒有注冊O(shè)nTouchListener方法梁沧,接下來事件傳遞到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即使是處于不可用狀態(tài),他還是會消費(consumes)點擊事件猖辫,只是他不會響應(yīng)點擊事件酥泞,也就是返回各種點擊的狀態(tài)(點擊,長按)啃憎。
- 接著看看剩下源碼對點擊事件的處理
```
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;
}
```
- 觸摸事件結(jié)束芝囤,也就是ACTION_UP,所以這里我們看對于ACTION_UP的處理就可以了辛萍。我們看到對于ACTION_UP悯姊,,如果沒有!clickable贩毕,也就是沒有View的CLICKABLE悯许、LONG_CLICKABLE和CONTEXT_CLICKABLE都不存在,則清除所有的狀態(tài)回調(diào)等辉阶,如果其中一個存在先壕,則直接消費這個時間,我們看到方法后面有個retrun true存在谆甜,也證實事件被消費了垃僚,也就是onTouchEvent方法返回了true。而如果ACTION_UP沒有消費事件规辱,最終onTouchEvent方法是返回false谆棺。
- 到這里,我們還看到ACTION_UP事件會觸發(fā)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;
}
```
- 可以從源碼看到他最終調(diào)用的是performClick()方法改淑,如果View設(shè)置了OnClickListener,則會調(diào)用onClick方法浴讯。我們知道View默認(rèn)的LONG_CLICKABLE是false朵夏,而CLICKABLE需要根據(jù)具體View才能知道,比如Button是可點擊的榆纽,則CLICKABLE為true侍郭,而ImageView默認(rèn)是不可點擊的询吴,所以CLICKABLE為false,但是開發(fā)中我們也發(fā)現(xiàn)亮元,不管View是否可以點擊,只要我們設(shè)置setOnClickListener()或者setOnLongClickListener()方法唠摹,則該View就是可以被點擊或者長按的爆捞,也就是LONG_CLICKABLE或者CLICKABLE為true。我們從源碼可以看出勾拉。到此煮甥,從源碼角度分析事件分發(fā)機制的流程我們已經(jīng)走完。
```
/**
* 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;
}
```
- 經(jīng)過前面的分析藕赞,我們還可以排出與View相關(guān)的事件調(diào)度優(yōu)先順序為onTouchListener>onTouchEvent > onLongClickListener > onClickListener
最后成肘,總結(jié)事件分發(fā)機制的核心知識點
- 正常情況下觸摸一次屏幕觸發(fā)事件序列為ACTION_DOWN-->ACTION_UP
- 當(dāng)一個View決定攔截,那么這一個事件序列只能由這個View來處理斧蜕,onInterceptTouchEvent方法并不是每次產(chǎn)生動作都會被調(diào)用到双霍,當(dāng)我們需要提前出來想要攔截的動作需要在事件必須傳遞到該ViewGroup的前提下在dispatchTouchEvent方法中進程操作。
- 一個View開始處理事件批销,但是它不消耗ACTION_DOWN洒闸,也就是onTouchEvent返回false,則這個事件會交由他的父元素的onTouchEvent方法來進行處理均芽,而這個事件序列的其他剩余ACACTION_MOVE丘逸,ACTION_UP也不會再給該View來處理。
- View沒有onInterceptTouchEvent方法掀宋,View一旦接收到事件就調(diào)用onTouchEvent方法
- ViewGroup默認(rèn)不攔截任何事件(onInterceptTouchEvent方法默認(rèn)返回false)深纲。
- View的onTouchEvent方法默認(rèn)是處理點擊事件的,除非他是不可點擊的(clickable和longClickable同時為false)
- 事件分發(fā)機制的核心原理就是責(zé)任鏈模式劲妙,事件層層傳遞湃鹊,直到被消費。
終于是趴,把事件分發(fā)機制給回顧了一遍涛舍,其實五月份的時候我就復(fù)習(xí)過一次事件分發(fā)機制,但是當(dāng)時沒有記錄唆途,所以這次在回頭看以前記得有些知識點感覺還是模糊富雅,所以記錄下來才能在以后忘記的時候去回顧再總結(jié)。如果文章中有寫得不對的地方肛搬,請給我留言指出没佑,大家一起學(xué)習(xí)進步。如果覺得我的文章給予你幫助温赔,也請給我一個喜歡和關(guān)注蛤奢。
-
參考鏈接
-
參考書籍
- 《Android開發(fā)藝術(shù)探索》
- 《Android進階之光》