Android 输入事件一撸到底之View接盘侠(3)

前言

系列文章

1、Android 输入事件一撸到底之源头活水(1)
2、Android 输入事件一撸到底之DecorView拦路虎(2)
3、Android 输入事件一撸到底之View接盘侠(3

image.png
前两篇文章分别分析了输入事件分发到App层以及DecorView对输入事件的处理,最终交给ViewTree处理。我们平时对事件的处理大部分集中在对ViewTree的处理上,网上绝大部分的文章也是针对此分析,为了将输入事件连贯起来,从总体看局部,由局部推总体,接下来分析ViewTree的事件分发。
通过本篇文章,你将了解到:

1、View/ViewGroup/ViewTree 易混淆之处
2、ViewGroup 事件分发
3、View 事件分发
4、ViewTree 事件分发
5、事件分发系列总结

一个小比喻

对代码调用流程比较疑惑的话,我们做个简单的比喻:
一个学校有3个年级,每个年级有1个年级主任、3个班,每个班有个班主任、有若干个学生,其中一个学生叫小明

有一天,校长接到了个任务:有个日本的女学生:石原里美要来学校做交流。
校长将这个任务指派下去,先找到1年级主任问你们年级要接收这个学生不?年级主任想到手底下还有几个班,就先问1班班主任你们能接收吗,1班主任想这么多学生我问问谁能带带这个石原里美,就先问小明。这个过程叫做dispatchTouchEvent。
小明接到任务,发现手底下没人了(自己是View,班主任、年级主任、校长是ViewGroup),就只能自己硬着头皮看看这石原里美资料,这时候他有两个选择:接受/拒绝
小明看了资料发现石原里美是个小美女,于是开心选择了接受,那么就答应班主任,班主任将结果告诉年级主任,年级主任告诉校长,校长长叹一口气,终于将包袱甩掉了。。这个过程就是dispatchTouchEvent 回传结果。
某天石原里美的同班男同学:松井 听说石原里美在中国玩得挺开心的,自己也想来。校长架不住,只能答应。校长知道石原里美在1年级里做交流,想想松井和石原里美是同学,在一起更好交流,于是直接就将这个任务交给了1年级主任,1年级主任知道石原里美在1班,于是直接交给了1班,1班主任知道小明接待过石原里美,就让小明陪松井(石原里美是Down事件,松井是后续的Move、Up事件),小明心里一万只草泥马,谁叫自己冲动了呢(自己xxx,跪着也要完成)。这个过程就是某布局一旦处理了Down事件,那么后续的事件都会通知给它。
时光回到过去,小明是个刚正不阿的学生,表示自己学习很忙,书中自有黄金屋,书中自有颜如玉,没时间陪伴石原里美,这时他告诉班主任他不接这个任务,班主任一看,班里没人愿意接这活儿了,幸好还有B计划,就先看看自己能完成这个任务不。如果不能完成,还是交给领导来做吧,交给了年级主任,年级主任暗自庆幸自己也有B计划,发现自己B计划能完成,于是就亲自带石原里美了。校长知道年级主任接收任务了,很开心,自己的B计划终于没有摆上台面。这个B计划就是onTouchEvent
当然,松井同学最后也交给了年级主任带。。
中间有个插曲,班主任看了石原里美资料,心里有个大胆的想法:自己的儿子和她差不多年级,多交流一下提高儿子的外语水平,多好啊。于是当他从年级主任那收到这个任务的时候,就不把这个任务发下去了,告诉年级主任自己可以处理。这个过程就是 onInterceptTouchEvent
当然小明也不是没机会陪伴石原里美,学校之前制定了规矩:任何人都可以禁止上级不将任务派给自己(领导不能私自将任务拦截了,至少得通知下级),小明使用了这条规定,班主任的大胆想法碎了一地。。当然这条规定一视同仁,包括年级主任、校长都要遵守。
这个过程就是 requestDisallowInterceptTouchEvent

View/ViewGroup/ViewTree 易混淆之处

父类/子类、父布局/子布局

网上一些文章在画关系图的时候没有将两者区分开,容易让刚接触此内容的人混淆。先看看View、ViewGroup定义:

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

public abstract class ViewGroup extends View implements ViewParent, ViewManager {}

可以看出ViewGroup 继承自View,也即是ViewGroup是View的子类,View是ViewGroup的父类。
父类/子类关系是语言范畴的关系:
子类访问父类的方法:

super.doMethod(xx)

再来看看常见的布局文件内容:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:background="@color/colorGreen"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    tools:context=".MainActivity">
    <View
        android:background="@color/colorAccent"
        android:layout_width="100dp"
        android:layout_height="100dp">
    </View>
</FrameLayout>

FrameLayout是ViewGroup子类,该布局文件里,FrameLayout是父布局,View是子布局。从ViewGroup的命名可以看出,ViewGroup是View的集合,当然ViewGroup也可以是ViewGroup的集合(嵌套)。
父布局/子布局关系是ViewGroup/View 里定义的。
子布局寻找父布局

    public final ViewParent getParent() {
        return mParent;
    }
  • ViewParent 是个接口,ViewGroup、ViewRootImpl 都实现了它
    获取到mParent后需要强转为对应类型
  • 每当将子布局添加到父布局里的时候,就给子布局指定其父布局,也就是给mParent赋值 (assignParent(xx))
  • RootView(如DecorView)的mParent指向ViewRootImpl (setView(xx) 里指定)

父布局寻找子布局
父布局通过addView(xx)方法将子布局添加到一维数组里,因此父布局寻找其子布局也即是访问该组数的过程:

    public View getChildAt(int index) {
        if (index < 0 || index >= mChildrenCount) {
            return null;
        }
        //private View[] mChildren;
        return mChildren[index];
    }

ViewTree 建立

了解父布局、子布局关系,将子布局添加到父布局里,这是最简单的ViewTree结构。再将父布局作为另一个父布局的子布局添加,那么ViewTree又增加了一层。如此反复,最终形成的ViewTree 如下:
image.png

注意:ViewTree并不是一个类,仅仅只是为了方便描述View/ViewGroup构成的布局层次而命名的。

由此可知:

  • View是ViewGroup父类
  • View只能作为子布局,不能作为父布局
  • ViewGroup既可做父布局,也可做子布局

厘清了上述概念,接下来进入正题。

ViewGroup 事件分发

回顾上篇文章内容:

1、DecorView 将事件传递给Activity处理,如果Activity没有处理,那么传递到Window进而传递到DecorView
2、DecorView 调用父类的dispatchTouchEvent(xx)方法继续分发事件

可以看出,此处重点是父类的dispatchTouchEvent(xx)方法。
DecorView继承自FrameLayout,在FrameLayout里寻找该方法,发现FrameLayout并没有重写该方法。而FrameLayout继承自ViewGroup,在ViewGroup里找到了dispatchTouchEvent(xx)。因此DecorView最终调用的是ViewGroup的dispatchTouchEvent(xx)方法。

一个小例子

image.png
如上图所示,ViewGroup里4个子布局,添加顺序如下:

addView(View1)
addView(View2)
addView(View3)
addView(View4)

View1、View2、View3相交于 “1"的位置
View3、View4相交于"2"的位置
当分别点击"1"、“2” 位置时,事件时怎么传递的呢?
先说结论:
当点击"1” 位置时:

1、ViewGroup 首先收到事件,并查找1位置是否落在某个子布局之内
2、ViewGroup 有4个子布局,倒序遍历寻找,也就是View4->View1的顺序寻找
3、先判断View4,"1"不在View4内,继续寻找
3、然后找到View3,判断View3是否想处理该事件,如果处理,那么事件不再传递给View1、View2;如果不接收,继续判断View2;
4、View2不处理,继续判断View1。
5、当View3、View2、View1 都不处理事件,那么只能交给他们的父布局ViewGroup。
6、当ViewGroup自身也不想处理,那么退回给它的父布局,其父布局的操作和ViewGroup对事件的分发一样的原理。

当点击"2" 位置时,与点击"1" 位置类似,不再赘述。
实际上上述事件分发的流程就是由ViewGroup dispatchTouchEvent(xx)方法完成,来看看它的源码:

ViewGroup dispatchTouchEvent(MotionEvent ev)

ViewGroup.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        //标记该事件是否已处理
        boolean handled = false;
        //如果该View没有被遮挡,那么可以接收事件
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                //如果是Down事件,表明是一次事件序列的开始
                //清空之前的状态
                //清空touchTarget链表
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            //标记是否拦截该事件
            final boolean intercepted;
            //两个条件满足一个即可
            //1、是Down事件 2、mFirstTouchTarget != null 表示有子布局处理了Down事件,也就是子布局在收到Down事件时返回了true
            //mFirstTouchTarget -> 指向链表头
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //查看标记位:自己能否被允许拦截事件 disallowIntercept=true 表示不被允许拦截事件------(1)
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //能够拦截事件,调用ViewGroup onInterceptTouchEvent 进行拦截------(2)
                    //返回值表示是否已处理该事件
                    intercepted = onInterceptTouchEvent(ev);
                    //恢复事件防止之前中途修改过
                    ev.setAction(action);
                } else {
                    //不允许拦截,则肯定未处理
                    intercepted = false;
                }
            } else {
                //如果不是Down事件且也没有任何子布局处理过Down事件,则表示ViewGroup已经拦截处理该事件
                intercepted = true;
            }
            ...
            //记录处理了Down事件的链表
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //事件未取消且没被拦截处理
            if (!canceled && !intercepted) {
                ...
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex();
                    //单指、多指Down事件,鼠标事件
                    //ViewGroup 直接子布局个数
                    final int childrenCount = mChildrenCount;
                    //有子布局且还未有任何子布局处理过事件
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        //根据绘制顺序生成接收事件的子布局列表,一般都忽略
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //倒序寻找子布局,addView(xx1) addView(xx2)是正序
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //先确定待检测的子布局
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            ...
                            //child.canReceivePointerEvents() -> 能否接收事件,也就是子布局是否可见 Visible 不可见无法接收事件
                            //isTransformedTouchPointInView() -> 检测当前点击的点是否落在子布局内,检测时候考虑了子布局的padding/scroll/matrix 对位置的影响
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                //不满足条件,则跳过该子布局,继续寻找另一个布局
                                continue;
                            }
                            //上述条件满足了,检测该子布局是否已经处理过该Down事件
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                //处理过直接跳出循环,不用再找下一个子布局了
                                break;
                            }
                            //该子布局还未处理过Down事件
                            //将事件分发给子布局->child ------(3)
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                //返回true->子布局已经处理了该Down事件
                                mLastTouchDownTime = ev.getDownTime();
                                ...
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //将子布局构成的TouchTarget挂到链表头(链表节点表示处理了Down事件的子布局)
                                //mFirstTouchTarget 指向当前链表头(也即是mFirstTouchTarget有值了,重要!)
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                //标记Down事件已经找到处理者了
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            ...
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                    ...
                }
            }

            //没找到任何处理了Down事件的子布局
            if (mFirstTouchTarget == null) {
                //因为没有找到任何处理了Down事件的子布局,因此事件分发是目标布局填:null ------(4)
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                //找到
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        //Down事件,之前已经处理过了,这里直接标记位已吹李
                        handled = true;
                    } else {
                        //Down之外的其它事件,如Move Up等
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //target.child 为目标子布局,分发给它处理 ------(5)
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            //如返回true,标记为已处理
                            handled = true;
                        }
                        if (cancelChild) {
                            //如已取消,则跳过当前继续寻找下一个节点
                        }
                    }
                    //继续寻找下一个需要处理的节点 一般来说该链表通常只有一个节点
                    predecessor = target;
                    target = next;
                }
            }
            ...
        }
        ...
        //最终返回dispatchTouchEvent(xx)该方法对事件处理结果
        //true->已处理 false->未处理 ------(6)
        return handled;
    }

