一篇文章搞定《Android事件分发》

什么是事件分发

事件分发是将屏幕触控信息分发给控件树的一个套机制。 当我们触摸屏幕时,会产生一系列的MotionEvent事件对象,经过控件树的管理者ViewRootImpl,调用view的dispatchPointerEvnet方法进行分发。
深入学习事件分发机制,是为了解决在Android开发中遇到的滑动冲突问题做准备。事件分发机制描述了用户的手势一系列事件是如何被Android系统传递并消费的。

MotionEvent事件

在MotionEvent.java中我们可以看到这些事件。(有很多事件这里只列举重要常用的几个)

事件 作用
ACTION_DOWN 第一个手指初次接触到屏幕时触发
ACTION_MOVE 手指 在屏幕上滑动 时触发,会多次触发
ACTION_UP 最后一个手指离开屏幕时触发
ACTION_CANCEL 事件被取消或被覆盖(事件被上层拦截了,由父View发送,不是用户自己触发的),也就是非人为触发
ACTION_POINTER_DOWN 有非主要的手指按下(即按下之前已经有手指在屏幕上)
ACTION_POINTER_UP 有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)

事件如何从屏幕到APP

这部分虽然暂时不必纠结,但是还是要知道整个流程。暂时重点放在页面的事件开发。后面如果想要深入的时候可以深入的进行研究。

InputManagerService

首先Android系统启动后在SystemServer进程会启动一系列系统服务,如AMS,WMS等
其中还有一个就是我们管理事件输入的InputManagerService(IMS)。
InputManagerService:这个服务就是用来负责与硬件通信,接受屏幕输入事件。他会读取我们系统收到的硬件点InputDispatcher线程,然后进行统一的事件分发调度。

WindowManagerService

Android中view的绘制和事件分发,都是以view树为单位。每一棵view树,都为一个window。
每一棵view树都有一个根,叫做ViewRootImpl ,他负责管理这整一棵view树的绘制、事件分发等。
1、所以我们最终肯定要把事件通知给该View树的ViewRootImpl。
2、而管理Window与之通信的就是我们的WindowManagerService(WMS)
3、每个viewRootImpl在wms中都有一个windowState对应,wms可以通过windowState找到对应的viewRootImpl进行管理。
4、WMS是通过windowState进行Binder通信提供相关需要Window信息,并由IMS发送给APP相关事件
5、InputEventReceiver它负责接收输入事件,并通过Handler发送给ViewRootImpl类处理
6、ViewRootImpl有一个内部类ViewRootHandler,其继承自Handler,工作于主线程上,主要用于处理各类输入事件,如触摸事件、按键事件等。
7、最终发送到Activity

Window

1、window机制就是为了管理屏幕上的view的显示以及触摸事件的传递问题。
2、那什么是window,在Android的window机制中,每个view树都可以看成一个window。
3、什么是view树?例如你在布局中给Activity设置了一个布局xml,那么最顶层的布局如LinearLayout就是view树的根,他包含的所有view就都是该view树的节点,所以这个view树就对应一个window。

每一个view树对应一个window,view树是window的存在形式,window是view树的载体,我们平时看到的应用界面、dialog、popupWindow以及上面描述的悬浮窗,都是window的表现形式。
举几个具体的例子:

  • 我们在添加dialog的时候,需要给他设置view,那么这个view他是不属于antivity的布局内的,是通过WindowManager添加到屏幕上的,不属于activity的view树内,所以这个dialog是一个独立的view树,所以他是一个window。
  • popupWindow他也对应一个window,因为它也是通过windowManager添加上去的,不属于Activity的view树。
  • 当我们使用使用windowManager在屏幕上添加的任何view都不属于Activity的布局view树,即使是只添加一个button。
    了解window机制的一个重要原因是:事件分发并不是由Activity驱动的,而是由系统服务驱动viewRootImpl来进行分发 ,甚至可以说,在框架层角度,和Activity没有任何关系。这将有助于我们对事件分发的本质理解。

