IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android Input系统7 View的事件分发 -> 正文阅读

[移动开发]Android Input系统7 View的事件分发

一 概述

从上一篇文章 Android Input系统6 事件处理全过程 中,我们知道当输入事件从 InputDispatcher 通过 server 端的 InputChannel 发送给应用程序的 client 端,其实最终事件被发送到了 java 层的 ViewRootImpl 中。

对于 java 层的事件分发从 ViewRootImpl.java 的 deliverInputEvent 开始,代码如下:

private void deliverInputEvent(QueuedInputEvent q) {
        Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
                q.mEvent.getSequenceNumber());
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
        }

        InputStage stage;
        if (q.shouldSendToSynthesizer()) {
            stage = mSyntheticInputStage;
        } else {
            stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
        }

        if (q.mEvent instanceof KeyEvent) {
            mUnhandledKeyManager.preDispatch((KeyEvent) q.mEvent);
        }

        if (stage != null) {
            handleWindowFocusChanged();
            stage.deliver(q);
        } else {
            finishInputEvent(q);
        }
    }

ViewRootImpl 类定义了多种类型的 InputStage,以责任链模式进行处理分发输入事件。

至于事件是如何通过 InputStage 传递到应用窗口的,我们在上篇文章中并没有讨论,现在在这篇文章中我们来研究这个问题。

二 InputStage

2.1 InputStage说明

Android 应用程序对输入系统的处理分为多个阶段,我们把这些阶段称为 InputStage

ViewRootImpl 中定义了多种类型的 InputStage,如下:

final class SyntheticInputStage extends InputStage{}
final class ViewPostImeInputStage extends InputStage{}
final class NativePostImeInputStage extends AsyncInputStage{}
final class EarlyPostImeInputStage extends InputStage{}
final class ImeInputStage extends AsyncInputStage{}
final class ViewPreImeInputStage extends InputStage{}
final class NativePreImeInputStage extends AsyncInputStage{}

这里对各个 InputStage 进行简要说明:

InputStage说明
NativePreImeInputStage分发早于 IME 的 InputEvent 到 NativeActivity 中去处理,NativeActivity 和普通 acitivty 的功能一致,不过是在 native 层实现,这样执行效率会更高,同时 NativeActivity 在游戏开发中很实用(不支持触摸事件)
ViewPreIMEInputStage分发早于 IME 的 InputEvent 到 View 框架处理,会调用 view(输入焦点)的 onkeyPreIme 方法,同时会给 View 在输入法处理 key 事件之前先得到消息并优先处理,View 系列控件可以直接复写 onKeyPreIme( 不支持触摸事件)
ImeInputStage分发 InputEvent 到 IME 处理,调用 ImeInputStage 的 onProcess,InputMethodManager 的 dispatchInputEvent 方法处理消息(不支持触摸事件)
EarlyPostImeInputStage与 touchmode 相关,比如你的手机有方向键,按方向键会退出 touchmode,这个事件被消费,有可能会有 view 的背景变化,但不确定(支持触摸事件)
NativePostImeInputStage分发 InputEvent 事件到 NativeActivity,IME 处理完消息后能先于普通 Activity 处理消息(此时支持触摸事件)
ViewPostImeInputStage分发 InputEvent 事件到 View 框架,view 的事件分发(支持触摸事件)。最终会调用到输入焦点的3个方法:使用 setKeyListener 注册的监听器的 onKey ,之后是 onKeyDown 和 onKeyUp,或者调用 activity 的 onKeyDown 和 onKeyUp 方法,也就是兜底处理无人处理的 key 事件
SyntheticInputStage未处理 InputEvent 最后处理

InputStage 将输入事件的处理分成若干个阶段(Stage),如果当前有输入法窗口,则事件处理从 NativePreIme 开始,否则从 EarlyPostIme 开始,事件依次经过每个 Stage,如果该事件没有被标识为 “Finished”, 该 Stage 就会处理它,然后返回处理结果 Forward 或 Finish,Forward 运行下一个 Stage 继续处理,而 Finished 事件将会简单的 Forward 到下一级,直到最后一级 SyntheticInputStage。

这里使用一张图来描述 InputStage 的处理流程,如下所示:
在这里插入图片描述
责任链模式的各个 InputStage 从整体流程如上来说,简要归纳如下:

  • 输入法之前的处理
  • 输入法处理
  • 输入法之后处理
  • 综合处理

2.2 InputStage初始化

在 ViewRootImpl 的 setView 方法中完成了 InputStage 的初始化操作:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ......
                // Set up the input pipeline.
                CharSequence counterSuffix = attrs.getTitle();
                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);

                mFirstInputStage = nativePreImeStage;
                mFirstPostImeInputStage = earlyPostImeStage;
                mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;
         }
     }
}

