《Android開發藝術探索》——View事件分發機制

小編發表於2017-03-14

概念

同一個事件序列指的是從手指觸控螢幕的那一刻開始,到手指離開螢幕的那一刻結束,在這個過程產生的一系列事件。以down事件開始,可能經過n多個move事件,最終以up事件結束。

案例

自定義三個佈局

<com.yolo.myapplication.view.FirstFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_view_demo"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_blue_bright"
    tools:context="com.yolo.myapplication.view.ViewDemoActivity">

    <com.yolo.myapplication.view.SecondRelativeLayout
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_gravity="center"
        android:background="@android:color/holo_green_dark">

        <com.yolo.myapplication.view.ThirdTextView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:clickable="true"
            android:background="@android:color/holo_red_light"
            android:layout_centerInParent="true" />

    </com.yolo.myapplication.view.SecondRelativeLayout>


</com.yolo.myapplication.view.FirstFrameLayout>複製程式碼

佈局.png
佈局.png

情況一

預設情況,未設定返回true或false,事件都是return super.onXXX(event);

I/Activity:             dispatchTouchEvent>>DOWN
I/FirstFrameLayout:     dispatchTouchEvent>>DOWN
I/FirstFrameLayout:     onInterceptTouchEvent>>>>DOWN
I/SecondRelativeLayout: dispatchTouchEvent>>DOWN
I/SecondRelativeLayout: onInterceptTouchEvent>>>>DOWN
I/ThirdTextView:         dispatchTouchEvent>>DOWN
I/ThirdTextView:         onTouchEvent>>>>DOWN
I/SecondRelativeLayout: onTouchEvent>>>>DOWN
I/FirstFrameLayout:     onTouchEvent>>>>DOWN
I/Activity:             onTouchEvent>>>>DOWN
I/Activity:             dispatchTouchEvent>>UP
I/Activity:             onTouchEvent>>>>UP複製程式碼

預設情況下,某個View一旦開始處理事件,如果事件序列中DOWN事件未被消耗,將事件重新交給它的父元素去處理,且事件序列中的其他事件則不會交給它來處理。

情況二

ThirdTextView的onTouchEvent返回true。

I/FirstFrameLayout:     dispatchTouchEvent>>DOWN
I/FirstFrameLayout:     onInterceptTouchEvent>>>>DOWN
I/SecondRelativeLayout: dispatchTouchEvent>>DOWN
I/SecondRelativeLayout: onInterceptTouchEvent>>>>DOWN
I/ThirdTextView:        dispatchTouchEvent>>DOWN
I/ThirdTextView:        onTouchEvent>>>>DOWN
I/FirstFrameLayout:     dispatchTouchEvent>>UP
I/FirstFrameLayout:     onInterceptTouchEvent>>>>UP
I/SecondRelativeLayout: dispatchTouchEvent>>UP
I/SecondRelativeLayout: onInterceptTouchEvent>>>>UP
I/ThirdTextView:        dispatchTouchEvent>>UP
I/ThirdTextView:        onTouchEvent>>>>UP複製程式碼

圖例:

預設情況下事件傳遞機制
預設情況下事件傳遞機制

  • 結論:

    正常情況下,一個事件序列只能被一個View攔截且消耗。

    View的OnTouchEvent的事件預設都會消耗事件(返回true),因此未發生回傳的現象。如果View是不可點選的(clickable和longClickable同時為false),則返回false。

  • 注意:
    View的longClickable預設都為false,clickable屬性分情況:如Button的clickable屬性預設為true,TextView的clickable屬性預設為false。
    View的enable屬性不影響onTouchEvent預設返回值。clickable和longClickable屬性會影響。

情況三

FristFrameLayout的onTouchEvent返回true

I/Activity:             dispatchTouchEvent>>DOWN
I/FirstFrameLayout:     dispatchTouchEvent>>DOWN
I/FirstFrameLayout:     onInterceptTouchEvent>>>>DOWN
I/SecondRelativeLayout: dispatchTouchEvent>>DOWN
I/SecondRelativeLayout: onInterceptTouchEvent>>>>DOWN
I/ThirdTextView:         dispatchTouchEvent>>DOWN
I/ThirdTextView:         onTouchEvent>>>>DOWN
I/SecondRelativeLayout: onTouchEvent>>>>DOWN
I/FirstFrameLayout:     onTouchEvent>>>>DOWN
I/Activity:                 dispatchTouchEvent>>UP
I/FirstFrameLayout:     dispatchTouchEvent>>UP
I/FirstFrameLayout:     onTouchEvent>>>>UP複製程式碼