小结

1、在我们手指触摸屏幕时,会产生一个触摸点信息,包括位置、压力等信息。这个触摸信息由屏幕这个硬件产生,被系统底层驱动获取,交给Android的输入系统服务:InputManagerService,也就是IMS。
2、输入系统会调用窗口管理器(WMS)的 API 来确定触摸事件应该被分发给哪个Window和对应的View。也就是说WMS提供了View信息。
3、IMS拿到WMS提供的信息,发送给对应View的ViewRootImpl。这里是InputChannel在帮忙建立SocketPair进行双向通信,有兴趣的可以查一下InputChannel相关内容,这里就不做讲解了。

在这里插入图片描述

事件如何从APP到达对应页面

第一步:分类

那么事件现在到了InputEventReceiver通知到了ViewRootImpl 看看他具体做了什么
下面是几个重要的方法:

//ViewRootImpl.java ::WindowInputEventReceiver
//1、接收到事件
final class WindowInputEventReceiver extends InputEventReceiver {
    
    
    public void onInputEvent(InputEvent event) {
    
    
        enqueueInputEvent(event, this, 0, true);
    }
}

//ViewRootImpl.java
//2、简单处理掉用doProcessInputEvents进行分类
void enqueueInputEvent(InputEvent event,
                       InputEventReceiver receiver, int flags, boolean processImmediately) {
    
    
    adjustInputEventForCompatibility(event);
    .....
    .....
    .....
    if (processImmediately) {
    
    
        doProcessInputEvents();
    } else {
    
    
        scheduleProcessInputEvents();
    }
}

//3、维护了输入事件队列
void doProcessInputEvents() {
    
    
   .....
    while (mPendingInputEventHead != null) {
    
    
        QueuedInputEvent q = mPendingInputEventHead;
        mPendingInputEventHead = q.mNext;
        deliverInputEvent(q);
    }
    ......
}

//调用InputStage责任链处理分类
private void deliverInputEvent(QueuedInputEvent q) {
    
    
    InputStage stage;
    ....
    //stage赋值操作
    ....
    if (stage != null) {
    
    
        stage.deliver(q);
    } else {
    
    
        //事件分发完成后会调用finishInputEvent,告知SystemServer进程的InputDispatcher线程,
        //最终将该事件移除,完成此次事件的分发消费。
        finishInputEvent(q);
    }
}

abstract class InputStage {
    
    
    .....
    public final void deliver(QueuedInputEvent q) {
    
    
        if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
    
    
            forward(q);
        } else if (shouldDropInputEvent(q)) {
    
    
            finish(q, false);
        } else {
    
    
            traceEvent(q, Trace.TRACE_TAG_VIEW);
            final int result;
            try {
    
    
                result = onProcess(q);
            } finally {
    
    
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            apply(q, result);
        }
    }
}

可以看到这里ViewRootImpl对时间进行了进一步的分类比如视图输入事件,输入法事件,导航面板事件等等。那么InputStage责任链具体在哪里生成的呢,具体有哪几类?
在setView方法中我们可以得到答案

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    
    
    synchronized (this) {
    
    
       ...
        mSyntheticInputStage = new SyntheticInputStage();
        InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
        InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
                "aq:native-post-ime:" + counterSuffix);
        InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
        InputStage imeStage = new ImeInputStage(earlyPostImeStage,
                "aq:ime:" + counterSuffix);
        InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
        InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
                "aq:native-pre-ime:" + counterSuffix);
     ....
    }
}

可以看到在setView方法中,就把这条输入事件处理的责任链拼接完成了。

责任链成员 作用
SyntheticInputStage 处理导航面板、操作杆等事件
ViewPostImeInputStage 视图输入处理阶段,比如按键、手指触摸等运动事件,我们常用view事件分发就发生在这个阶段
NativePostImeInputStage 本地方法处理阶段,主要构建了可延迟的队列
EarlyPostImeInputStage 输入法早期处理阶段
ImeInputStage 输入法事件处理阶段,处理输入法字符
ViewPreImeInputStage 视图预处理输入法事件阶段
NativePreImeInputStage 本地方法预处理输入法事件阶段