在上篇文章中我们知道,最终执行的是 mFirstPostImeInputStage 的 deliver 方法。我们来看下 InputStage 的 deliver:
ViewRootImpl#InputStage

2.3 InputStage.deliver

public final void deliver(QueuedInputEvent q) {
        if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
            forward(q); // 是否带有 FLAG_FINISHED 的 flag,如有则将事件分发给 mNext
        } else if (shouldDropInputEvent(q)) {
            finish(q, false);
        } else {
            // 实际处理事件
            apply(q, onProcess(q));
        }
}

protected void forward(QueuedInputEvent q) {
     onDeliverToNext(q);
}


protected void onDeliverToNext(QueuedInputEvent q) {
     if (DEBUG_INPUT_STAGES) {
         Log.v(mTag, "Done with " + getClass().getSimpleName() + ". " + q);
     }
     if (mNext != null) {
         mNext.deliver(q); // 继续执行下一个 mNext
     } else {
         finishInputEvent(q);
     }
}

protected void apply(QueuedInputEvent q, int result) {
            if (result == FORWARD) {
                forward(q);
            } else if (result == FINISH_HANDLED) {
                finish(q, true);
            } else if (result == FINISH_NOT_HANDLED) {
                finish(q, false);
            } else {
                throw new IllegalArgumentException("Invalid result: " + result);
            }
}

从以上代码可以看到,mFirstPostImeInputStage 的 deliver 函数,最终会执行到 ViewPostImeInputStage 的 onProcess:

2.4 ViewPostImeInputStage.onProcess

/**
     * Delivers post-ime input events to the view hierarchy.
     */
    final class ViewPostImeInputStage extends InputStage {
        public ViewPostImeInputStage(InputStage next) {
            super(next);
        }

        @Override
        protected int onProcess(QueuedInputEvent q) {
            if (q.mEvent instanceof KeyEvent) {
                return processKeyEvent(q);
            } else {
                final int source = q.mEvent.getSource();
                if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                    return processPointerEvent(q);
                } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                    return processTrackballEvent(q);
                } else {
                    return processGenericMotionEvent(q);
                }
            }
        }
  }

onProcess 方法用于输入事件的具体处理,此方法中将按键事件和触摸事件分开,本篇文章主要研究触摸事件的流程,对于触摸事件,根据事件源分了三个方法,普通的触摸事件由 processPointerEvent 处理。

2.5 ViewPostImeInputStage.processPointerEvent

private int processPointerEvent(QueuedInputEvent q) {
     final MotionEvent event = (MotionEvent)q.mEvent;
     ......
     boolean handled = mView.dispatchPointerEvent(event);
     ......
     return handled ? FINISH_HANDLED : FORWARD;
}

mView 指向当前界面的顶层布局 DecorView,handled 作为此次事件分发的结果返回,DecorView 并没有重写父类的 dispatchPointerEvent 方法,所以接着到 View 中看此方法实现。

至此事件分发中的 InputStage 阶段分析完毕。接下来就是 View 的事件分发了。

三 View事件分发

3.1 View.dispatchPointerEvent

@UnsupportedAppUsage
    public final boolean dispatchPointerEvent(MotionEvent event) {
        if (event.isTouchEvent()) {
            return dispatchTouchEvent(event);
        } else {
            return dispatchGenericMotionEvent(event);
        }
    }

针对 Touch 事件又会调 dispatchTouchEvent 方法,此方法在 DecorView 中有重写:

3.2 DecorView.dispatchTouchEvent

DecorView.java

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

这里的 Window.Callback 指向当前 Activity,在 Activity 启动时会调用自己的 attach 方法,此方法中会将自己作为 callback 传给 window,

Activity.java

final void attach(......){
    ......
    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
	mWindow.setCallback(this); // 这里的 this 就是 Activity
	......
}

所以 mWindow.getCallback() 获取的就是当前的 Activity,接着会调用 Activity 的 dispatchTouchEvent 进一步分发事件:

3.3 Activity.dispatchTouchEvent

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

onUserInteraction 在 Activity 中是个空方法,它的作用应该是告诉开发者此 Activity 接收到事件了。

接着就会调用 PhoneWindow 的 superDispatchTouchEvent 方法,其返回值代表目标 View 对事件的处理结果,我们可以看到如果目标 View 没有消费掉此次事件(即返回值为 false)则会调用 Activity 的 onTouchEvent 来处理。