圖例:

View事件傳遞
View事件傳遞

  • 結論:

    如果View一旦開始處理事件,如果它不消費ACTION_DOWN事件,那麼同一個事件序列的其他事件也不會再交給它,並且重新交給它的父元素去處理。例如紅線的down事件流向。

情況四

SecondRelativeLayout 攔截事件,onInterceptTouchEvent返回true

I/Activity:             dispatchTouchEvent>>DOWN
I/FirstFrameLayout:     dispatchTouchEvent>>DOWN
I/FirstFrameLayout:     onInterceptTouchEvent>>>>DOWN
I/SecondRelativeLayout: dispatchTouchEvent>>DOWN
I/SecondRelativeLayout: onInterceptTouchEvent>>>>DOWN
I/SecondRelativeLayout: onTouchEvent>>>>DOWN
I/FirstFrameLayout:     onTouchEvent>>>>DOWN
I/Activity:             onTouchEvent>>>>DOWN
I/Activity:             dispatchTouchEvent>>UP
I/Activity:             onTouchEvent>>>>UP複製程式碼

SecondRelativeLayout 攔截事件,onInterceptTouchEvent返回true,且onTouchEvent返回true

I/FirstFrameLayout:     dispatchTouchEvent>>DOWN
I/FirstFrameLayout:     onInterceptTouchEvent>>>>DOWN
I/SecondRelativeLayout: dispatchTouchEvent>>DOWN
I/SecondRelativeLayout: onInterceptTouchEvent>>>>DOWN
I/SecondRelativeLayout: onTouchEvent>>>>DOWN
I/FirstFrameLayout:     dispatchTouchEvent>>UP
I/FirstFrameLayout:     onInterceptTouchEvent>>>>UP
I/SecondRelativeLayout: dispatchTouchEvent>>UP
I/SecondRelativeLayout: onTouchEvent>>>>UP複製程式碼

View事件傳遞機制
View事件傳遞機制

  • 結論:

    View一旦決定攔截,那麼事件序列只能由它來處理,並且它的onInterceptTouchEvent不會被呼叫。比如SecondRelativeLayout攔截了Down事件,那麼事件序列中的Up直接交給它處理,並且不會再去執行onInterceptTouchEvent。

  • 注意:
    事件傳遞由外向內,即事件總是縣傳遞到父元素,然後再由父元素分發給子View,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的事件分發過程。但是ACTION_DOWN事件除外。

注意

  • 當一個View需要處理事件時,如果它設定了OnTouchListener,那麼這個介面的onTouch方法會被回撥。如果onTouch方法返回true,則onTouchEvent不會被呼叫。因此OnTouchListener優先順序高於onTouchEvent。

    OnTouchListener > onTouchEvent > OnClickListener

  • View攔截了事件,那麼事件序列中的其餘事件則直接跳過onInterceptTouchEvent方法去執行onTouchEvent方法。是因為mFirstTouchTarget != null 的標記。

    // 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();
    }
    final boolean intercepted;
    //如果子View處理了事件,mFirstTouchTarget != null 成立,為true。
    //且此時傳遞的是事件序列的其餘事件,不是Down事件,因此整個條件不成立。
    //不會執行onInterceptTouchEvent
    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;
    }複製程式碼
  • FLAG_DISALLOW_INTERCEPT標記位。
    這個標記位可以通過requestDisallowInterceptTouchEvent方法來設定。一般用於子View中,設定後,ViewGroup將無法攔截除了ACTION_DOWN以外的事件。

    ViewGroup在分發事件的時候,會重置此標記位。將導致子View設定這個標記位無效。

滑動衝突解決

一般採用相對簡單的外部攔截法進行處理。
虛擬碼:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
        intercepted = false;
        //TODO
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        int deltaX = x - mLastXIntercept;
        int deltaY = y - mLastYIntercept;
        //攔截條件:如果X軸偏移量大於Y軸,則表示水平滑動,進行攔截處理。
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
            intercepted = true;
        } else {
            intercepted = false;
        }
        break;
    }
    case MotionEvent.ACTION_UP: {
        intercepted = false;
        break;
    }
    default:
        break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;

    return intercepted;
}複製程式碼

相關文章