View 體系詳解:座標系、滑動、手勢和事件分發機制

Brick發表於2018-10-08

1、位置

1.1 座標系

下面是 Android 中的 View 座標系的基本圖。要獲得一個 View 的位置,我們可以藉助兩個物件,一個是 View ,一個是 MotionEvent。以下是它們的一些方法的位置的含義:

Android View 座標系

在 View 中共有 mLeft, mRight, mTopmBottom 四個變數包含 View 的座標資訊,你可以在原始碼中獲取它們的含義:

  1. mLeft:指定控制元件的左邊緣距離其父控制元件左邊緣的位置,單位:畫素;
  2. mRight:指定控制元件的右邊緣距離其父控制元件左邊緣的位置,單位:畫素;
  3. mTop:指定控制元件的上邊緣距離其父控制元件上邊緣的位置,單位:畫素;
  4. mBottom:指定控制元件的下邊緣距離其父控制元件上邊緣的位置,單位:畫素。

此外,View 中還有幾個方法用來獲取控制元件的位置等資訊,實際上就是上面四個變數的 getter 方法:

  1. getLeft():即 mLeft
  2. getRight():即 mRight
  3. getTop():即 mTop
  4. getBottom():即 mBottom

所以,我們可以得到兩個獲取 View 高度和寬度資訊的方法:

  1. getHeight():即 mBottom - mTop
  2. getWidth():即 mRight - mLeft

另外,就是 View 中的 getX()getY() 兩個方法,你需要注意將其與 MotionEvent 中的同名方法進行區分。在沒有對控制元件進行平移的時候,getX()getLeft() 返回結果相同,只是前者會在後者的基礎上加上平移的距離:

  1. getX():即 mLeft + getTranslationX(),即控制元件的左邊緣加上 X 方向平移的距離;
  2. getY():即 mTop + getTranslationY(),即控制元件的上邊緣加上 Y 方向平移的距離;

以上是我們對 View 中獲取控制元件位置的方法的梳理,你可以到原始碼中檢視它們更加相詳盡的定義,那更有助於自己的理解。

1.2 MotionEvent

通常當你對控制元件進行觸控監聽的時候會用到 MotionEvent ,它封住了觸控的位置等資訊。下面我們對 MotionEvent 中的獲取點選事件的位置的方法進行梳理,它主要涉及下面四個方法:

  1. MotionEvent.getX():獲取點選事件距離控制元件左邊緣的距離,單位:畫素;
  2. MotionEvent.getY():獲取點選事件距離控制元件上邊緣的距離,單位:畫素;
  3. MotionEvent.getRawX():獲取點選事件距離螢幕左邊緣的距離,單位:畫素;
  4. MotionEvent.getRawY():獲取點選事件距離螢幕上邊緣的距離,單位:畫素。

另外是觸控事件中的三種典型的行為,按下、移動和抬起。接下來的程式碼示例中我們會用到它們來判斷手指的行為,並對其做響應的處理:

  1. MotionEvent.ACTION_DOWN:按下的行為;
  2. MotionEvent.ACTION_MOVE:手指在螢幕上移動的行為;
  3. 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, mRightmBottom。所以,呼叫該方法同時可以改變控制元件的高度和寬度,但有時候我們不需要改變控制元件的高度和寬度,只要移動其位置即可。所以,我們又有方法 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 的 transitionXtransitionY 屬性:

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 進一步重繪。所以,也就是靠著這種不斷進行重繪的方式實現了滾動的效果。

滑動效果最終結束的判斷是通過 ScrollercomputeScrollOffset() 方法實現的,當滾動停止的時候,該方法就會返回 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() 方法的時候,會呼叫該 PhoneWindowsetContentView() 方法,並在這個方法中生成一個 DecorView 作為 Activity 的跟 View

根據上面的分析,當一個點選事件被觸發的時候,首先接收到該事件的是 Activity。因為,Activity 覆蓋了整個螢幕,我們需要先讓它接收事件,然後它把事件傳遞給根 View 之後,再由根 View 向下繼續傳遞。這樣不斷縮小搜尋的範圍,直到最頂層的 View。當然,任何的父容器都可以決定這個事件是不是要繼續向下傳遞,因此,我們可以大致得到下面這個事件傳遞的圖:

事件傳遞圖

左邊的圖是一個 Activity 內部的 ViewWindow 的組織結構。右面的圖可以看作它的切面圖,其中的黑色箭頭表示事件的傳遞過程。這裡事件傳遞的過程是先從下到上,然後再從上到下。也就是從大到小,不斷定位到觸控的控制元件,其中每個父容器可以決定是否將事件傳遞下去。(需要注意的地方是,如果一個父容器有多個子元素的話,那麼在這些子元素中進行遍歷的時候,順序是從上往下的,也就是按照展示的順序)。

上面我們分析了 Android 事件傳遞的過程,相信你有了一個大致的瞭解。但是,想要了解整個事件傳遞過程具體涉及了哪些方法、如何作用等,還需要我們對原始碼進行分析。

4.2 事件傳遞的原理