所以第一步是将我们的事件通过InputStage来进行分类和分发。我们的View触摸事件就发生在ViewPostImeInputStage阶段。这样我们就将事件分类到ViewPostImeInputStage了哦,兄弟们。

第二步:送去Activity

我们的View层级如下。Activity -> PhoneWindow -> DecorView
平时我们在调用setContentView绘制布局也是绘制在DecorView的ContentView中。
在这里插入图片描述
那我们就要看看事件是怎么传递到页面事件的开始Activty的。
首先我们在上面说到了,所有的事件都在ViewPostImeInputStage责任链中处理的
那先看看ViewPostImeInputStage都有什么啊,宝贝们。

//ViewRootImpl.java ::ViewPostImeInputStage 
final class ViewPostImeInputStage extends InputStage {
    
    
    .....
    .....
    private int processPointerEvent(QueuedInputEvent q) {
    
    
        final MotionEvent event = (MotionEvent)q.mEvent;
        boolean handled = mView.dispatchPointerEvent(event)
        return handled ? FINISH_HANDLED : FORWARD;
    }

    //View.java
    public final boolean dispatchPointerEvent(MotionEvent event) {
    
    
        if (event.isTouchEvent()) {
    
    
            return dispatchTouchEvent(event);
        } else {
    
    
            return dispatchGenericMotionEvent(event);
        }
    }

可以看到最后还是走到了mView.dispatchPointerEvent(event)
而ViewRootImpl中的mView就是DecorView
现在事件已经传递到了DecorView,也就是我们界面的根布局

//DecorView.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    
    
    //Callback是我们的Activity和Dialog
    final Window.Callback cb = mWindow.getCallback();
    return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
            ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

上面的cb是我们创建布局时创建的Activity
之后我们可以在Activity中可以找到我们的dispatchTouchEvent。这个大家就熟悉了吧。
就是DecorView这个大叛徒通过getCallback将时间传递给了Activity,让Activity作为页面事件的开端。

后续的传递

我们看一下Activity中dispatchTouchEvent和后续的传递

//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    
    
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    
    
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
    
    
        return true;
    }
    return onTouchEvent(ev);
}

//PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    
    
    return mDecor.superDispatchTouchEvent(event);
}

//DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
    
    
    return super.dispatchTouchEvent(event);
}

我们发现最后其实又传递回了DecorView
整个流程为ViewRootImpl ->DecorView -> Activity -> PhoneWindow -> DecorView

问题:那么问题来了为什么ViewRootImpl不直接先从Activity开始处理事件呢?
答案:ViewRootImpl并不知道有Activity这种东西存在!它只是持有了DecorView。所以,不能直接把触摸事件送到Activity.dispatchTouchEvent()

问题:那么当没有cb怎么办,也就是没有Activity。我们看到else调用了super.dispatchTouchEvent(ev);
答案:所以如果顶层的viewGroup不是DecorView,那么对调用对应view的dispatchTouchEvent方法进行分发。例如,顶层的view是一个Button,那么会直接调用Button的 dispatchTouchEvent 方法;如果顶层viewGroup子类没有重写 dispatchTouchEvent 方法,那么会直接调用ViewGroup默认的 dispatchTouchEvent 方法。

问题:为什么Activity不直接调用DecorView
答案:因为Activity没有维护DecorView,其中DecorView被PhoneWindow维护着。

小结:

1、首先事件被InputEventReceiver接收到给ViewRootImpl处理,先进行分类
2、ViewRootImpl作为
View的处理类,负责View的事件处理和管理
3、事件被分类到
ViewPostImeInputStage中,传递到mView的dispatchTouchEvent中
4、mView是布局的DecorView根布局
5、通过ViewRootImpl ->DecorView -> Activity -> PhoneWindow -> DecorView 最后到页面ViewGroup的事件分发
6、PhoneWindow在最后会有简单个讲解