3.4 PhoneWindow.superDispatchTouchEvent

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

3.5 DecorView.superDispatchTouchEvent

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

这里调用 super.dispatchTouchEvent 最后到了 ViewGroup 中,此时才真正开始 View 的事件分发,我们可以看到输入事件从 native 层到 ViewRootImpl 之后会经过:DecorView -> Activity -> PhoneWindow -> DecorView -> ViewGroup 的流程。

ViewGroup 可以理解为 View 事件分发的起始点,事件分发机制其实分为两部分,一部分为事件的下发,一部分为事件处理结果的上报。

事件下发的目的是找到处理该事件的目标 View,事件上报的目的是将最终的处理结果返回给 ViewPostImeInputStage:

ViewPostImeInputStage.processPointerEvent

private int processPointerEvent(QueuedInputEvent q) {
     final MotionEvent event = (MotionEvent)q.mEvent;
     ......
     boolean handled = mView.dispatchPointerEvent(event);
     ......
     return handled ? FINISH_HANDLED : FORWARD;
}

即上述方法中的 handled 就是事件处理上报的最终结果,有了这个结果就可以确定此次事件是朝着 InputStage 责任链的下一环继续分发还是就此结束。

3.6 ViewGroup.dispatchTouchEvent

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
  		......
  		boolean handled = false;
  		// 判断此次触摸事件是否被过滤掉,条件由两个 flag 决定,FILTER_TOUCHES_WHEN_OBSCURED
  		// 和 MotionEvent.FLAG_WINDOW_IS_OBSCURED,这两个 flag 用来表示
  		// 当前接收触摸事件的 View 是否被遮挡或者隐藏,只有未被遮挡或隐藏才能
  		// 进一步处理事件。
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 当 ACTION_DOWN 事件到来时,会清除并重置之前设置的各种状态,
                // 这是因为 Android 的事件分发是以一个事件序列为单位进行分发
                // 和拦截,当一个此次事件为 ACTION_DOWN 则表明这是一个新的
                // 事件序列,所以需要清空和重置上一个事件序列的状态
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 代表此次事件是否被拦截
            final boolean intercepted;
            // 两种情况下会走进事件拦截的流程,1.此次事件为 ACTION_DOWN,
            // 2.mFirstTouchTarget != null,mFirstTouchTarget 描述的是
            // 接收此次事件的目标
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                // 子 View 是否有请求过父 View 不要拦截事件,通过调用父 View 的
                // requestDisallowInterceptTouchEvent 方法来请求
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    // 如果子 View 没有请求 View 不要拦截事件,则走正常
                    // 父 View 事件拦截流程
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // 对于非 ACTION_DOWN 事件并且 mFirstTouchTarget 为空则直接拦截
                // 此次事件序列的后续事件
                intercepted = true;
            }

            ...

            // 检查是否需要取消事件,由 resetCancelNextUpFlag 或者事件本身
            // 决定,resetCancelNextUpFlag 的实现很简单,对于添加了
            // PFLAG_CANCEL_NEXT_UP_EVENT 的 View 清空此状态并返回 true,
            // 这个 PFLAG_CANCEL_NEXT_UP_EVENT 的含义从注释字面意思看
            // 是 View 和 ViewGroup 处于分离状态
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
            
            	......
            // 记录接收事件的目标
            TouchTarget newTouchTarget = null;
            // 记录是否成功分发事件的状态值
            boolean alreadyDispatchedToNewTouchTarget = false;
            // 如果事件没有被取消并且没有被拦截
            if (!canceled && !intercepted) {
                ...
                // ACTION_POINTER_DOWN 和 ACTION_HOVER_MOVE 是多点触控与
                // 鼠标相关的事件本篇不讨论,这里只看 ACTION_DOWN 会走进如下分支
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                        
                    // 多点触控相关的逻辑,省略...

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x =
                                isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                        final float y =
                                isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                        // 这里会将 ViewGroup 所有的子 View 重新以 Z-order 的顺序
                        // 从小到大排列,返回一个 list
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        // 子 View 是否自定义绘制顺序,一般情况都为 false
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        // 对所有子 View 进行遍历
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            // 对于没有自定义子 View 绘制顺序的情况,
                            // childIndex 就等于i
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            
                            // 这里会从前面得到的根据 Z-order 排好序的 preorderedList
                            // 中获取 View,并且是从 Z-order 最大的 View 开始遍历
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            if (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                // 如果此 View 无法接收事件或者当前事件的
                                // 落点不在这个View区域内则返回进行下一轮循环
                                continue;
                            }
                            // 走到这里说明当前 View 既能接收事件,并且
                            // 事件也落在 View 内,接着就需要进一步处理事件了

                            // 对于单点触控事件,newTouchTarget 此时为空
                            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;
                            }
                            // 如果有设置 PFLAG_CANCEL_NEXT_UP_EVENT,在此清除
                            resetCancelNextUpFlag(child);
                            // 这里就会执行子 View 事件分发处理逻辑了,待后面详细分析      
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // 走进来说明子 View 成功消费事件
                                ....
                                
                                // 为消费此次事件的子 View 构建 TouchTarget,
                                // 并且会将构建的 TouchTarget 赋值给 mFirstTouchTarget
                                // 和 newTouchTarget,对于单点触控来说
                                // TouchTarget 链表只有 mFirstTouchTarget 一个元素
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                // alreadyDispatchedToNewTouchTarget 赋值为 true
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                         ...
                        }
                        // 清空 preorderedList
                        if (preorderedList != null) preorderedList.clear();
                    }
                    ...
                }
            }

            if (mFirstTouchTarget == null) {
                // mFirstTouchTarget 为空说明没有子 View 接收此次事件
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                // 遍历 TouchTarget 链表,因为 mFirstTouchTarget 不为空,所以已经找到目标,只需要进行事件分发即可
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                // 如果事件被取消或者一个事件序列结束就需要重置事件的各种状态,
                // 最重要的状态就是将 mFirstTouchTarget 置空
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                // 多点触控相关的收尾
				...
            }
        } 

        if (!handled && mInputEventConsistencyVerifier != null) {
            // 事件未被处理,回调通知
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
  	......
}

