View体系与自定义View(四)—— View的事件分发机制

1. View的事件分发机制

当点击屏幕时,就会产生点击事件,这个事件被封装成了一个类:MotionEvent。而当这个MotionEvent产生后,系统就会将这个MotionEvent传递给View的层级,MotionEventView中的层级传递过程就是点击事件分发。

MotionEvent对象产生一系列事件,它有四种状态:

  • MotionEvent.ACTION_DOWN:手指按下屏幕的瞬间(一切事件的开始)
  • MotionEvent.ACTION_MOVE:手指在屏幕上移动
  • MotionEvent.ACTION_UP:手指离开屏幕瞬间
  • MotionEvent.ACTION_CANCEL :取消手势,一般由程序产生,不会由用户产生

Android中的行为onClickonLongClickonScrollonFling等等,都是由许多个Touch事件构成的(一个ACTION_DOWNnACTION_MOVE1ACTION_UP)。

motion [ˈmoʊʃn] 动作;移动;手势;请求;意向;议案

AndroidUI界面由ActivityViewGroupView 及其派生类组成,事件也就在ActivityViewGroupView中进行传递。

事件分发过程由dispatchTouchEventonInterceptTouchEventonTouchEvent协作完成:

  • dispatchTouchEvent:分发事件,当点击事件能够传递给当前View时,该方法就会被调用
  • onInterceptTouchEvent:事件拦截,只存在于ViewGroup中,普通的View没有此方法。在ViewGroupdispatchTouchEvent内部调用
  • onTouchEvent:处理点击事件,在dispatchTouchEvent中调用

dispatch [dɪˈspætʃ] 派遣,发送;迅速处理,快速办妥;杀死,处决 intercept [ˌɪntərˈsept] 拦截;截断;窃听

当屏幕被触摸,首先会通过硬件产生触摸事件传入内核,然后走到Framework层,最后经过一系列事件处理到达ViewRootImpl.processPointerEvent(...) 方法,而 ViewRootImpl.processPointerEvent(...) 方法中的mView就是DecorView

当点击事件传递到当前的Activity时,具体的事件处理工作都是交由Activity中的PhoneWindow完成的,然后PhoneWindow再把事件处理工作交给DecorViewDecorView一般就是当前界面的底层容器,即setContentView(...)所设置的View的父容器),之后再由DecorView将事件处理工作交给ViewGroup

首先来看 ViewRootImpl.processPointerEvent(...) 方法:

// /frameworks/base/core/java/android/view/ViewRootImpl.java
View mView;

private int processPointerEvent(QueuedInputEvent q) {
    final MotionEvent event = (MotionEvent)q.mEvent;

    mAttachInfo.mUnbufferedDispatchRequested = false;
    mAttachInfo.mHandlingPointerEvent = true;
    boolean handled = mView.dispatchPointerEvent(event); // 1
    maybeUpdatePointerIcon(event);
    maybeUpdateTooltip(event);
    mAttachInfo.mHandlingPointerEvent = false;
    if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
        mUnbufferedInputDispatch = true;
        if (mConsumeBatchedInputScheduled) {
            scheduleConsumeBatchedInputImmediately();
        }
    }
    return handled ? FINISH_HANDLED : FORWARD;
}

注释 1 处调用了 View.dispatchPointerEvent(...) 方法,以下是相关源码:

// /frameworks/base/core/java/android/view/View.java
@UnsupportedAppUsage
public final boolean dispatchPointerEvent(MotionEvent event) {
    if (event.isTouchEvent()) {
        return dispatchTouchEvent(event); // 1
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

以下是 ViewGroup.dispatchTouchEvent(...) 的源代码:

// /frameworks/base/core/java/android/view/ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
     ...

     if (onFilterTouchEventForSecurity(ev)) {
         ...

         // Handle an initial down.
         if (actionMasked == MotionEvent.ACTION_DOWN) {
             cancelAndClearTouchTargets(ev);
             resetTouchState();
         }

         // Check for interception.
         final boolean intercepted;
         if (actionMasked == MotionEvent.ACTION_DOWN
                 || mFirstTouchTarget != null) { // 1
             final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 2
             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;
         }

         ...
     }

     ...
     return handled;
}

首先会判断事件是否是为 ACTION_DOWN 事件,如果是,则进行初始化。这里为什么要进行初始化呢?这是因为一个完整的事件序列是以 DOWN 开始,以 UP 结束的。所以,如果是 DOWN 事件,说明这是一个新的事件序列,故而需要初始化之前的状态。在注释 1 处,如果条件满足,则执行下面的代码,mFirstTouchTarget 的意义是,当前 ViewGroup 是否拦截了此事件,如果拦截了,mFirstTouchTarget == null,如果没有拦截则交给子 View 来处理,mFirstTouchTarget != null

假设当前的 ViewGroup 拦截了此事件,mFirstTouchTarget == nullfalse,如果这时触发 ACTION_DOWN 事件,则会执行 onInterceptTouchEvent(ev) 方法;如果触发的是 ACTION_MOVEACTION_UP 事件,则不再执行 onInterceptTouchEvent(ev) ,而是直接设置 intercepted = true ,此后的一个时间序列均由这个 ViewGroup 处理。

在注释 2 处出现了 FLAG_DISALLOW_INTERCEPT 标志位,它主要是禁止 ViewGroup 拦截除了 DOWN 之外的事件,一般通过子 View.requestDisallowInterceptTouchEvent(...) 来设置。

所以,总结一下就是,当 ViewGroup 要拦截事件的时候,那么后续的事件序列都交给它处理,而不用再调用 onInterceptTouchEvent(...) 方法了。所以 onInterceptTouchEvent(...) 方法并不是每次事件都会调用的。

以下是 ViewGroup.onInterceptTouchEvent(...) 方法:

// /frameworks/base/core/java/android/view/ViewGroup.java
public boolean onInterceptTouchEvent(MotionEvent ev) {
    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;
}

如果想要 ViewGroup 拦截事件,那么应该在自定义的 ViewGroup 中重写这个方法,接下来查看 ViewGroup.dispatchTouchEvent(...) 方法剩下的部分源码:

// /frameworks/base/core/java/android/view/ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
  	...
        final View[] children = mChildren;
        for (int i = childrenCount - 1; i >= 0; i--) { // 1
            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 (!child.canReceivePointerEvents()
                    || !isTransformedTouchPointInView(x, y, child, null)) { // 2
                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)) { // 3
                // 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;
            }

            // The accessibility focus didn't handle the event, so clear
            // the flag and do a normal dispatch to all children.
            ev.setTargetAccessibilityFocus(false);
        }
  
  	...
}

注释 1 处是 for 循环,首先遍历 ViewGroup 的子元素,判断子元素是否能够接收到点击事件,如果子元素能够接收到点击事件,则交由子元素来处理。需要注意的是这个 for 循环是倒序便利的,即从最上层的子 View 开始往内层遍历。接着看注释 2 处的代码,其意思是判断触摸点的卫士是否在子 View 的范围或者子 View 是否在播放动画,如果均不符合则执行 continue 语句,表示这个子 View 不符合条件,开始便利下一个子 View。接下来查看注释 3 处的 ViewGroup.dispatchTransformedTouchEvent(...) 方法做了什么,代码如下所示:

// /frameworks/base/core/java/android/view/ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
      View child, int desiredPointerIdBits) {
      final boolean handled;

      final int oldAction = event.getAction();
      if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
          event.setAction(MotionEvent.ACTION_CANCEL);
          if (child == null) {
              handled = super.dispatchTouchEvent(event);
          } else {
              handled = child.dispatchTouchEvent(event);
          }
          event.setAction(oldAction);
          return handled;
      }

     ...
}

如果有子 View,则调用子 View.dispatchTouchEvent(...) 方法,如果没有子 View,则调用 super.dispatchTouchEvent(...) 方法。ViewGroup 是继承自 View 的,以下是 View.dispatchTouchEvent(...) 方法:

// /frameworks/base/core/java/android/view/View.java
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)) { // 1
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

   ...

    return result;
}

li.mOnTouchListener.onTouch(...) 返回 true,表示该事件被消费,就不会执行 View.onTouch(...) 方法,否则就会执行该 View.onTouch(...) 方法。可以看出 li.mOnTouchListener.onTouch(...) 方法的优先级要高于 View.onTouchEvent(...) 方法。下面接着看 View.onTouchEvent(...) 方法:

// /frameworks/base/core/java/android/view/View.java
public boolean onTouchEvent(MotionEvent event) {
  .	...
    final int action = event.getAction();

    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 1

    ...

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                ...
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    // take focus if we don't have it already and we should in
                    // touch mode.
                    boolean focusTaken = false;
                    ...
                    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();
                            }
                        }
                    }

                    ...
                }
                mIgnoreNextUpEvent = false;
                break;

 								...
        }

        return true;
    }

    return false;
}

从注释 1 处可以知道,只要 ViewCLICKABLELONG_CLICKABLE 有一个为 true,那么 View.onTouch(...) 就会返回 true 消耗这个事件。CLICKABLELONG_CLICKABLE 代表 View 可以被点击和长按点击,可以通过 View.setClickable(...)View.setLongClickable(...) 方法来设置,也可以通过 View.setOnClickListener(...)View.setOnLongClickListener(...) 方法来设置,它们会自动将 View 设置为 CLICKABLDELONG_CLICKABLE。接着在 ACTION_UP 事件中会调用 View.performClickInternal() 方法,源码如下所示:

// /frameworks/base/core/java/android/view/View.java
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) { // 1
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

从上面的代码注释 1 处可以看出,如果 View 设置了点击事件 OnClickListener,那么它的 onClick 方法就会被执行。

ViewRootImpl.DecorView.dispatchPointerEvent - > ViewRootImpl.DecorView.dispatchTouchEvent -> Activity.dispatchTouchEvent() -> PhoneWindow.superDispatchTouchEvent -> DecorView.superDispatchTouchEvent -> ViewGroup.dispatchTouchEvent()。 如果ViewGroup.dispatchTouchEvent()返回false则调用onTouchEvent方法,true表示事件被消费掉了。

但是为什么DecorView就走了两次?—— 主要原因就是解耦

  • ViewRootImpl并不知道有Activity的存在,它只是持有DecorView,所以传给了DecorView
  • DecorView知道Activity的存在,所以传给了Activity
  • Activity不知道DecorView的存在,它只是持有PhoneWindow,所以就形成了这样一段调用链;

Android事件响应机制是先分发(先由外部的View接收,然后依次传递给其内层的最小View)再处理(从最小View单元(事件源)开始依次向外层传递)的形式实现的。其复杂性表现在:可以控制每层事件是否继续传递(分发和拦截协同实现),以及事件的具体消费(事件分发也具有事件消费能力)。

Activity只有dispatchTouchEventonTouchEvent方法。以下是ActivitydispatchTouchEvent方法和onTouchEvent方法:

public class Activity extends ContextThemeWrapper implements Window.Callback, KeyEvent.Callback {

  public boolean dispatchTouchEvent(MotionEvent ev) {
    if (getWindow().superDispatchTouchEvent(ev)) {
      return true;
    }
    return onTouchEvent(ev);
  }

  public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
      finish();
      return true;
    }
    return false;
  }
}

以下是ViewdispatchTouchEvent方法和onTouchEvent方法:

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

  public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
      ...
      if (!result && onTouchEvent(event)) {
        result = true;
      }
    }
    return result;
  }
  
  public boolean onTouchEvent(MotionEvent event) { } 
}

以下是ViewGroupdispatchTouchEvent方法和onInterceptTouchEvent方法:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

  public boolean dispatchTouchEvent(MotionEvent ev) {
    intercepted = onInterceptTouchEvent(ev);
  }

  public boolean onInterceptTouchEvent(MotionEvent ev) { }
}

View没有onInterceptTouchEvent方法,一旦有点击事件传递給它,它就会处理。

U 型图