页面的事件分发

DecorView继承自FrameLayout,但是FrameLayout并没有重写 dispatchTouchEvent 方法,所以调用的就是ViewGroup类的方法了。所以到这里,事件就交给ViewGroup去分发给控件树了。
当然我们的ViewGroup中包含了我们大量的View。

整个流程

结合上面的流程我们可以知道整个流程为:
ViewRootImpl -> DecorView -> Activity ->PhoneWindow -> DecorView -> ViewGroup -> View
然而我们最后在应用开发时关注的就是从Activity出发,或者从ViewGroup的事件处理。
下面将会以Activity出发为例讲解

从Activity出发

Activity ->PhoneWindow -> DecorView -> ViewGroup -> View
其中我们在页面时是不需要处理PhoneWindow -> DecorView的所以我们的流程可以简化为
Activity -> ViewGroup -> View
是不是一下子熟悉了。老生常谈的Activity -> ViewGroup -> View

事件序列

指从手指刚接触屏幕,到手指离开屏幕的那一刻结束,在这一过程产生的一系列事件,这个序列一般以down事件开始,中间含有多个move事件,最终以up事件结束

源码解析

这里简单的对源码解析一下哦,建议看一遍没多难的,也没多少代码,主要是了解流程中重要的方法。

Activity对事件的处理

//Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    
      
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    
      
            onUserInteraction();  
        }  
        if (getWindow().superDispatchTouchEvent(ev)) {
    
      
            return true;  
        }  
        return onTouchEvent(ev);  
}

从源码里可以看出,事件交给了Activity所附属的Window进行分发,返回true则结束事件分发,否则代表所有的View的onTouchEvent返回了false(均不处理),这时是由Activity的onTouchEvent来处理。

Window对事件的处理

Window的唯一实现类为PhoneWindow

//PhoneWindow.java
@Override  
public boolean superDispatchTouchEvent(MotionEvent event) {
    
      
    return mDecor.superDispatchTouchEvent(event);  
}

从源码里可以看出,事件交给了DecorView处理。我们继续看DecorView。

DecorView对事件的处理

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker{
    
    }
 
public boolean superDispatchTouchEvent(MotionEvent event) {
    
    
    return super.dispatchTouchEvent(event);
}

DecorView是继承自FrameLayout的,而大家都知道FrameLayout又继承了ViewGroup,所以下一级为ViewGroup。

ViewGroup对事件的处理

dispatchTouchEvent核心代码如下:
ViewGroup与Activity、View相比,多了一个onInterceptTouchEvent()事件拦截方法,事件传递到ViewGroup若onInterceptTouchEvent返回true,则事件由ViewGroup处理。若返回false,才会调用子View的dispatchTouchEvent

// 检查是否进行事件拦截  
final boolean intercepted;  
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    
      
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    if (!disallowIntercept) {
    
      
    //回调onInterceptTouchEvent(),返回false表示不拦截touch,否则拦截touch事件 
    intercepted = onInterceptTouchEvent(ev);  
    ev.setAction(action);
  } else {
    
      
      intercepted = false;  
     }  
} else {
    
      
   //没有touch事件的传递对象,同时该动作不是初始动作down,ViewGroup继续拦截事件  
   intercepted = true;
}

当ViewGroup的onInterceptTouchEvent返回false,会首先遍历所有的子元素,判断子元素是否能够接收点击事件。若子元素具备接收事件的条件,那么它的dispatchTouchEvent会被调用,若遍历完所有的子元素均返回false,那么只能ViewGroup自己去处理该事件。子元素的该方法返回true会终止遍历子元素。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    
    
    final boolean handled;

    // Canceling motions is a special case.  We don't need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    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的事件处理了