注意,为了理解方便,上面以子布局代替子View阐述,子布局可以是View也可以是ViewGroup
上边注释比较比较清晰了,列出了(1)~(6)比较重要的点:
(1)
禁止拦截标记:

ViewGroup.java
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            //已设置过了,无需再次设置
            return;
        }
        //设置标记位
        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
        if (mParent != null) {
            //递归调用父类,直至ViewRootImpl
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

该方法为ViewGroup独有,若某个子布局不想让其父布局拦截其事件序列,那么调用getParent().requestDisallowInterceptTouchEvent(true)即可。该方法一直往上追溯设置父布局,也就是子布局之上的所有层次的父布局不拦截事件

(2)
只有ViewGroup 有onInterceptTouchEvent(xx)方法,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;
    }

如果不重写onInterceptTouchEvent(xx)方法,默认不拦截事件。当重写该方法进行拦截的时候需要注意:

onInterceptTouchEvent(xx)拦截到Down事件后,如果此时没有任何子布局处理Down事件,那么后续的Move、Up等事件onInterceptTouchEvent(xx) 将不会收到,也就是onInterceptTouchEvent不执行
一般很少重写ViewGroup dispatchTouchEvent(xx),处理事件使用onInterceptTouchEvent(xx) + onTouchEvent(xx) 处理