说明:

  1. ActivitydispatchTouchEvent只有return super.dispatchTouchEvent(ev)才往下走,返回true或者false事件就被消费了(终止传递);
  2. 如果事件不被中断,整个事件流向就是一个U型图。 如果没有对控件里面的方法进行重写或更改返回值,而直接用super调用父类的默认实现,那么整个事件流向应该是从Activity -> ViewGroup -> View从上往下调用dispatchTouchEvent方法,一直到叶子节点(View)的时候,再由View -> ViewGroup -> Activity从下往上调用onTouchEvent方法;
  3. 对于dispatchTouchEventonTouchEventonInterceptTouchEvent
    ViewGroupView的这些方法的默认实现就是会让整个事件按照U型完整走完,所以return super.xxxxxx()就会让事件依照U型的方向的完整走完整个事件流动路径,中间不做任何改动,不回溯、不终止,每个环节都走到;
  4. dispatchTouchEventonTouchEvent一旦return true,事件就停止传递了,没有谁能再收到这个事件;return false的时候,事件都回传给父控件的onTouchEvent处理;
  5. 对于dispatchTouchEvent返回false的含义应该是:事件停止往子View传递和分发,同时开始往父控件回溯(父控件的onTouchEvent开始从下往上回传直到某个onTouchEvent return true), 事件分发机制就像递归,return false的意义就是递归停止然后开始回溯;
  6. 对于onTouchEvent return false就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动;
  7. 对于onInterceptTouchEventintercept的意思就拦截,每个ViewGroup每次在做分发的时候,问一问拦截器要不要拦截(也就是问问自己这个事件要不要自己来处理),如果要自己处理那就在onInterceptTouchEvent方法中return true就会交给自己的onTouchEvent的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子View也需要这个事件,所以onInterceptTouchEvent拦截器return super.onInterceptTouchEvent()return false是一样的,是不会拦截的,事件会继续往子ViewdispatchTouchEvent传递;
  8. ViewGroupdispatchTouchEventreturn true是终结传递,return false是回溯到父ViewonTouchEvent,然后ViewGroup怎样通过dispatchTouchEvent方法能把事件分发到自己的onTouchEvent处理呢,return truefalse都不行,那么只能通过Interceptor把事件拦截下来给自己的onTouchEvent,所以ViewGroup dispatchTouchEvent方法的super默认实现就是去调用onInterceptTouchEvent;
  9. 对于ViewdispatchTouchEvent return super.dispatchTouchEvent()的时候呢事件会传到哪里呢,很遗憾View没有拦截器。但是同样的道理return true是终结,return false是回溯会父类的onTouchEvent怎样把事件分发给自己的onTouchEvent处理呢,那只能return super.dispatchTouchEventView类的dispatchTouchEvent方法默认实现就是能帮你调用View自己的onTouchEvent方法的;

总结:

  1. 对于dispatchTouchEventonTouchEventreturn true是终结事件传递,retrun false是回溯到父ViewonTouchEvent方法;
  2. ViewGroup想把自己分发给自己的onTouchEvent,需要拦截器onInterceptTouchEvent方法return true把事件拦截下来;
  3. ViewGroup的拦截器onInterceptTouchEvent默认是不拦截的,所以return super.onInterceptTouchEvent = return false
  4. View没有拦截器,为了让View可以把事件分发给自己的onTouchEventViewdispatchTouchEvent默认实现(super)就是把事件分发给自己的onTouchEvent;

2 验证View事件分发机制

以下是MyView的代码,继承View:

class MyView @JvmOverloads constructor(
  context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

  override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
    Log.e("CAH", "MyView: dispatchTouchEvent")
    return super.dispatchTouchEvent(event)
  }

  override fun onTouchEvent(event: MotionEvent?): Boolean {
    Log.e("CAH", "MyView: onTouchEvent")
    return super.onTouchEvent(event)
  }

}

MyViewGroup01MyViewGroup02是一样的代码,这里以MyViewGroup01为例,继承ViewGroup

class MyViewGroup01 @JvmOverloads constructor(
  context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {

  override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    Log.e("CAH", "MyViewGroup01: dispatchTouchEvent")
    return super.dispatchTouchEvent(ev)
  }

  override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    Log.e("CAH", "MyViewGroup01: onInterceptTouchEvent")
    return super.onInterceptTouchEvent(ev)
  }

  override fun onTouchEvent(event: MotionEvent?): Boolean {
    Log.e("CAH", "MyViewGroup01: onTouchEvent")
    return super.onTouchEvent(event)
  }

}

MyViewMyViewGroup布局文件:

<com.example.kotlintest.MyViewGroup02 xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:background="@android:color/holo_green_light">

    <com.example.kotlintest.MyViewGroup01
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@android:color/holo_purple">

        <com.example.kotlintest.MyView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="@android:color/holo_red_light" />

    </com.example.kotlintest.MyViewGroup01>

</com.example.kotlintest.MyViewGroup02>

运行:

事件分发

点击MyView(红色部分):先接收事件的是父容器(MyViewGroup02),继续分发,而事件的分发过程分为两个步骤:分发过程和处理过程。其正常的分发结果为:

// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
// CAH: MyViewGroup01: onInterceptTouchEvent
// CAH: MyView: dispatchTouchEvent