ViewGroup 的 dispatchTouchEvent 比较长,但是逻辑不算复杂,上述方法我们省略了鼠标相关和多点触控相关的代码,就单点触控的事件分发来看逻辑很简单,很多代码其实都是对分发的条件判断,比如某个子 View 想要接收事件,它起码得不被遮挡,然后你触摸的地方得在这个 View 的边界内吧,这个 View 还得处于 Z-order 的最顶层等,只有经过各种条件的判断这个 View 最后才有资格拿到事件进行处理。

ViewGroup.dispatchTouchEvent 总结:

  • 首先会调用 onFilterTouchEventForSecurity 方法对此次事件进行初步判断,判断的是 ViewGroup 是否被遮挡或者隐藏
  • 步骤 1 的条件判断通过之后,接着对于 ACTION_DOWN 事件会清空上一个事件序列(一个事件序列通常由一个 ACTION_DOWN,N 个 ACTION_MOVE,一个 ACTION_UP 组成)留下的各种状态,最主要是清空 TouchTarget 链表
  • 接着会有两个条件来判断是否走 ViewGroup 的拦截机制
  1. 条件1:此次事件是否为 ACTION_DOWN
  2. 条件2:mFirstTouchTarget 是否为空

这两个条件的意思是:对于一个事件序列的 ACTION_DOWN 事件,一定会走 ViewGroup 的拦截机制,并且同一事件序列的一个事件如果被拦截了,那么后续事件默认都会被拦截而不会再走拦截方法 onInterceptTouchEvent,子 View 可以通过 requestDisallowInterceptTouchEvent 方法请求父 View 不要拦截。

  • 接着又会有两个条件来判断事件是否继续分发,canceled 和 intercepted,事件被取消和被拦截,其实 canceled 多半是因为 intercepted 导致的,这个后面再说
  • 对于没有被取消且没有被拦截,且是 ACTION_DOWN 的事件,就要开始遍历 View 树,寻找真正消费事件的子 View 了,这里为何会单独对 ACTION_DOWN 进行判断呢?这是因为一个事件序列可能包含多个触摸事件,而触摸事件寻找消费的子 View 是通过递归遍历 View 树,为了性能考虑,Android 的设计为:当接收到 ACTION_DOWN 时开始对 View 树进行遍历,找到最终消费事件的子 View 之后将其保存,同一事件序列的后续 ACTION_MOVE,ACTION_UP 则不再需要遍历,直接将事件发送给保存好的子 View 就行了,对子 View 的保存就用到了 TouchTarget,这是一种链表结构,后面再说
  • 对于目标子 View 的寻找就比较简单了,首先将当前 ViewGroup 的所有子 View 以 Z-order 的顺序进行重建,保存在一个 list 中,然后遍历 list,从 Z-order 最大的子 View 开始,遍历条件有两个:当前遍历的子 View 可以接收事件,并且触摸区域落在当前子 View 之内则说明成功找到子 View,然后调用 dispatchTransformedTouchEvent 执行子 View 事件处理流程,如果事件成功处理则会为此子 View 构建 TouchTarget,并赋值给 mFirstTouchTarget
  • 接着对于 mFirstTouchTarget 不为空的情况会遍历链表,其目的是在上一步中已经找到接收事件的目标子 View 并且保存到了 TouchTarget 链表,对于一个事件序列的后续事件只需要遍历链表分发事件就行了
  • 对于没有找到消费事件的子 View 即 mFirstTouchTarget 为空,以及事件被取消的情况会做一些收尾工作