當觸控事件發生的時候,首先會被 Activity 接收到,然後該 Activity 會通過其內部的 dispatchTouchEvent(MotionEvent) 將事件傳遞給內部的 PhoneWindow;接著 PhoneWindow 會把事件交給 DecorView,再由 DecorView 交給根 ViewGroup。剩下的事件傳遞就只在 ViewGroupView 之間進行。我們可以通過覆寫 Activity 的 dispatchTouchEvent(MotionEvent) 來阻止把事件傳遞給 PhoneWindow。實際上,在我們開發的時候不會對 Window 的事件傳遞方法進行重寫,一般是對 ViewGroup 或者 View。所以,下面我們的分析只在這兩種控制元件之間進行。

當討論 View 的事件分發機制的時候,無外乎下面三個方法:

  1. boolean onInterceptTouchEvent(MotionEvent ev):用來對事件進行攔截,該方法只存在於 ViewGroup 中。一般我們會通過覆寫該方法來攔截觸控事件,使其不再繼續傳遞給子 View。
  2. boolean dispatchTouchEvent(MotionEvent event):用來分發觸控事件,一般我們不覆寫該方法,返回 true 則表示事件被處理了。在 View 中,它負責根據手勢的型別和控制元件的狀態對事件進行處理,會回撥我們的 OnTouchListener 或者 OnClickListener;在 ViewGroup 中,該方法被覆寫,它的責任是對事件進行分發,會對所有的子 View 進行遍歷,決定是否將事件分發給指定的 View。
  3. boolean onTouchEvent(MotionEvent event):用於處理觸控事件,返回 true 表示觸控事件被處理了。ViewGroup 沒有覆寫該方法,故在 ViewGroup 中與 View 中的功能是一樣的。需要注意的是,如果我們為控制元件設定了 OnTouchListener 並且在或者中返回了 true,那麼這個方法不會被呼叫,也就是 OnTouchListener 比該方法的優先順序較高。對我們開發來說,就是 OnTouchListenerOnClickListenerOnLongClickListener 的優先順序要高。

於是,我們可以得到如下的虛擬碼。這段程式碼是存在於 ViewGroup 中的,也就是事件分發機制的核心程式碼:

boolean dispatchTouchEvent(MotionEvent e) {
    boolean result;
    if (onInterceptTouchEvent(e)) {
        result = super.dispatchTouchEvent(e);
    } else {
        result = child.dispatchTouchEvent(e);
    }
    return result;
}
複製程式碼

按照上述分析,觸控事件經過 Activity 傳遞給根 ViewGroup 之後:

如果 ViewGourp 覆寫了 onInterceptTouchEvent() 並且返回了 true 就表示希望攔截該方法,於是就把觸控事件交給當前 ViewGroup 進行處理(觸發 OnTouchListener 或者 OnClickListener 等);否則,會交給子元素的繼續分發。如果該子元素是 ViewGroup 的話,就會在該子 View 中執行一遍上述邏輯,否則會在當前的子元素中對事件進行處理(觸發 OnTouchListener 或者 OnClickListener 等)……就這樣一層層地遍歷下去,本質上是一個深度優先的搜尋演算法。

這裡我們對整個事件分發機制的整體做了一個素描,在接下來的文章中我們會對各個方法的細節進行原始碼分析,為了防止您在接下來的行文中迷路,我們先把這個整體邏輯按下圖進行描述:

事件分發機制原理

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 處通過行為是否是“按下”的來判斷是否是一次新的觸控事件,如果是的話我們需要重置當前的觸控狀態。其次,我們需要根據事件的型別來決定是否應該呼叫 onInterceptTouchEvent(),因為對一次觸控事件,我們只需要在“按下”的時候判斷一次就夠了。所以,顯然我們需要將 MotionEvent.ACTION_DOWN 作為一個判斷條件。然後,我們使用 mFirstTouchTarget 這個全域性的變數來記錄上次攔截的結果——如果之前的事件交給過子元素處理,那麼它就不為空。

除了 mFirstTouchTarget,我們還需要用 mGroupFlagsFLAG_DISALLOW_INTERCEPT 標誌位來判斷該 ViewGroup 是否禁用了攔截。這個標誌位可以通過 ViewGroup 的 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() 方法,並會在這個方法中嘗試從 ListenerInfoOnClickListener 進行回撥;會在長按的時候進行監聽以呼叫相應長按事件;其他的事件與之類似,可以自行分析。所以,我們可以得出結論:當為控制元件的觸控事件進行了賦值並且在其中返回了 true 就代表該事件被消費了,即使設定過單擊和長按事件也不會被回撥,觸控事件的優先順序比後面兩者要高。

經過上述分析,我們可以知道 View 中的 dispatchTouchEvent(MotionEvent) 方法就是用來對手勢進行處理的,所以回到 4.3.2,那裡的意思就是:如果 ViewGroup 攔截了觸控事件,那麼它就自己來對事件進行處理;否則就把觸控事件傳遞給子元素,讓它來進行處理。

4.4.4 總結

以上就是我們對 Android 中事件分發機制的詳解,你可以通過圖片和程式碼結合來更透徹得了解這方面的內容。雖然這部分程式碼比較多、比較長,但是每個地方的設計都是合情合理的。

原始碼

你可以在Github獲取以上程式的原始碼: Android-references


Hello, 我是 WngShhng. 如果您喜歡我的文章,可以在以下平臺關注我:

相關文章