關鍵類 | 路徑 |
---|---|
Activity.java | frameworks/base/core/java/android/app/Activity.java |
DecorView.java | frameworks/base/core/java/com/android/internal/policy/DecorView.java |
PhoneWindow.java | frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java |
View.java | frameworks/base/core/java/android/view/View.java |
ViewGroup.java | frameworks/base/core/core/java/android/view/ViewGroup.java |
Window.java | frameworks/base/core/java/android/view/Window.java |
Android 「View」 雖然不是四大組件,但其並不比四大組件的地位低。而 View 的核心知識點 「事件分發機制」 則是很多剛入門童鞋的攔路虎(一、項目中到處遇到事件分發機制;二、面試管最喜歡說起的問題)。java
在實際項目的開發過程當中,ScrollView 嵌套 RecyclerView(或者 ListView)的滑動衝突這種老大難問題的理論基礎就是「事件分發機制」。android
首先,咱們瞭解一下「事件分發機制」中的基礎知識點!面試
事件分發的對象:點擊事件(Touch事件)app
1. 定義ide
當用戶觸摸屏幕時(View 或 ViewGroup 派生的控件),將產生點擊事件(Touch事件)函數
Touch 事件的相關細節(發生觸摸的位置、時間等)被封裝成 MotionEvent 對象。
2. 事件類型佈局
事件類型 | 具體動做 |
---|---|
MotionEvent.ACTION_DOWN | 按下 View(全部事件的開始) |
MotionEvent.ACTION_UP | 擡起 View(與DOWN對應) |
MotionEvent.ACTION_MOVE | 滑動 View |
MotionEvent.ACTION_CANCEL | 結束事件(非人爲緣由) |
3. 事件序列post
所謂事件序列就是指:從手指接觸屏幕至手指離開屏幕,這個過程產生的一系列事件。測試
通常狀況下,事件列都是以 DOWN 事件開始、UP 事件結束,中間有無數的 MOVE 事件。ui
以下圖:
即當一個點擊事件(MotionEvent )產生後,系統需把這個事件傳遞給一個具體的 View 去處理。
本質: 將點擊事件(MotionEvent)傳遞到某個具體的 View 並處理的整個過程。
即:事件傳遞的過程 = 分發過程
對象: Activity、ViewGroup、View
Android 的 UI 界面由 Activity、ViewGroup、View 及其派生類組成。
以下圖:
順序: Activity -> ViewGroup -> View
即:1個點擊事件發生後,事件先傳到 Activity、再傳到 ViewGroup、最終再傳到 View。
事件分發的順序流程圖以下:
三個重要方法: dispatchTouchEvent() 、onInterceptTouchEvent() 和 onTouchEvent()。
dispatchTouchEvent(事件分發):當監聽到有觸發事件時,首先由 Activity 進行捕獲,而後事件就進入事件分發的流程。Activity 自己沒有事件攔截,從而將事件傳遞給最外層的 View 的 dispatchTouchEvent(MotionEvent ev),該方法將對事件進行分發。
返回值:表示是否消費了當前事件。多是 View 自己的 onTouchEvent() 消費,也多是子 View 的 dispatchTouchEvent() 中消費。返回 true 表示事件被消費,本次的事件終止。返回 false 表示 View 以及子 View 均沒有消費事件,將調用父 View 的 onTouchEvent()。
onInterceptTouchEvent(事件攔截):當一個 ViewGroup 在接到 MotionEvent 事件序列時候,首先會調用此方法判斷是否須要攔截。特別注意,這是 ViewGroup 特有的方法,View 並無攔截方法。
返回值:是否攔截事件傳遞,返回 true 表示攔截了事件,那麼事件將再也不向下分發而是調用 View 自己的 onTouchEvent()。返回 false 表示不作攔截,事件將向下分發到子 View 的 dispatchTouchEvent()。
onTouchEvent(事件響應):真正對 MotionEvent 進行處理或者說消費的方法,在 dispatchTouchEvent() 中進行調用。
返回值:返回 true 表示事件被消費,本次的事件終止。返回 false 表示事件沒有被消費,將調用父 View 的 onTouchEvent 方法,直到返回 true 爲止。
咱們能夠作個總結:
Activity 的點擊事件事實上是調用它內部的 ViewGroup 的點擊事件,能夠直接當成 ViewGroup 處理。
ViewGroup 的相關事件方法有三個:onInterceptTouchEvent
、dispatchTouchEvent
、onTouchEvent
。
View 的相關事件方法只有兩個:dispatchTouchEvent
、onTouchEvent
。
要想充分理解 Android 事件分發機制,本質上是要理解如下三個部分:
✯ Activity - Touch 事件分發 ✯ ViewGroup - Touch 事件分發 ✯ View - Touch 事件分發
當一個點擊事件發生時,事件最早傳到 Activity 的 dispatchTouchEvent() ,進行事件分發。
public boolean dispatchTouchEvent(MotionEvent ev) { // 通常事件列開始都是 DOWN 事件,即按下事件,因此此處基本是 true if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); // 1 } if (getWindow().superDispatchTouchEvent(ev)) { // 2 return true; } return onTouchEvent(ev); // 3 }
Activity 的 dispatchTouchEvent 方法很簡單,主要涉及三個方法:onUserInteraction
、superDispatchTouchEvent
、onTouchEvent
。
/** * 說明: * a. 該方法爲空方法,主要實現屏保功能 * b. 當此 activity 在棧頂時,觸屏點擊按 home,back,menu 鍵等都會觸發此方法 */ public void onUserInteraction() { }
/** * getWindow():獲取 Window 類的對象,Window 類是抽象類, * 其惟一實現類是 PhoneWindow 類;即此處的 Window 類對象 = PhoneWindow 類對象。 */ if (getWindow().superDispatchTouchEvent(ev)) { return true; }
Window 類的 superDispatchTouchEvent() 是一個抽象方法,由子類 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) { return mDecor.superDispatchTouchEvent(event); } ... ... }
咱們發現,Activity 的 superDispatchTouchEvent 方法最終會走到 DecorView 的 superDispatchTouchEvent 方法。
/** * public class DecorView extends FrameLayout, * -- DecorView 繼承自 FrameLayout,是全部界面的父類。 * public class FrameLayout extends ViewGroup, -- FrameLayout 是 ViewGroup 的子類,故 DecorView 的間接父類是 ViewGroup。 */ public boolean superDispatchTouchEvent(MotionEvent event) { // 調用父類的方法:ViewGroup 的 dispatchTouchEvent(), // 即:將事件傳遞到 ViewGroup 去處理,咱們在 ViewGroup 的事件分發機制繼續討論。 return super.dispatchTouchEvent(event); }
因此不難發現,Activity 的 dispatchTouchEvent 方法最終會走到 ViewGroup 的 dispatchTouchEvent 方法。
public boolean onTouchEvent(MotionEvent event) { // 通常返回 true,除非 Touch 事件在 Window 邊界外,因此這邊咱們再也不繼續跟蹤。 if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }
至此,咱們分析了 Activity 對點擊事件的分發機制處理流程,咱們不難發現,Activity 的事件走到了 ViewGroup 進行處理,那麼接下來就是分析 ViewGroup 對點擊事件的分發機制了。
上面咱們說過,Activity 的 dispatchTouchEvent 方法最終會走到 ViewGroup 的 dispatchTouchEvent 方法。
ViewGroup 對點擊事件的分發流程就複雜不少了,咱們來細細研究:
// First touch target in the linked list of touch targets. private TouchTarget mFirstTouchTarget; public boolean dispatchTouchEvent(MotionEvent ev) { ... ... // 這個變量用於記錄事件是否被處理完 boolean handled = false; // 過濾掉一些不合法的事件 if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 判斷是否是 Down 事件,若是是的話,就要作初始化操做 if (actionMasked == MotionEvent.ACTION_DOWN) { // 若是是 Down 事件,就要清空掉以前的狀態, cancelAndClearTouchTargets(ev); resetTouchState(); } // 是否攔截事件 final boolean intercepted; // 若是當前是 Down 事件,或者已經有處理 Touch 事件的目標了 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { /** * disallowIntercept:是否禁用事件攔截的功能,默認爲 false, * 咱們也能夠經過調用 requestDisallowInterceptTouchEvent 方法 * 對這個值進行修改。 */ final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // disallowIntercept 默認爲 false 因此會走如下流程 if (!disallowIntercept) { // 每次事件分發時,都需調用 onInterceptTouchEvent() 詢問是否攔截事件 intercepted = onInterceptTouchEvent(ev); // 從新恢復 Action,以避免 Action 在上面的步驟被人爲地改變了 ev.setAction(action); // restore action in case it was changed } else { // 若是禁用了事件攔截功能,則 intercepted 確定爲 false intercepted = false; } } else { // 若是說,事件已經初始化過了,而且沒有子 View 被分配處理, // 那麼就說明,這個 ViewGroup 已經攔截了這個事件。 intercepted = true; } if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } // Check for cancelation,標誌着取消事件. final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // 若是須要(不取消,也沒有被攔截),那麼在觸摸 Down 事件的時候更新觸摸目標列表 // split:表明當前的 ViewGroup 是否是支持分割 MotionEvent 到不一樣的 View 當中 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; // 新的觸摸對象 TouchTarget newTouchTarget = null; //是否把事件分配給了新的觸摸 boolean alreadyDispatchedToNewTouchTarget = false; ★ ★ ★ ★ ★ ★ ★ ★ 重點方法 ★ ★ ★ ★ ★ ★ ★ ★ // 若是事件不是取消事件,也沒有攔截,那麼進入此函數 if (!canceled && !intercepted) { View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; /* * 若是是個全新的 Down 事件,或者是有新的觸摸點,或者是光標來回移動事件 */ if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { // 事件的索引,Down 事件的 index:0 final int actionIndex = ev.getActionIndex(); // always 0 for down // 獲取分配的 ID 的 bit 數量 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // 清理以前觸摸這個指針標識,以防它們的目標變得不一樣步 removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; // 若是新的觸摸對象爲 null & 當前 ViewGroup 有子元素 if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; // 經過 for 循環,遍歷了當前 ViewGroup 下的全部子 View for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } ... ... // 可有可無的代碼咱們這邊暫且省略 // 派發事件到子 View 處理 if (dispatchTransformedTouchEvent(ev, ...)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { 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; // 若是子 View 處理了事件,則 break, // ViewGroup 不在處理事件(事件被攔截)。 break; } ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } ... ... } } ... ... } return handled; }
其實 ViewGroup 的 dispatchTouchEvent 處理流程,咱們只須要關注兩個重點方法:onInterceptTouchEvent
和 dispatchTransformedTouchEvent
。
public boolean onInterceptTouchEvent(MotionEvent ev) { // 一堆判斷,咱們只須要知道一點,通常 Touch 事件默認返回 false 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; }
dispatchTransformedTouchEvent 方法的做用,主要就是把事件下發給子 View 進行處理。
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; ... ... final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { ... ... // dispatchTouchEvent() handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); } // Perform any necessary transformations and dispatch. if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { ... ... // dispatchTouchEvent() handled = child.dispatchTouchEvent(transformedEvent); } ... ... // Done. transformedEvent.recycle(); return handled; }
你有沒有發現?當存在子 View 的時候,會調用 View.dispatchTouchEvent 方法,若是沒有則會向上調用 super.dispatchTouchEvent 方法。
Layout 層次:
Layout 代碼:
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/my_layout" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/button_01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button_01" tools:layout_editor_absoluteX="94dp" tools:layout_editor_absoluteY="106dp" /> <Button android:id="@+id/button_02" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button_02" tools:layout_editor_absoluteX="94dp" tools:layout_editor_absoluteY="211dp" /> </android.support.constraint.ConstraintLayout>
Activity 代碼:
package com.example.marco.myapplication; import android.support.constraint.ConstraintLayout; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.Button; public class MainActivity extends AppCompatActivity { private Button button_01; private Button button_02; private ViewGroup myLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button_01 = (Button) findViewById(R.id.button_01); button_02 = (Button) findViewById(R.id.button_02); myLayout = (ConstraintLayout) findViewById(R.id.my_layout); // 1. ViewGroup: myLayout 佈局設置監聽事件 myLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "點擊了ViewGroup"); } }); // 2. View: button_01 設置監聽事件 button_01.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "點擊了button_01"); } }); // 3. View: button_02設置監聽事件 button_02.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "點擊了button_02"); } }); } }
測試結果:
09-26 16:17:51.877 16250 16250 D TAG : 點擊了button_01 // 點擊按鈕 button_01 09-26 16:17:53.875 16250 16250 D TAG : 點擊了button_02 // 點擊按鈕 button_02 09-26 16:17:54.758 16250 16250 D TAG : 點擊了ViewGroup // 點擊空白處
結果說明:
結論:Button 的 onClick() 將事件消費掉了,所以事件不會再繼續向下傳遞。
這邊咱們先作個說明:
其實,只要你觸摸了任何控件,就必定會調用該控件的 dispatchTouchEvent
方法!
嚴格一點來講:當你點擊了某個控件,首先會去調用該控件所在佈局
的 dispatchTouchEvent
方法,而後在佈局
的 dispatchTouchEvent
方法中找到被點擊的相應控件
,再去調用該控件
的 dispatchTouchEvent
方法。
public boolean dispatchTouchEvent(MotionEvent event) { ... ... if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; ★ ★ ★ ★ ★ ★ ★ ★ 重點方法 ★ ★ ★ ★ ★ ★ ★ ★ /* * 注意:只有如下3個條件都爲真,dispatchTouchEvent() 才返回 true * 1. li != null & li.mOnTouchListener != null * 2. (mViewFlags & ENABLED_MASK) == ENABLED * 3. mListenerInfo.mOnTouchListener.onTouch(this, event) */ if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } ★ ★ ★ ★ ★ ★ ★ ★ 分別分析以上幾個條件 ★ ★ ★ ★ ★ ★ ★ ★ // 若是上面沒有返回 true,那麼執行 onTouchEvent() if (!result && onTouchEvent(event)) { // 下面再分析 result = true; } } ... ... return result; }
條件 1:li != null & li.mOnTouchListener != null
mOnTouchListener 這個變量是在哪裏賦值的呢?
/** * 條件 1:mListenerInfo.mOnTouchListener != null * 說明:mOnTouchListener 變量在 View.setOnTouchListener() 方法裏賦值 */ public void setOnTouchListener(OnTouchListener l) { // 只要咱們給控件註冊了 Touch 事件,mOnTouchListener 就必定被賦值(不爲空) getListenerInfo().mOnTouchListener = l; }
條件 2:(mViewFlags & ENABLED_MASK) == ENABLED
/** * 條件 2:(mViewFlags & ENABLED_MASK) == ENABLED * 說明: * a. 該條件是判斷當前點擊的控件是否 enable * b. 通常 View 默認 enable,故該條件恆定爲 true */
條件 3:mListenerInfo.mOnTouchListener.onTouch(this, event)
mListenerInfo.mOnTouchListener.onTouch(this, event),其實也就是去回調控件註冊 touch 事件時的 onTouch 方法。
也就是說若是咱們在 onTouch 方法裏返回 true,就會讓這三個條件所有成立,從而整個方法直接返回 true。若是咱們在 onTouch 方法裏返回 false,就會再去執行 onTouchEvent(event) 方法。
/** * 條件 3:mOnTouchListener.onTouch(this, event) * 說明:回調控件註冊 Touch 事件時的 onTouch() */ button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return false; } });
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; ... ... // 若該控件可點擊,則進入 switch 判斷中 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { // a. 若當前的事件 = 擡起 View case MotionEvent.ACTION_UP: ... ... 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(); if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); // 重點分析函數 } } } ... } break; // b. 若當前的事件 = 按下 View case MotionEvent.ACTION_DOWN: ... ... break; // c. 若當前的事件 = 結束事件(非人爲緣由) case MotionEvent.ACTION_CANCEL: ... ... break; // d. 若當前的事件 = 滑動 View case MotionEvent.ACTION_MOVE: ... ... break; } // 若該控件可點擊,就必定返回true return true; } // 若該控件不可點擊,就必定返回false return false; }
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; /* * 只要咱們經過 setOnClickListener() 爲控件 View 註冊1個點擊事件, * 那麼就會給 li.mOnClickListener 變量賦值(即不爲空), * 則會往下回調 onClick(),performClick() 返回 true。 */ 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; }
Layout代碼:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/my_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <Button android:id="@+id/button_01" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button_01" tools:layout_editor_absoluteX="94dp" tools:layout_editor_absoluteY="106dp" /> </LinearLayout>
Activity代碼:
package com.example.marco.myapplication; import android.support.constraint.ConstraintLayout; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.LinearLayout; public class MainActivity extends AppCompatActivity { private Button button_01; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); button_01 = (Button) findViewById(R.id.button_01); /** * 結論驗證1:在回調 onTouch() 裏返回 false */ // 1. 經過 OnTouchListener() 複寫 onTouch(),從而手動設置返回 false button_01.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d("TAG", "run onTouch(), action:" + event.getAction()); return false; } }); // 2. 經過 OnClickListener()爲控件設置點擊事件, // 爲 mOnClickListener 變量賦值(即不爲空),從而往下回調 onClick() button_01.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "run onClick()"); } }); /** * 結論驗證2:在回調 onTouch() 裏返回 true */ // 1. 經過 OnTouchListener()複寫 onTouch(),從而手動設置返回 true button_01.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { Log.d("TAG", "run onTouch(), action:" + event.getAction()); return true; } }); // 2. 經過 OnClickListener()爲控件設置點擊事件, // 爲 mOnClickListener 變量賦值(即不爲空),從而往下回調 onClick() button_01.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d("TAG", "run onClick()"); } }); } }
測試結果:
// 經過 OnTouchListener() 複寫 onTouch(),從而手動設置返回 false 09-26 18:14:19.299 23350 23350 D TAG : run onTouch(), action:0 // ACTION_DOWN 09-26 18:14:19.327 23350 23350 D TAG : run onTouch(), action:2 // ACTION_MOVE 09-26 18:14:19.343 23350 23350 D TAG : run onTouch(), action:2 // ACTION_MOVE 09-26 18:14:19.383 23350 23350 D TAG : run onTouch(), action:2 // ACTION_MOVE 09-26 18:14:19.384 23350 23350 D TAG : run onTouch(), action:1 // ACTION_UP 09-26 18:14:19.385 23350 23350 D TAG : run onClick()
// 經過 OnTouchListener() 複寫 onTouch(),從而手動設置返回 true 09-26 18:16:29.758 23847 23847 D TAG : run onTouch(), action:0 // ACTION_DOWN 09-26 18:16:29.773 23847 23847 D TAG : run onTouch(), action:2 // ACTION_MOVE 09-26 18:16:29.856 23847 23847 D TAG : run onTouch(), action:2 // ACTION_MOVE 09-26 18:16:29.858 23847 23847 D TAG : run onTouch(), action:1 // ACTION_UP
✒ 一、Android 事件分發機制主要由【「事件分發」 —> 「事件攔截」 —> 「事件響應」】這三步來進行邏輯控制的。
✒ 二、ViewGroup 默認不攔截任何事件。
✒ 三、onInterceptTouchEvent 返回 true 表示事件攔截,onTouchEvent 返回 true 表示事件消費。
✒ 四、點擊事件的分發過程以下:dispatchTouchEvent —> onTouchListener 的 OnTouch 方法 —> onTouchEvent —> onClickListener 的 onClick 方法。從而也能夠看出 onTouch 是優先於 onClick 執行,事件傳遞的順序是先通過 onTouch,再傳遞到 onClick。
✒ 五、Android 中的事件 onClick、onLongClick、onScroll 等,都是由多個 Touch 事件(一個 ACTION_DOWN,多個 ACTION_MOVE,一個 ACTION_UP)組成。
✒ 六、子 View 能夠經過使用 getParent().requestDisallowInterceptTouchEvent(true) ,阻止 ViewGroup 對其 MOVE 或 UP 事件進行攔截。
✒ 七、MotionEvent 對象的四種狀態:MotionEvent.ACTION_DOWN:手指按下屏幕的瞬間
MotionEvent.ACTION_MOVE:手指在屏幕上移動
MotionEvent.ACTION_UP:手指離開屏幕瞬間
MotionEvent.ACTION_CANCEL:取消手勢
✒ 八、點擊某個控件,首先會去調用該控件所在佈局的 dispatchTouchEvent方法,而後在佈局的 dispatchTouchEvent 方法中找到被點擊的相應控件,再去調用該控件的 dispatchTouchEvent方法。
✒ 九、若是 View 沒有消費 ACTION_DOWN 事件,則以後的 ACTION_MOVE 等事件都不會再接收。
✒ 十、事件在從 Activity.dispatchTouchEvent 往下分發的過程當中:
若是中間的 ViewGroup 都不攔截,進入最底層的 View 後,由 View.onTouchEvent 處理,若是 View 也沒有消費事件,最後會返回到 Activity.onTouchEvent。 若是中間任何一層 ViewGroup 攔截事件,則事件再也不往下分發,交由攔截的 ViewGroup 的 onTouchEvent 來處理。