上面的总结主要是针对单点触控的情况,如果是多点触控还要复杂一些,同学们可以自行研究。

我们接着来理解 TouchTarget,这个数据结构对于事件分发非常重要,TouchTarget 在整个 ViewGroup 事件分发中是以链表的形式存在,它保存了一个 ViewGroup 中接收事件的目标子 View。

3.7 TouchTarget

   private TouchTarget mFirstTouchTarget;
   private static final class TouchTarget {
        ......
        // 保存接收事件的一级 View
        public View child;
        ......
        //
        public TouchTarget next;

        @UnsupportedAppUsage
        private TouchTarget() {
        }
        ...
   }

在 ViewGroup 中只有一个地方会构造 TouchTarget,就是在接收到 ACTION_DOWN时,此时会遍历子 View 并找到目标子 View,且目标子 View 成功处理事件之后,就会为此子 View 构建对应 TouchTarget,而多个 ViewGroup 的多个目标子 View 就构成了 TouchTarget 事件分发的链表:

 private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

这种代码就是典型的链表,mFirstTouchTarget 作为链表的头指针,我们用图来表示,第一次构建 TouchTarget :
在这里插入图片描述
第二次构建 TouchTarget :
在这里插入图片描述
以此类推,mFirstTouchTarget 总是指向最新构建的 TouchTarget:
在这里插入图片描述
对于上面的布局结构,如果我们点击 Button,事件的分发就是从 DecorView->ViewGroup A->ViewGroup B->ViewGroup C->Button,mFirstTouchTarget 最终作为头指针指向事件分发流程的所有 View 所构建的 TouchTarget 链表,有了 TouchTarget 链表,只需要在事件序列开始即 ACTION_DOWN 到来时进行一次 View 树遍历,后续的 ACTION_DOWN,ACTION_UP 就可以直接通过 TouchTarget 链表进行分发。

3.8 事件拦截onInterceptTouchEvent

我们继续来看事件拦截,ViewGroup 提供了 onInterceptTouchEvent 用来对事件进行拦截,默认情况父 View 是不会拦截事件的,什么情况下需要拦截呢?
在这里插入图片描述
比如这种情况,一个 ListView 中嵌套了多个 Button,当我们点击 Button 进行上下滑动操作时,此时我们并不希望 Button 来处理事件,而是希望 ListView 处理,而我们如果仅仅是点击 Button,此时又希望 Button 来处理事件,这时就需要 ListView 有条件的对事件进行拦截,比如判断用户滑动的距离,速度等。

对于事件拦截的使用很简单,只需要重写 ViewGroup 的 onInterceptTouchEvent 方法:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        ....
        return true;
    }

返回 true 就能实现拦截,事件的拦截同样是针对一个事件序列来说的,当一个事件序列的事件被父 View 拦截,那么此事件序列的后续事件子 View 都收不到,这就引出一个问题,像 Button 这样的控件是需要收到 ACTION_UP 才能对事件进行处理的,因为 Button 需要判断当前事件是点击还是长按,如果在 ACTION_MOVE 时 Button 的事件被拦截掉,那么 Button 同样收不到后续的 ACTION_UP 事件,这个时候就需要引入一种新的事件用于告诉被拦截掉事件的子控件,它就是 ACTION_CANCEL,有了 ACTION_CANCEL 就可以对事件被拦截掉的情况做收尾工作。

3.9 子View事件处理dispatchTouchEvent

对于事件的具体处理就比较简单了:

public boolean dispatchTouchEvent(MotionEvent 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;
}

首先判断该 View 是否设置了触摸监听,即是否调用过 setOnTouchListener 方法,如果有则将事件传入其 onTouch 方法进行处理并返回结果,如果没有设置,则会调用 View 的 onTouchEvent 方法,此方法中会有条件的调用 onClick,对于事件处理的顺序就是:onTouch,onTouchEvent,onClick。

关于 View 事件分发的详细介绍还请参考 Android View事件分发机制。这篇文章详细介绍了其中的细节。

(完毕)

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-07-15 16:18:51  更:2021-07-15 16:19:35 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/2 4:55:35-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码