(3)
将事件分发给子布局

ViewGroup.java
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                  View child, int desiredPointerIdBits) {
        //child 指的是要接收事件的子布局
        final boolean handled;
        ...
        if (child == null) {
            //如果没有子布局接收Down事件,那么Down/Move/Up事件直接调用父类处理方法
            //ViewGroup 父类就是View 因此调用的是View的dispatchTouchEvent(xx)方法
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            //有子布局接收Down事件
            //计算子布局位置偏移
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            //将Event 位置偏移,使它落在子布局内
            transformedEvent.offsetLocation(offsetX, offsetY);
            //考虑matrix 对位置的影响
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }
            //Event位置调整后,它的位置是基于当前子布局的左上角为原点偏移的
            //继续分发给子布局 child可能为View也可能为ViewGroup,如果是ViewGroup那么又重新走到了其父布局的分发逻辑
            //继续递归分发,直至返回
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        //处理结果
        return handled;
    }

1、该方法递归派发事件
2、MotionEvent修改的是AXIS_X、AXIS_Y值,也就是Event.getX()、Event.getY()取得的值,这也就是为什么这两个值是距离当前View左上角的原因

(4)
此处方法

handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
传入的是null,因此交给View dispatchTouchEvent(xx)处理

(5)
当有子布局处理了Down事件,那么后续的Move、Up等事件传递过来后,直接派发给当初处理了Down事件的子布局。
由此可以看出:

1、Down事件是事件序列(Down->Move->Up)的开始,如果某个布局没有处理Down事件,那么后续的事件将收不到
2、如果某个布局处理了Down事件,那么即使父布局拦截(onInterceptTouchEvent(xx))了事件,子布局依然能够收到完整的事件序列

(6)
最终事件的处理结果体现在一个布尔值上。
true -> 表示该事件已处理
false -> 表示该事件未处理
需要注意的是:

不管是否对事件"真正处理",只要返回true,就告诉调用者该事件已处理。即使对事件做了"很多处理",返回false,就告诉调用者该事件未处理。

上面分析了一堆可能比较混淆,用图表示ViewGroup 分发事件的过程:
image.png

ViewGroup 事件分发常用方法

image.png
以上,分析了ViewGroup分发事件的逻辑,接下来看看事件分发到View时如何处理。

View 事件分发

从ViewGroup分发逻辑可以看出:ViewGroup分发事件的过程就是递归查找子布局并分发。那么递归结束的条件是什么呢?

1、有子布局处理了该事件,最终调用View.dispatchTouchEvent(xx)
2、没有子布局处理该事件,只能自己接收(不一定处理),此时调用super.dispatchTouchEvent(xx),也就是View.dispatchTouchEvent(xx)

由此可知,ViewGroup分发事件最终需要交给View.dispatchTouchEvent(xx)处理。

View dispatchTouchEvent(MotionEvent ev)

    public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            //停止滑动
            stopNestedScroll();
        }
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //如果注册了onTouch回调,则执行onTouch方法
            //如果该方法返回true,则认为该事件已经处理了
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //事件未处理,则调用onTouchEvent处理
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ...
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }
        return result;
    }