// 处理过程
// CAH: MyView: onTouchEvent
// CAH: MyViewGroup01: onTouchEvent
// CAH: MyViewGroup02: onTouchEvent
2.1 dispatchTouchEvent(分发事件)

如果在MyViewGroup01dispatchTouchEvent方法中返回true,表示需要在MyViewGroup01消费了整个事件,即不会再分发,也不会再处理。dispatchTouchEvent方法中返回true的打印信息:

// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent

如果在MyViewGroup01dispatchTouchEvent方法中返回false,表示在MyViewGroup01点击事件在本层不再继续进行分发,并交由上层控件的onTouchEvent方法进行消费。dispatchTouchEvent方法中返回false的打印信息:

// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent

// 处理过程
// CAH: MyViewGroup02: onTouchEvent
2.2 onInterceptTouchEvent(拦截事件)

如果在MyViewGroup01onInterceptTouchEvent方法中返回true,表示需要在MyViewGroup01拦截这个点击事件,不再继续往下分发,即MyView不再执行dispatchTouchEvent方法。但是只是分发结束了而已,接着开始处理事件。下面是onInterceptTouchEvent方法中返回true的打印信息:

// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
// CAH: MyViewGroup01: onInterceptTouchEvent

// 处理过程
// CAH: MyViewGroup01: onTouchEvent
// CAH: MyViewGroup02: onTouchEvent

如果在MyViewGroup01onInterceptTouchEvent方法中返回false,表示需要在MyViewGroup01不会拦截这个点击事件,继续往下分发。下面是onInterceptTouchEvent方法中返回false的打印信息:

// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
// CAH: MyViewGroup01: onInterceptTouchEvent
// CAH: MyView: dispatchTouchEvent

// 处理过程
// CAH: MyView: onTouchEvent
// CAH: MyViewGroup01: onTouchEvent
// CAH: MyViewGroup02: onTouchEvent
2.3 onTouchEvent(消费事件)

如果MyViewGroup01onTouchEvent方法中返回true,表示MyViewGroup01可以将该事件直接消费掉了,即分发结束后,处理事件的时候,直接处理到MyViewGroup01就可以结束了。下面是onTouchEvent方法中返回true的打印信息:

// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
// CAH: MyViewGroup01: onInterceptTouchEvent
// CAH: MyView: dispatchTouchEvent

// 处理过程
// CAH: MyView: onTouchEvent
// CAH: MyViewGroup01: onTouchEvent

如果MyViewGroup01onTouchEvent方法中返回false,表示MyViewGroup01不可以将该事件直接消费掉,即事件继续往上处理。下面是onTouchEvent方法中返回false的打印信息:

// 分发过程
// CAH: MyViewGroup02: dispatchTouchEvent
// CAH: MyViewGroup02: onInterceptTouchEvent
// CAH: MyViewGroup01: dispatchTouchEvent
// CAH: MyViewGroup01: onInterceptTouchEvent
// CAH: MyView: dispatchTouchEvent

// 处理过程
// CAH: MyView: onTouchEvent
// CAH: MyViewGroup01: onTouchEvent
// CAH: MyViewGroup02: onTouchEvent

3 对于View.onTouchEventOnClickListerner.onClickOnTouchListener.onTouch的优先级

View.dispatchTouchEvent -> OnTouchListener.onTouch -> View.onTouchEvent -> View.performClick -> OnClickListener.onClick

以下是View的源码:

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

  public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    if (onFilterTouchEventForSecurity(event)) {
      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;
  }

  public boolean onTouchEvent(MotionEvent event) {

    switch (action) {
      case MotionEvent.ACTION_UP:
      if (!post(mPerformClick)) {
        performClick();
      }
      break;
    }

  }

  public boolean performClick() {
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
      li.mOnClickListener.onClick(this);
      result = true;
    } else {
      result = false;
    }

    return result;
  }

  static class ListenerInfo {
    private OnTouchListener mOnTouchListener;
    public OnClickListener mOnClickListener;
  }

  public interface OnTouchListener {
    boolean onTouch(View v, MotionEvent event);
  }
  
  public interface OnClickListener {
    void onClick(View v);
  }

}