View无法继续向下传递事件,通过一系列的判断最后通过onTouchEvent消费事件

public boolean dispatchTouchEvent(MotionEvent event) {
    
      
boolean result = false;
//...
    if (onFilterTouchEventForSecurity(event)) {
    
      
       //noinspection SimplifiableIfStatement  
       ListenerInfo li = mListenerInfo;  
       //会执行View的OnTouchListener.onTouch这个函数,若返回true,onTouchEvent便不会再被调用了。
       //可见OnTouchListener比onTouchEvent优先级更高。
       if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED  
                    && li.mOnTouchListener.onTouch(this, event)) {
    
      
         return true; }  
  
       if (onTouchEvent(event)) {
    
      
            return true; 
       }  
}
    //…
    return result;  
}

小知识:其中判断也使用了li.mOnTouchListener.onTouch(this, event)。所以OnTouchListener比onTouchEvent优先级更高。也是在这里体现出来的。

页面分发流程

废话不多说直接上个U型流程图,老生常谈的流程图,都用烂了。
在这里插入图片描述注意:有一点不要被图所误导,ViewGroup中是没有onTouchEvent的。因为ViewGroup继承了View所以ViewGroup的onTouchEvent在View中。

dispatchTouchEvent() :分发点击事件

dispatchTouchEvent()方法是事件分发的核心方法,并且它是Activity、ViewGroup、View都实现了的方法。在事件分发中,事件首先会传递到Activity的dispatchTouchEvent()方法,该方法会根据具体情况将事件传递到父容器和子View的dispatchTouchEvent()方法中进行处理。如果事件被消耗了,就会立即停止事件传递,否则会一直传递到最后一个View中。如果View处理了此次事件则返回true否则返回false。

onInterceptTouchEvent():在ViewGroup层拦截点击事件

onInterceptTouchEvent()方法是ViewGroup中的一个方法,其主要作用是在ViewGroup拦截子View的TouchEvent,即截获子View的TouchEvent。如果ViewGroup拦截了TouchEvent,则对应的子View就接收不到TouchEvent。

onTouchEvent():处理点击事件

onTouchEvent()方法是View的一个方法,其主要作用是处理View的TouchEvent,比如说View被点击、View被拖动等。当View接收到TouchEvent时,会通过onTouchEvent()方法对TouchEvent进行响应处理。如果View处理了此次事件则返回true否则返回false。

小结

小结是结合图来的,需要对照着图来看(一定对着图看,不然看到字你会不想看)
1、首先消费代表时间到此为止不再继续传递
2、如果我们没有对控件里面的方法进行重写或更改返回值,而直接用super调用父类的默认实现,那么整个事件流向应该是从Activity---->ViewGroup—>View 从上往下调用dispatchTouchEvent方法,一直到叶子节点(View)的时候,再由View—>ViewGroup—>Activity从下往上调用onTouchEvent方法。
3、对于 dispatchTouchEvent,onTouchEvent,return true是终结事件传递。return false 是回溯到父View的onTouchEvent方法。
5、onTouchEvent return false就是不消费事件,并让事件继续往父控件的方向从下往上流动。
6、ViewGroup 想把自己分发给自己的onTouchEvent,需要拦截器onInterceptTouchEvent方法return true 把事件拦截下来。
7、View 没有拦截器,为了让View可以把事件分发给自己的onTouchEvent,View的dispatchTouchEvent默认实现(super)就是把事件分发给自己的onTouchEvent。

ACTION_MOVE 和 ACTION_UP

对于在onTouchEvent消费事件的情况:在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。
举个例子:
我们在ViewGroup2的onInterceptTouchEvent 返回true拦截此次事件并且在ViewGroup 1 的onTouchEvent返回true消费这次事件。
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
在这里插入图片描述
可以看到我们的ACTION_MOVE 和 ACTION_UP会在事件消费的ViewGroup1就不再往下传递了,直接返回给自己ViewGroup1的onTouchEvent去处理。