onTouch通过以下方法注册:

    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

如果onTouch(xx)处理了事件,那么onTouchEvent(xx)就不会调用

onTouchEvent(MotionEvent event)

    public boolean onTouchEvent(MotionEvent event) {
        //获取点击坐标
        final float x = event.getX();
        final float y = event.getY();
        //View控制标记,根据标记内容控制View的属性
        final int viewFlags = mViewFlags;
        //点击动作->Down/Move/Up等
        final int action = event.getAction();
        //如果该View设置了:可以单击、可以长按、鼠标右键弹出(很少用)中的一个
        //那么认为该View可以点击-------(1)
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            //View默认是ENABLED,若是DISABLED,则返回clickable
            return clickable;
        }
        if (mTouchDelegate != null) {
            //用在子布局扩大其点击区域使用,当点击坐标位于子布局之外时,通过该方法判断点击坐标是否位于子布局的"扩大区域内"
            //若是则将事件交给子布局处理---------(2)
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        //View可点击或者鼠标移动悬浮显示(很少用)
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    ...
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    //在Down事件里已经处在按下状态
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        ...
                        //如果处在焦点获取状态但又未获得焦点,则主动申请焦点
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
                        //mHasPerformedLongPress 表示长按事件是否已经处理了事件
                        //如果已经处理了,则单击事件不会执行
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            //长按事件还没来得及处理,此处将长按事件移除
                            removeLongPressCallback();
                            //如果上一步是请求获取焦点并成功了,则这一次不处理后续事件
                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    //实现Runnable接口的类,用于回调
                                    mPerformClick = new PerformClick();
                                }
                                //为了给View更新其他状态留够时间,此处是通过Handler发送到主线程执行------(3)
                                if (!post(mPerformClick)) {
                                    //如果不成功,则直接调用
                                    performClickInternal();
                                }
                            }
                        }
                        ...
                    }
                    mIgnoreNextUpEvent = false;
                    break;
                case MotionEvent.ACTION_DOWN:
                    ...
                    boolean isInScrollingContainer = isInScrollingContainer();
                    if (isInScrollingContainer) {
                        ...
                        //在可滚动的容器内,为了容错,延迟点击
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        //设置按下的状态,多用于按下时背景/前景等变化
                        setPressed(true, x, y);
                        //开启一个长按延时事件,当延时事件到了就执行该事件(长按事件)
                        //ViewConfiguration.getLongPressTimeout() 就是长按的时间阈值。不同系统可能不一样
                        //我手机上是400ms,缺省值是500ms----------(4)
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    ...
                    break;
                case MotionEvent.ACTION_MOVE:
                    ...
                    break;
            }
            //只要是clickable=true 则认为已经处理了该事件
            return true;
        }
        return false;
    }

和ViewGroup分析类似,上边注释比较比较清晰了,列出了(1)~(4)比较重要的点:
(1)
CLICKABLE/LONG_CLICKABLE 相关
赋值操作:

View.java
    public void setClickable(boolean clickable) {
        setFlags(clickable ? CLICKABLE : 0, CLICKABLE);
    }
    public void setLongClickable(boolean longClickable) {
        setFlags(longClickable ? LONG_CLICKABLE : 0, LONG_CLICKABLE);
    }

当前也可以在XML里指定属性。你可能比较疑惑,一般我们都不用设置上面的属性,View.onTouchEvent(xx)依然能够执行,咋回事呢?这得要分两种情况:

1、View CLICKABLE/LONG_CLICKABLE 属性默认是没有设置的,比如TextView就没设置CLICKABLE,但是Button在其默认属性了设置了CLICKABLE
2、不管有没有设置上述属性,只要调用了View.setOnClickListener(xx)/View.setOnLongClickListener,这俩方法内部分别调用了View.setClickable(xx)/View.setLongClickable(xx)方法

(2)
通常来说,内容区域不变,扩大View的点击区域有两种方法:

1、设置padding
2、设置TouchDelegate

