View 體系詳解:座標系、滑動、手勢和事件分發機制
下面是 Android 中的 View 座標系的基本圖。要獲得一個 View 的位置,我們可以藉助兩個物件,一個是 View ,一個是 MotionEvent。以下是它們的一些方法的位置的含義:

在 View 中共有 mLeft
, mRight
, mTop
和 mBottom
四個變數包含 View 的座標資訊,你可以在原始碼中獲取它們的含義:
mLeft mRight mTop mBottom
此外,View 中還有幾個方法用來獲取控制元件的位置等資訊,實際上就是上面四個變數的 getter 方法:
-
getLeft()
:即mLeft
; -
getRight()
:即mRight
; -
getTop()
:即mTop
; -
getBottom()
:即mBottom
;
所以,我們可以得到兩個獲取 View 高度和寬度資訊的方法:
-
getHeight()
:即mBottom - mTop
; -
getWidth()
:即mRight - mLeft
;
另外,就是 View 中的 getX()
和 getY()
兩個方法,你需要注意將其與 MotionEvent 中的同名方法進行區分。在沒有對控制元件進行平移的時候, getX()
與 getLeft()
返回結果相同,只是前者會在後者的基礎上加上平移的距離:
-
getX()
:即mLeft + getTranslationX()
,即控制元件的左邊緣加上 X 方向平移的距離; -
getY()
:即mTop + getTranslationY()
,即控制元件的上邊緣加上 Y 方向平移的距離;
以上是我們對 View 中獲取控制元件位置的方法的梳理,你可以到原始碼中檢視它們更加相詳盡的定義,那更有助於自己的理解。
1.2 MotionEvent
通常當你對控制元件進行觸控監聽的時候會用到 MotionEvent ,它封住了觸控的位置等資訊。下面我們對 MotionEvent 中的獲取點選事件的位置的方法進行梳理,它主要涉及下面四個方法:
MotionEvent.getX() MotionEvent.getY() MotionEvent.getRawX() MotionEvent.getRawY()
另外是觸控事件中的三種典型的行為,按下、移動和抬起。接下來的程式碼示例中我們會用到它們來判斷手指的行為,並對其做響應的處理:
MotionEvent.ACTION_DOWN MotionEvent.ACTION_MOVE MotionEvent.ACTION_UP
2、滑動
我們有幾種方式實現 View 的滑動:
2.1 layout() 方法
呼叫控制元件的 layout()
方法進行滑動,下面是該方法的定義:
public void layout(int l, int t, int r, int b) { /*...*/ } 複製程式碼
其中的四個引數 l
, t
, r
, b
分別表示控制元件相對於父控制元件的左、上、右、下的距離,分別對應於上面的 mLeft
, mTop
, mRight
和 mBottom
。所以,呼叫該方法同時可以改變控制元件的高度和寬度,但有時候我們不需要改變控制元件的高度和寬度,只要移動其位置即可。所以,我們又有方法 offsetLeftAndRight()
和 offsetTopAndBottom()
可以使用,後者只會對控制元件的位置進行平移。因此,我們可以進行如下的程式碼測試:
private int lastX, lastY; private void layoutMove(MotionEvent event) { int x = (int) event.getX(), y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: int offsetX = x - lastX, offsetY = y - lastY; getBinding().v.layout(getBinding().v.getLeft() + offsetX, getBinding().v.getTop() + offsetY, getBinding().v.getRight() + offsetX, getBinding().v.getBottom() + offsetY); break; case MotionEvent.ACTION_UP: break; } } 複製程式碼
上面的程式碼的效果是指定的控制元件會隨著手指的移動而移動。這裡我們先記錄下按下的位置,然後手指移動的時候記錄下平移的位置,最後呼叫 layout()
即可。
2.2 offsetLeftAndRight() 和 offsetTopAndBottom()
上面已經提到過這兩個方法,它們只改變控制元件的位置,無法改變大小。我們只需要對上述程式碼做少量修改就可以實現同樣的效果:
getBinding().v.offsetLeftAndRight(offsetX); getBinding().v.offsetTopAndBottom(offsetY); 複製程式碼
2.3 改變佈局引數
通過獲取並修改控制元件的 LayoutParams
,我們一樣可以達到修改控制元件的位置的目的。畢竟,本身這個物件就代表著控制元件的佈局:
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getBinding().v.getLayoutParams(); lp.leftMargin = getBinding().v.getLeft() + offsetX; lp.topMargin = getBinding().v.getTop() + offsetY; getBinding().v.setLayoutParams(lp); 複製程式碼
2.4 動畫
使用動畫我們也可以實現控制元件移動的效果,這裡所謂的動畫主要是操作 View 的 transitionX
和 transitionY
屬性:
getBinding().v.animate().translationX(5f); getBinding().v.animate().translationY(5f); 複製程式碼
關於動畫的內容,我們會在後面詳細介紹。
2.5 scrollTo() 和 scrollBy()
scrollBy()
方法內部呼叫了 scrollTo()
,以下是這部分的原始碼。 scrollBy()
表示在當前的位置上面進行平移,而 scrollTo()
表示平移到指定的位置:
public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); } 複製程式碼
同樣對上述程式碼進行修改,我們也可以實現之前的效果:
((View) getBinding().v.getParent()).scrollBy(-offsetX, -offsetY); 複製程式碼
或者
View parent = ((View) getBinding().v.getParent()); parent.scrollTo(parent.getScrollX()-offsetX, parent.getScrollY()-offsetY); 複製程式碼
此外,還有一個需要注意的地方是:與上面的 offsetLeftAndRight()
和 offsetTopAndBottom()
不同的是,這裡我們用了平移的值的相反數。原因很簡單,因為我們要使用這兩個方法的時候需要對指定的控制元件所在的父容器進行呼叫(正如上面是先獲取父控制元件)。當我們希望控制元件相對於之前的位置向右下方向移動,就應該讓父容器相對於之前的位置向左上方向移動。因為實際上該控制元件相對於父控制元件的位置沒有發生變化,變化的是父控制元件的位置。(參考的座標系不同)
2.6 Scroller
上面,我們的測試程式碼是讓指定的控制元件隨著手指移動,但是假如我們希望控制元件從一個位置移動到另一個位置呢?當然,它們也可以實現,但是這幾乎就是在瞬間完成了整個操作,實際的UI效果肯定不會好。所以,為了讓滑動的過程看起來更加流暢,我們可以藉助 Scroller
來實現。
在使用 Scroller
之前,我們需要先例項化一個 Scroller
:
private Scroller scroller = new Scroller(getContext()); 複製程式碼
然後,我們需要覆寫自定義控制元件的 computeScroll()
方法,這個方法會在繪製 View 的時候被呼叫。所以,這裡的含義就是,當 View 重繪的時候會呼叫 computeScroll()
方法,而 computeScroll()
方法會判斷是否需要繼續滾動,如果需要繼續滾動的時候就呼叫 invalidate()
方法,該方法會導致 View 進一步重繪。所以,也就是靠著這種不斷進行重繪的方式實現了滾動的效果。
滑動效果最終結束的判斷是通過 Scroller
的 computeScrollOffset()
方法實現的,當滾動停止的時候,該方法就會返回 false
,這樣不會繼續呼叫 invalidate()
方法,因而也就不會繼續繪製了。下面是該方法典型的覆寫方式:
@Override public void computeScroll() { super.computeScroll(); if (scroller.computeScrollOffset()) { ((View) getParent()).scrollTo(scroller.getCurrX(), scroller.getCurrY()); invalidate(); } } 複製程式碼
然後,我們再加入一個滾動到指定位置的方法,在該方法內部我們使用了 2000ms 來指定完成整個滑動所需要的時間:
public void smoothScrollTo(int descX, int descY) { scroller.startScroll(getScrollX(), getScrollY(), descX - getScrollX(), descY - getScrollY(), 2000); invalidate(); } 複製程式碼
這樣定義了之後,我們只需要在需要滾動的時候呼叫自定義 View 的 smoothScrollTo()
方法即可。
3、手勢
3.1 ViewConfiguration
在類 ViewConfiguration
中定義了一些列的常量用來標誌指定的行為,比如, TouchSlop
就是滑動的最小的距離。你可以通過 ViewConfiguration.get(context)
來獲取 ViewConfiguration
例項,然後通過它的 getter 方法來獲取這些常量的定義。
3.2 VelocityTracker
VelocityTracker
用來檢測手指滑動的速率,它的使用非常簡單。在使用之前,我們先使用它的靜態方法 obtain()
獲取一個例項,然後在 onTouch()
方法中呼叫它的 addMovement(MotionEvent)
方法:
velocityTracker = VelocityTracker.obtain(); 複製程式碼
隨後,當我們想要獲得速率的時候,先呼叫 computeCurrentVelocity(int)
傳入一個時間片段,單位是毫秒,然後呼叫 getXVelocity()
和 getYVelocity()
分別獲得在水平和豎直方向上的速率即可:
velocityTracker.computeCurrentVelocity((int) duration); getBinding().tvVelocity.setText("X:" + velocityTracker.getXVelocity() + "\n" + "Y:" + velocityTracker.getYVelocity()); 複製程式碼
本質上,計算速率的時候是用指定時間的長度變化除以我們傳入的時間片。當我們使用完了 VelocityTracker
之後,需要回收資源:
velocityTracker.clear(); velocityTracker.recycle(); 複製程式碼
3.3 GestureDectector
GestureDectector
用來檢測手指的手勢。在使用它之前我們需要先獲取一個 GestureDetector
的例項:
mGestureDetector = new GestureDetector(getContext(), new MyOnGestureListener()); 複製程式碼
這裡我們用了 GestureDetector
的構造方法,需要傳入一個 OnGestureListener
物件。這裡我們用了 MyOnGestureListener
例項。 MyOnGestureListener
是一個自定義的類,實現了 OnGestureListener
介面:
private class MyOnGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onSingleTapUp(MotionEvent e) { ToastUtils.makeToast("Click detected"); return false; } @Override public void onLongPress(MotionEvent e) { LogUtils.d("Long press detected"); } @Override public boolean onDoubleTap(MotionEvent e) { LogUtils.d("Double tab detected"); return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { LogUtils.d("Fling detected"); return true; } } 複製程式碼
在 MyOnGestureListener
中,我們覆寫了它的一些方法。比如,單擊、雙擊和長按等等,當檢測到相應的手勢的時候這些方法就會被呼叫。
然後,我們可以這樣使用 GestureDetector
,只要在控制元件的觸控事件回撥中呼叫即可:
getBinding().vg.setOnTouchListener((v, event) -> { mGestureDetector.onTouchEvent(event); return true; }); 複製程式碼
4、事件分發機制
4.1 事件傳遞的過程
當討論事件分發機制的時候,我們首先要了解 Android 中 View
的組成結構。在 Android 中,一個 Activity 包含一個 PhoneWindow
,當我們在 Activity 中呼叫 setContentView()
方法的時候,會呼叫該 PhoneWindow
的 setContentView()
方法,並在這個方法中生成一個 DecorView
作為 Activity 的跟 View
。
根據上面的分析,當一個點選事件被觸發的時候,首先接收到該事件的是 Activity
。因為, Activity
覆蓋了整個螢幕,我們需要先讓它接收事件,然後它把事件傳遞給根 View
之後,再由根 View
向下繼續傳遞。這樣不斷縮小搜尋的範圍,直到最頂層的 View
。當然,任何的父容器都可以決定這個事件是不是要繼續向下傳遞,因此,我們可以大致得到下面這個事件傳遞的圖:

左邊的圖是一個 Activity 內部的 View
和 Window
的組織結構。右面的圖可以看作它的切面圖,其中的黑色箭頭表示事件的傳遞過程。這裡事件傳遞的過程是先從下到上,然後再從上到下。也就是從大到小,不斷定位到觸控的控制元件,其中每個父容器可以決定是否將事件傳遞下去。(需要注意的地方是,我們這裡是從下向上的,因為每個父容器只有一個子元素,如果一個父容器有多個子元素的話,那麼在這些子元素中進行遍歷的時候,順序是從上往下的。)
上面我們分析了 Android 事件傳遞的過程,相信你有了一個大致的瞭解。但是,想要了解整個事件傳遞過程具體涉及了哪些方法、如何作用等,還需要我們對原始碼進行分析。
4.2 事件傳遞的原理
當觸控事件發生的時候,首先會被 Activity 接收到,然後該 Activity 會通過其內部的 dispatchTouchEvent(MotionEvent)
將事件傳遞給內部的 PhoneWindow
;接著 PhoneWindow
會把事件交給 DecorView
,再由 DecorView
交給根 ViewGroup
。剩下的事件傳遞就只在 ViewGroup
和 View
之間進行。我們可以通過覆寫 Activity 的 dispatchTouchEvent(MotionEvent)
來阻止把事件傳遞給 PhoneWindow
,也就不會對任何觸控事件進行處理。
實際上,在我們開發的時候不會對 Window
的事件傳遞方法進行重寫,一般是對 ViewGroup
或者 View
。所以,下面我們的分析只在這兩種控制元件之間進行。
當討論 View 的事件分發機制的時候,無外乎 dispatchTouchEvent(MotionEvent)
、 onTouchEvent(MotionEvent)
和 onInterceptTouchEvent(MotionEvent)
三個方法。這裡的 dispatchTouchEvent(MotionEvent)
和 onTouchEvent(MotionEvent)
在 View 中定義,所以是在 View 和 ViewGroup 中都存在的方法。而 onInterceptTouchEvent(MotionEvent)
只定義在 ViewGroup 中。下面是它們在原始碼中的定義:
-
boolean onInterceptTouchEvent(MotionEvent ev)
:用來對事件進行攔截,該方法只存在於 ViewGroup 中。一般我們會通過覆寫該方法來攔截所有的觸控事件,使其不再繼續傳遞給子 View。 -
boolean dispatchTouchEvent(MotionEvent event)
:用來分發觸控事件,一般我們不覆寫該方法。返回true
表示觸控事件被處理了。在 View 中,它只負責處理該 View 內部的邏輯,即根據 View 的狀態決定是否將事件分配給onTouchEvent(MotionEvent)
進行處理;而在 ViewGroup 中,它的責任是對事件進行分發,會對所有的子 View 進行遍歷,決定是否將事件分發給指定的 View。 -
boolean onTouchEvent(MotionEvent event)
:用於處理觸控事件,返回true
表示觸控事件被處理了,在 ViewGroup 中沒有對該方法進行覆寫,所以該方法在 ViewGroup 中與 View 中是一致的。一般,我們會通過覆寫該方法來處理觸控事件,並決定該事件是否繼續傳遞。
所以,根據上面的分析,這三個方法外加兩種 View,一共 5 種情況,而我們需要關注的只有下面4個方法:
ViewGroup.onInterceptTouchEvent(MotionEvent) ViewGroup.dispatchTouchEvent(MotionEvent) View.dispatchTouchEvent(MotionEvent) View.onTouchEvent(MotionEvent)
於是,我們可以得到如下的虛擬碼。這段程式碼是存在於 ViewGroup 中的,也就是事件分發機制的核心程式碼:
boolean dispatchTouchEvent(MotionEvent e) { boolean result; if (onInterceptTouchEvent(e)) { result = onTouchEvent(e); } else { result = child.dispatchTouchEvent(e); // 可看作 result = child.onTouchEvent(e); } return result; } 複製程式碼
按照上述分析,觸控事件經過 Activity 傳遞給根 ViewGroup 之後,會由它來判斷,如果 onInterceptTouchEvent()
被覆寫並且返回了 true
就表示希望攔截該方法,於是就把觸控事件交給當前 ViewGroup 的 onTouchEvent(MotionEvent)
處理;否則,會交給子 View 的 dispatchTouchEvent(MotionEvent)
進行處理,如果該子ViewGroup 的話,就會在該子 View 中執行一遍上述邏輯,否則會呼叫子 View 的 dispatchTouchEvent(MotionEvent)
的方法,這近似呼叫了子 View 的 onTouchEvent(MotionEvent)
。然後,就這樣一層層地遍歷下去,直到最頂層的 View 。所以,本質上, dispatchTouchEvent(MotionEvent)
的返回結果就是由 onTouchEvent(MotionEvent)
決定的,只是它要麼是當前 ViewGroup 的 onTouchEvent(MotionEvent)
,要麼是子 View 的 onTouchEvent(MotionEvent)
。
4.3 事件傳遞的原始碼分析
上述我們分析了事件分發機制的原理,下面我們通過原始碼來更具體地瞭解這塊是如何設計的。同樣,我們的焦點也只在那三個需要重點關注的方法。
4.3.1 決定是否攔截事件
首先,我們來看 ViewGroup 中的 dispatchTouchEvent(MotionEvent)
方法,我們節選了其一部分:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { // ... boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; if (actionMasked == MotionEvent.ACTION_DOWN) { // 1 // 這裡表示如果是一個新的觸控事件就要重置所有的狀態,其中包括將 mFirstTouchTarget 置為 null cancelAndClearTouchTargets(ev); resetTouchState(); } // 在這裡檢查是否攔截了事件,mFirstTouchTarget 是之前處理觸控事件的 View 的封裝 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 這裡判斷該 ViewGroup 是否禁用了攔截,由 requestDisallowInterceptTouchEvent 設定 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); } else { intercepted = false; } } else { // 非按下事件並且 mFirstTouchTarget 為 null,說明判斷過攔截的邏輯並且啟用了攔截 intercepted = true; } // ... } // ... return handled; } 複製程式碼
上面程式碼是我們節選的 ViewGroup 攔截事件的部分程式碼,這裡的邏輯顯然比虛擬碼複雜的多。不過,儘管如此,這些程式碼確實必不可少的。因為,當我們要去判斷是否攔截一個觸控事件的時候,此時觸控的事件仍然在繼續,這意味著這個方法會被持續呼叫;抬起的時候再按下,又是另一次呼叫。考慮到這個連續性,我們需要多做一些邏輯。
這裡我們首先在 1 處通過行為是否是“按下”的來判斷是否是一次新的觸控事件,如果是的話我們需要重置當前的觸控狀態。然後,我們需要進行是否攔截的判斷,這裡用了一個 if-else
的邏輯,我覺得這裡的設計很好。我們可以從設計的角度來考慮為什麼這麼設計。
首先,我們需要根據事件的型別來決定是否應該呼叫 onInterceptTouchEvent()
,因為對一次觸控事件,我們只需要在“按下”的時候判斷一次就夠了,其餘的事件只需要沿用之前的判斷結果即可。所以,顯然我們需要將 MotionEvent.ACTION_DOWN
作為一個判斷條件。然後,我們要如何記錄上次的判斷結果?我們可以將 intercepted
作為一個全域性的變數來記錄。不過這裡用的是 mFirstTouchTarget
這個全域性的變數,它用來記錄上次處理觸控事件的子元素,試想如果之前的事件交給過子元素處理,那麼它就不為空,所以我們將它是否為 null,作為一個判斷的依據。
當 mFirstTouchTarget != null
的時候,我們可以認為 intercepted = false
。但是,我們還有一個 mGroupFlags
需要考慮。這裡我們使用 FLAG_DISALLOW_INTERCEPT
取了這個標誌位的欄位來判斷該 ViewGroup 是否禁用了攔截。我們可以通過 ViewGroup 的 requestDisallowInterceptTouchEvent(boolean)
設定這個變數的值。顯然,只有沒有禁用攔截事件的時候我們才需要呼叫 onInterceptTouchEvent()
判斷是否開啟了攔截。如果把 requestDisallowInterceptTouchEvent(boolean)
和 onInterceptTouchEvent()
比如攔截事件的兩個開關的話,那麼顯然前者的控制範圍更大一些。
4.3.2 分發事件給子元素
如果在上面的操作中事件沒有被攔截並且沒有被取消,那麼就會進入下面的邏輯。這部分程式碼處在 dispatchTouchEvent()
中。在下面的邏輯中會根據子元素的狀態將事件傳遞給子元素:
// 對子元素進行倒序遍歷,即從上到下進行遍歷 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 (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } // ... // 在這裡呼叫了 dispatchTransformedTouchEvent() 方法將事件傳遞給子元素 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // ... 記錄一些狀態資訊 // 在這裡完成對 mFirstTouchTarget 的賦值,表示觸控事件被子元素處理 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; // 結束迴圈,完成子元素的遍歷 break; } // 顯然,如果到了這一步,那麼子元素的遍歷仍將繼續 } 複製程式碼
當判斷了指定的 View 可以接收觸控事件之後會呼叫 dispatchTransformedTouchEvent()
方法分發事件。其定義的節選如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // ... if (child == null) { // 本質上邏輯與 View 的 dispatchTouchEvent() 一致 handled = super.dispatchTouchEvent(transformedEvent); } else { // ... // 交給子元素繼續分發事件 handled = child.dispatchTouchEvent(transformedEvent); } return handled; } 複製程式碼
dispatchTransformedTouchEvent()
會根據傳入的 child
是否為 null
分成兩種呼叫的情形:事件沒有被攔截的時候,讓子元素繼續分發事件;另一種是當事件被攔截的時候,呼叫當前的 ViewGroup 的 super.dispatchTouchEvent(transformedEvent)
處理事件。
4.3.3 View 中的 dispatchTouchEvent
上面我們分析的 dispatchTouchEvent(MotionEvent)
是 ViewGroup 中重寫之後的方法。但是,正如我們上面的分析,重寫之前的方法總是會被呼叫,只是物件不同。這裡我們就來分析以下這個方法的作用。
public boolean dispatchTouchEvent(MotionEvent event) { // ... boolean result = false; // .... if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } // 這裡回調了 setOnTouchListener() 方法傳入的 OnTouchListener ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // 如果 OnTouchListener 沒有被回撥過或者返回了 false,就會呼叫 onTouchEvent() 進行處理 if (!result && onTouchEvent(event)) { result = true; } } // ... return result; } 複製程式碼
根據上面的原始碼分析,我們知道,如果當前的 View 設定過 OnTouchListener
, 並且在 onTouch()
回撥方法中返回了 true
,那麼 onTouchEvent(MotionEvent)
將不會得到呼叫。那麼,我們再來看一下 onTouchEvent()
方法:
public boolean onTouchEvent(MotionEvent event) { // ... // 判斷當前控制元件是否是可以點選的:實現了點選、長按或者設定了可點選屬性 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // ... if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: // ... if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } // ... break; case MotionEvent.ACTION_DOWN: // ... if (!clickable) { checkForLongClick(0, x, y); break; } // ... break; // ... } return true; } return false; } 複製程式碼
這裡先判斷指定的控制元件是否是可點選的,即是否設定過點選或者長按的事件。然後會在手勢抬起的時候呼叫 performClick()
方法,並會在這個方法中嘗試從 ListenerInfo
取 OnClickListener
進行回撥;會在長按的時候進行監聽以呼叫相應長按事件;其他的事件與之類似,可以自行分析。所以,我們可以得出結論,當為控制元件的觸控事件進行了賦值並且在其中返回了 false
那麼就代表這個觸控事件被消耗了。這個觸控事件的優先順序比較高,即使設定過單擊和長按事件的回撥,它們也不會被呼叫。
經過上述分析,我們可以知道 View 中的 dispatchTouchEvent(MotionEvent)
方法就是用來對手勢進行處理的,所以回到 4.3.2
,那裡的意思就是:如果 ViewGroup 攔截了觸控事件,那麼它就自己來對事件進行處理;否則就把觸控事件傳遞給子元素,讓它來進行處理。