Cancel事件讲解

什么时候会触发ACTION_CANCEL事件呢?
首先Cancel事件是由父View通知给子View的。
触发Cancel事件有以下两种:
1、ViewGroup拦截并消费事件
2、ViewGroup中移除了View

以上两种情况要保证,Down事件是该View需要消费的情况下。
举两个例子:
例子一:ViewGroup拦截并消费事件

在 ScrollView 中,手势的优先级一般是滚动操作 > 点击操作
像Scrollview 这种可滚动控件中,如果是手指按下操作后继续滑动,会对之前点中的子控件发送一个 Cancel 事件

这是因为当手指按下时,如果用户继续向上或向下滑动,就会触发 ScrollView 的滚动操作,而 ScrollView 同时也会响应子控件的点击事件。如果用户在这时继续向上或向下滑动,就会产生一个冲突:ScrollView 想要滚动,同时子控件也想要处理点击事件。

为了避免这种冲突,ScrollView 会在用户按下并持续滑动时,向之前被点中的子控件发送一个 Cancel 事件,通知它取消掉之前的点击操作。这样一来,ScrollView 就可以顺利地滚动,而子控件也不会误操作。

也就是说ScrollView子View被ScrollView这个ViewGroup将事件拦截了。在ScrollView的源码中我们可以看到

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    
    
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
    
    
        return true;
    }

    if (super.onInterceptTouchEvent(ev)) {
    
    
        return true;
    }
    ....
    ....
    switch (action & MotionEvent.ACTION_MASK) {
    
    
        case MotionEvent.ACTION_MOVE: {
    
    
            if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
    
    
                mIsBeingDragged = true;
                mLastMotionY = y;
                initVelocityTrackerIfNotExists();
                mVelocityTracker.addMovement(ev);
                mNestedYOffset = 0;
                if (mScrollStrictSpan == null) {
    
    
                    mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
                }
                final ViewParent parent = getParent();
                if (parent != null) {
    
    
                    parent.requestDisallowInterceptTouchEvent(true);
                }
            }
            break;
        }
        ....
        ....    
    }
    return mIsBeingDragged;
}

在ACTION_MOVE的过程中,看那个If判断,我来解读一下。

如果手指在竖直方向上移动的距离大于系统默认的最小触摸距离(mTouchSlop),并且嵌套滑动轴(NestedScrolling)上竖直方向没有滚动,则将ScrollView标记为正在被拖拽(mIsBeingDragged = true)。
记录当前手指的位置(mLastMotionY = y),并初始化速度追踪器(initVelocityTrackerIfNotExists())以追踪用户的滑动速度。此外,还设置NestedYOffset为0,为处理嵌套滑动做准备。

也就是说在ACTION_MOVE在确定是滑动状态后mIsBeingDragged = true。也就是onInterceptTouchEvent的返回值为true。
上面我们讲到了onInterceptTouchEvent返回true就会把事件拦截在本层ViewGroup送去当前ViewGroup的onTouchEvent去消费。也就是将事件消费到了ScrollView。

总结:因为ScrollView在滑动时会拦截事件,所以会向子View发送了Cancel事件

例子二:ViewGroup中移除了View
这个大家自己去测试一下比较简单:

手指按在View 上,3s后将View 从ViewGroup里移除。之后观察onTouchEvent的事件返回。

viewGroup.postDelayed(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            getWindowManager().removeView(getWindow().getDecorView());
        }
    }, 3000);

常见的事件分发问题

嵌套滑动

多个滑动控件同时存在时,滑动事件可能会相互干扰,需要通过事件分发机制来解决滑动冲突问题。
我会单独对嵌套滑动单独出一篇文章来讲解,并且附带一些实战的用例。
比如:
1、ScrollView+ ListView(RecyclerView) 嵌套冲突
2、ScrollView+ ViewPager嵌套问题
3、RecyclerView + RecyclerView相同和不同方向的嵌套滑动
4、商城APP常见的主页多列表,还有吸顶的Tab