简单说说TouchDelegate,顾名思义,Touch事件代理。使用方法如下:

        //view为待扩大的点击区域
        view.post(new Runnable() {
            @Override
            public void run() {
                Rect areaRect = new Rect();
                //获取原本的区域
                view.getHitRect(areaRect);
                //将区域扩大
                areaRect.left -= 100;
                areaRect.top -= 100;
                areaRect.right += 100;
                areaRect.bottom += 100;
                View parentView = (View)view.getParent();
                //设置代理,当点击坐标落在areaRect之内时,事件优先交给view处理
                TouchDelegate touchDelegate = new TouchDelegate(areaRect, view);
                //给父布局设置代理,事件流转:子布局的onTouchEvent->父布局的onTouchEvent->落在扩大的区域内->将坐标值更改->子布局的dispatchTouchEvent->子布局onTouchEvent
                parentView.setTouchDelegate(touchDelegate);
            }
        });

(3)
PerformClick
最终调用到

    public boolean performClick() {
        ...
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            //声音反馈
            playSoundEffect(SoundEffectConstants.CLICK);
            //熟知的onClick
            li.mOnClickListener.onClick(this);
            //只要执行了onClick,就认为已经处理了事件
            result = true;
        } else {
            result = false;
        }
        ...
        return result;
    }

我们平时给View设置的点击事件:

 public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

就是此时回调的。
(4)
来看看长按事件
最终调用CheckForLongPress里的Run方法

    public void run() {
        if ((mOriginalPressedState == isPressed()) && (mParent != null)
                && mOriginalWindowAttachCount == mWindowAttachCount) {
            recordGestureClassification(mClassification);
            if (performLongClick(mX, mY)) {
                //记录长按事件已经处理了
                mHasPerformedLongPress = true;
            }
        }
    }
    private boolean performLongClickInternal(float x, float y) {
       sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            //执行 setOnLongClickListener(xx) 注册的回调
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        ...
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }

长按和短按的区别

  • 重写onLongClick(View v),其返回值决定是否执行onClick(View v)方法,若是返回true表示已经处理长按事件,短按无需处理了
  • 重写onClick(View v),该方法没有返回值,执行了该方法就不会执行onLongClick(View v)

用图表示View事件分发流程:
image.png

View 事件分发常用方法

image.png
值得注意的是onTouch(xx)回调和短按操作区别:

短按是在收到Up事件后触发的,而onTouch(xx)则是只要收到事件就会触发

ViewTree 事件分发

以上分别分析了ViewGroup、View的事件分发流程,而众多ViewGroup、View组成了ViewTree结构。将父、子布局的dispatchTouchEvent、onTouchEvent关联起来,如图:
image.png
从中可以看出:

  • 若是父布局dispatchTouchEvent处理了事件,那么子布局的dispatchTouchEvent将收不到事件
  • 若是子布局的onTouchEvent处理了事件,那么父布局的onTouchEvent将收不到事件

ViewGroup/View 事件分发难点在:

弄清楚子布局/父布局、View/ViewGroup继承关系

事件分发系列总结

Android事件分发系列文章分了三篇来讲述
Android 输入事件一撸到底之源头活水(1)
分析了App层从底层收到事件后ViewRootImpl.java的处理

Android 输入事件一撸到底之DecorView拦路虎(2)
分析了DecorView对事件的处理

Android 输入事件一撸到底之View接盘侠(3)
前面事件没有处理,流转到此处进行处理后就完成了使命,这也就是为什么本篇文章叫做"View接盘侠的原因"

网上很多文章将DecorView与View/ViewGroup事件处理一起讲解,没有明确指出两者之间的差异。通过本系列文章,我们知道DecorView对事件的处理并不是必须的,只有使用了DecorView作为RootView才特殊处理(比如Activity、Dialog等)。
将三者串联起来:
image.png
一般来说,我们常接触到的就是第二部分、第三部分,尤其是第三部分常用。
建议将这三部分对应的文章结合起来看,局部---->整体---->局部,这样对整个事件分发有个更清晰的认识。
本文基于 Android 10.0 源码

如果您喜欢,请点赞,您的鼓励是我前进的动力。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值