onTouch方法是ViewOnTouchListener接口中定义的方法;onClick方法是ViewOnClickListener接口中定义的方法;

  • View.dispatchTouchEvent方法中,OnTouchListener.onTouch 方法优先级比View.onTouchEvent方法的优先级高,会先触发;
  • 如果OnTouchListener.onTouch方法返回false会接着触发View.onTouchEvent方法;返回true,则View.onTouchEvent方法不会被调用;
  • OnClickListerner.onClick方法是在View.onTouchEventMotionEvent.ACTION_UP事件通过View.performClick方法触发的;

因此,OnTouchListener.onTouch方法如果返回true,则不会执行View.onTouchEvent方法,也就更不会执行OnClickListener.onClick方法,如果返回false,则两个都会执行。

4 事件序列

事件序列:同一个事件序列是指从手指触摸屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中产生的一系列事件,这个事件以DOWN事件开始,中间含有不定数量的MOVE事件,最终以UP事件结束

ViewGroup.dispatchTouchEvent中消费ACTION_DOWN事件,ACTION_UP事件是怎么传递?

正常情况下,一个事件序列只能被一个View拦截且消耗,因为一旦某个View拦截了此事件,那么同一个事件序列内的所有事件都会直接交给它处理,并且它的onInterceptTouchEvent不会再被调用。 因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊的手段可以做到,比如一个View将本该字自己理的事件通过onTouchEvent强行传递给其他View处理。

Activity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onInterceptTouchEvent() -> view1.dispatchTouchEvent() -> view1.onTouchEvent() -> ViewGroup1.onTouchEvent()-> Activity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onTouchEvent()

如果ActivityViewGroupView都不消费ACTION_DOWN,那么ACTION_UP事件是怎么传递的?

某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一时间序列中的其它事件也不会交给它来处理,事件会重新交给它的父元素去处理,即父元素的onTouchEvent会被调用。 如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。

ACTION_DOWNActivity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onInterceptTouchEvent()-> view1.dispatchTouchEvent() -> view1.onTouchEvent() -> ViewGroup1.onTouchEvent() -> Activity.onTouchEvent();

ACTION_MOVEActivity.dispatchTouchEvent() -> Activity.onTouchEvent() -> 消费

ACTION_CANCEL什么时候触发?

  • 如果在父View中拦截ACTION_UPACTION_MOVE,在父视图拦截消息的瞬间,就指定子视图不接受后续消息了,同时子视图会收到ACTION_CANCEL事件;
  • 如果触摸某个控件,但是又不是在这个控件的区域上抬起(移动到别的地方了),就会出现ACTION_CANCEL

同时对父View和子View设置点击方法,优先响应哪个?

优先响应子View, 如果先响应父View,那么子View将永远无法响应。View要优先响应事件,必须先调用onInterceptTouchEven对事件进行拦截,那么事件不会再往下传递,直接交给父ViewonTouchEvent处理。

以下是ViewGroupdispatchTouchEvent()源码:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
  
  private TouchTarget mFirstTouchTarget;
  
  @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {

    if (actionMasked == MotionEvent.ACTION_DOWN) { // 1. 
      cancelAndClearTouchTargets(ev);
      resetTouchState();
    }

    // 2
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 2.1
      final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 2.2
      if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); 
      } else {
        intercepted = false;
      }
    } else {
      intercepted = true;
    }
  }
  
  // 3
  public boolean onInterceptTouchEvent(MotionEvent ev) {
    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;
  }
}

注释1:这里首先判断事件是否为DOWN事件,如果是,则进行初始化,这事因为一个完整的事件序列是以DOWN开始,以UP结束的。

FLAG_DISALLOW_INTERCEPT标志位,它主要是禁止ViewGroup拦截除了DOWN之外的事件,一般通过子ViewrequestDisallowInterceptTouchEvent来设置。

requestDisallowInterceptTouchEvent的调用时机或者点击事件被拦截,但是想传到下面的View,如何操作?

重写子类的requestDisallowInterceptTouchEvent()方法,返回true就不会执行父类的onInterceptTouchEvent(),可将点击事件传到下面的View, 剥夺了父View对除了ACTION_DOWN以外的事件的处理权。

ViewGroup要拦截事件的时候,那么后续的事件都交给它处理,而不用调用onInterceptTouchEvent方法了。所以onInterceptTouchEvent方法并不是每次事件都会调用。

参考

https://blog.csdn.net/qq_32534441/article/details/103634329
https://www.jianshu.com/p/e99b5e8bd67b
Android | 理解 ViewRootImpl

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值