多点触控

因为多点触控可能会出现手指交错的情况,导致事件分发混乱,所以需要通过事件分发机制去处理多点触控事件。

onTouch、onTouchEvent、onLongClick、onClick之间的关系

1、onTouch的优先级高于onTouchEvent。上面在View对事件的处理中通过源码给大家说明了,因为View的dispatchTouchEvent通过onTouchListner.onTouch()进行了判断。当View的OnTouchListener.onTouch这个函数,若返回true,onTouchEvent便不会再被调用了。当然onClick和Long事件也不会再响应
2、onClick优先级最低:onClick是在onTouchEvent的方法中的下面的代码是证据
在View的onTouchEvent中可以看到 在MotionEvent.ACTION_UP中
这里由performClick()执行点击事件,其实是在设置我们的onClickListens

case MotionEvent.ACTION_UP:
        .....
        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    
    
            // This is a tap, so remove the longpress check
            removeLongPressCallback();
            if (!focusTaken) {
    
    
                .....
                if (mPerformClick == null) {
    
    
                    mPerformClick = new PerformClick();
                }
                if (!post(mPerformClick)) {
    
    
                    performClickInternal();
                }
            }
        }

点进去可以追溯的performClick()
其中我们可以看到li.mOnClickListener.onClick(this);去触发了onClick事件
所以onTouchEvent优先于onClick()

 public boolean performClick() {
    
    
        notifyAutofillManagerOnClick();
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        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;
    }

3、 onLongClick > onClick。大家都知道当onLongClick返回true了之后onClick就会被拦截住了。那怎么回事呢,看看源码:
LongClick的触发则是从ACTION_DOWN开始,由CheckForLongClick()方法完成
在View 的performClick()方法中回调了onClick()方法
在View的performLongClickInternal方法中回调了onLongClickListener的onLongClick()方法

private final class CheckForLongPress implements Runnable {
    
    
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;
        private boolean mOriginalPressedState;

        @Override
        public void run() {
    
    
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
    
    
                if (performLongClick(mX, mY)) {
    
    
                    mHasPerformedLongPress = true;
                }
            }
        }
        ...
    }

可以看到当performLongClick返回true时mHasPerformedLongPress = true;那细心的同学可能会发现我见过mHasPerformedLongPress。没错!
就是上面看到的在view的onTouchEvent中调用performClick()的if条件中。

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) {
    
    
        if (!post(mPerformClick)) {
    
    
            performClick();
        }
    }
}

也就是说performLongClick返回true。if就行不通了,就不会调用performClick(),自然onClick就不能被调用了。
4、总结:onTouch > onTuchEvent> onLongClick > onClick

Enable对View事件分发的影响

当我们对View设置了Enable = false时,那么View不会在响应我们的事件。也就是上面说到的那4个事件都不会被响应,包括onTouch。
那么事件是怎么被处理的呢? 是怎样被影响的呢。
直接上源码!!!!

public boolean dispatchTouchEvent(MotionEvent event) {
    
    
		.........
		.........
        if (onFilterTouchEventForSecurity(event)) {
    
    
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
    
    
                result = true;
            }
           
            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;
            }
        }

看到了吗?宝贝们当 (mViewFlags & ENABLED_MASK) == ENABLED = false就不会再走li.mOnTouchListener.onTouch(this, event)了。
那onTouch老大都不调用了。后面小弟还调用个屁。

这里考大家一个问题:那么对View设置了Enable = false时。事件在哪里被消费的?
留做作业了,兄弟们。

总结

没啥总结的,希望大家可以认真阅读。内容较多,可以分批阅读。关于嵌套滑动会在下一篇文章讲解。
大家发现问题一定要评论下面哦,大家共同进步。

猜你喜欢

转载自blog.csdn.net/weixin_45112340/article/details/130863139