大领导又给小明安排任务——Android触摸事件

作者 : 开心源码 本文共7979个字,预计阅读时间需要20分钟 发布时间: 2022-05-12 共192人阅读

这是Android触摸事件系列的第二篇,系列文章目录如下:

  1. 大领导给小明安排任务——Android触摸事件
  2. 大领导又给小明安排任务——Android触摸事件

把上一篇中领导分配任务的故事,延展一下:

大领导安排任务会经历一个“递”的过程:大领导先把任务告诉小领导,小领导再把任务告诉小明。也可能会经历一个“归”的过程:小明告诉小领导做不了,小领导告诉大领导任务完不成。而后,就没有而后了。。。。但假如这次完成了任务,大领导还会继续将后序任务分配给小明。

故事的延展部分和今天要讲的ACTION_DONW后序事件很相似,先来答复上一篇中遗留的另一个问题“阻拦事件”:

阻拦事件

ViewGroup在遍历孩子分发触摸事件前还有一段阻拦逻辑:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {            ...            // Check for interception.            //检查ViewGroup能否要阻拦触摸事件的下发            final boolean intercepted;            //第一个条件表示阻拦ACTION_DOWN事件            //第二个条件表示阻拦ACTION_DOWN事件已经分发给孩子,现在阻拦后序事件            if (actionMasked == MotionEvent.ACTION_DOWN                    || mFirstTouchTarget != null) {                //检查能否允许阻拦                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;                //阻拦分发给孩子的触摸事件                if (!disallowIntercept) {                    intercepted = onInterceptTouchEvent(ev);                    ev.setAction(action); // restore action in case it was changed                } else {                    intercepted = false;                }            } else {                // There are no touch targets and this action is not an initial down                // so this view group continues to intercept touches.                intercepted = true;            }            ...            //当事件没有被阻拦的时候,将其分发给孩子            if (!canceled && !intercepted) {                //遍历孩子并将事件分发给它们                //假如有孩子宣称要消费事件,则将其增加到触摸链上                //这段逻辑在上一篇中分析过,这里就省略了            }        }                //将触摸事件分发给触摸链        if (mFirstTouchTarget == null) { //没有触摸链             //假如事件被ViewGroup阻拦,则触摸链为空,ViewGroup自己消费事件            handled = dispatchTransformedTouchEvent(ev, canceled, null,                        TouchTarget.ALL_POINTER_IDS);        } else {            ...        }    }        //返回true表示阻拦事件,默认返回false    public boolean onInterceptTouchEvent(MotionEvent ev) {        return false;    }        private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {        ...        if (child == null) {            //ViewGroup孩子都不愿意接收触摸事件或者者触摸事件被阻拦 则其将自己当成View解决(调用View.dispatchTouchEvent())            handled = super.dispatchTouchEvent(transformedEvent);        }        ...    }}

当允许阻拦时,onInterceptTouchEvent()会被调用,假如重载这个方法并且返回true,表示ViewGroup要对事件进行阻拦,此时不再将事件分发给孩子而是自己消费(通过调用View.dispatchTouchEvent()最终走到ViewGroup.onTouchEvent())。

用一张图总结一下:

图1

  • 图中黑色的箭头表示触摸事件传递的路径,灰色的箭头表示触摸事件消费的回溯路径。onInterceptTouchEvent()返回true,导致onTouchEvent()被调用,由于onTouchEvent()返回true,导致dispatchTouchEvent()返回true
  • 精确的说,阻拦触摸事件的受益者是所有上层的ViewGroup(包括自己),由于触摸事件不再会向下层的View传递。

ACTION_MOVE和ACTION_UP

上一篇在阅读源码的时候,埋下了一个伏笔,现在将其补全:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {    //触摸链头结点    private TouchTarget mFirstTouchTarget;    ...    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        if (!canceled && !intercepted) {            ...            //当ACTION_DOWN的时候才遍历寻觅消费触摸事件的孩子,若找到则将其加入到触摸链            if (actionMasked == MotionEvent.ACTION_DOWN                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {                //遍历孩子                for (int i = childrenCount - 1; i >= 0; i--) {                    ...                    //转换触摸坐标并分发给孩子(child参数不为null)                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {                          ...                          //有孩子愿意消费触摸事件,将其插入“触摸链”                          newTouchTarget = addTouchTarget(child, idBitsToAssign);                          //表示已经将触摸事件分发给新的触摸目标                          alreadyDispatchedToNewTouchTarget = true;                          break;                    }                     ...                }            }        }            if (mFirstTouchTarget == null) {                //假如没有孩子愿意消费触摸事件,则自己消费(child参数为null)                handled = dispatchTransformedTouchEvent(ev, canceled, null,                        TouchTarget.ALL_POINTER_IDS);        }         //触摸链不为null,表示有孩子消费了ACTION_DOWN        else {                //将伏笔补全                TouchTarget predecessor = null;                TouchTarget target = mFirstTouchTarget;                //遍历触摸链将ACTION_DOWN的后序事件分发给孩子                while (target != null) {                    final TouchTarget next = target.next;                    //上一篇分析了,ACTION_DOWN会走这里                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {                        //假如已经将触摸事件分发给新的触摸目标,则返回true                        handled = true;                    }                     //ACTION_DONW的后序事件走这里                    else {                        ...                        //将触摸事件分发给触摸链上的触摸目标                        if (dispatchTransformedTouchEvent(ev, cancelChild,                                target.child, target.pointerIdBits)) {                            handled = true;                        }                        ...                    }                    predecessor = target;                    target = next;                }        }        ...        if (canceled            || actionMasked == MotionEvent.ACTION_UP            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {                //假如是ACTION_UP事件,则将触摸链清空                resetTouchState();        }        return handled;    }        private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,            View child, int desiredPointerIdBits) {        final boolean handled;        ...        // Perform any necessary transformations and dispatch.        //进行必要的坐标转换而后分发触摸事件        if (child == null) {            //ViewGroup孩子都不愿意消费触摸事件 则其将自己当成View解决(调用View.dispatchTouchEvent())            handled = super.dispatchTouchEvent(transformedEvent);        } else {            final float offsetX = mScrollX - child.mLeft;            final float offsetY = mScrollY - child.mTop;            transformedEvent.offsetLocation(offsetX, offsetY);            if (! child.hasIdentityMatrix()) {                transformedEvent.transform(child.getInverseMatrix());            }            //将触摸事件分发给孩子            handled = child.dispatchTouchEvent(transformedEvent);        }        ...        return handled;    }        /**     * Resets all touch state in preparation for a new cycle.     * 重置Touch标志     */    private void resetTouchState() {        clearTouchTargets();        resetCancelNextUpFlag(this);        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;        mNestedScrollAxes = SCROLL_AXIS_NONE;    }        /**     * Clears all touch targets.     * 清空触摸链     */    private void clearTouchTargets() {        TouchTarget target = mFirstTouchTarget;        if (target != null) {            do {                TouchTarget next = target.next;                target.recycle();                target = next;            } while (target != null);            mFirstTouchTarget = null;        }    }}

触摸事件是一个序列,序列总是以ACTION_DOWN开始,紧接着有ACTION_MOVEACTION_UPACTION_DOWN发生时,ViewGroup.dispatchTouchEvent()会将愿意消费触摸事件的孩子存储在触摸链中,当后序事件会分发给触摸链上的对象。

用一种图总结一下:

图2

  • 图中黑色箭头表示ACTION_DOWN事件的传递路径,灰色箭头表示ACTION_MOVEACTION_UP事件的传递路径。即只需有视图宣称消费ACTION_DOWN,则其后序事件也传递给它,不论它能否宣称消费ACTION_MOVEACTION_UP,假如它不消费,则后序事件会像上一篇分析的ACTION_DOWN一样向上回溯给上层消费。

图3

  • 图中黑色箭头表示ACTION_DOWN事件的传递路径,灰色箭头表示ACTION_MOVEACTION_UP事件的传递路径。即所有视图都不消费ACTION_DOWN,则其后序事件只会传递给Activity.onTouchEvent()

ACTION_CANCEL

把领导布置任务的故事继续延展一下:大领导给小领导布置了任务1,小领导把他传递给小明,小明完成了。紧接着大领导给小领导布置了任务2,小领导决定自己解决任务2,于是他和小明说后序任务我来接手,你可以忙别的事情。

故事对应的触摸事件传递场景是:ActivityACTION_DOWN传递给ViewGroupViewGroup将其传递给ViewView宣称消费ACTION_DOWNActivity继续将ACTION_MOVE传递给ViewGroup,但ViewGroup对其做了阻拦,此时ViewGroup会发送ACTION_CANCEL事件给View

看下源码:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {    public boolean dispatchTouchEvent(MotionEvent ev) {        //检查ViewGroup能否要阻拦触摸事件的下发        final boolean intercepted;        if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;                //阻拦分发给孩子的触摸事件                if (!disallowIntercept) {                    intercepted = onInterceptTouchEvent(ev);                    ev.setAction(action); // restore action in case it was changed                } else {                    intercepted = false;                }        }        ...        //假如孩子消费ACTION_DOWN事件,则会在这里将其增加到触摸链中        if (!canceled && !intercepted) {            ...        }        //将触摸事件分发给触摸链        if (mFirstTouchTarget == null) { //没有触摸链 表示当前ViewGroup中没有孩子愿意接收触摸事件            //将触摸事件分发给自己        } else {            // Dispatch to touch targets, excluding the new touch target if we already            // dispatched to it.  Cancel touch targets if necessary.            TouchTarget predecessor = null;            TouchTarget target = mFirstTouchTarget;            //遍历触摸链分发触摸事件给所有想接收的孩子            while (target != null) {                final TouchTarget next = target.next;                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {                    handled = true;                } else {                    //假如事件被阻拦则cancelChild为true                    final boolean cancelChild = resetCancelNextUpFlag(target.child)                            || intercepted;                    //将ACTION_CANCEL事件传递给孩子                    if (dispatchTransformedTouchEvent(ev, cancelChild,                            target.child, target.pointerIdBits)) {                        handled = true;                    }                    //假如发送了ACTION_CANCEL事件,将孩子从触摸链上摘除                    if (cancelChild) {                        if (predecessor == null) {                            mFirstTouchTarget = next;                        } else {                            predecessor.next = next;                        }                        target.recycle();                        target = next;                        continue;                    }                }                predecessor = target;                target = next;            }        }        ...    }        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 {                //将ACTION_CANCEL事件传递给孩子                handled = child.dispatchTouchEvent(event);            }            event.setAction(oldAction);            return handled;    }    ...}

当孩子消费了ACTION_DOWN事件,它的引用被会保存在父亲的触摸链中。当父亲阻拦后序事件时,父亲会向触摸链上的孩子发送ACTION_CANCEL事件,并将孩子从触摸链上摘除。后序事件就传递到父亲为止。

总结

经过两篇文章的分析,对Android触摸事件的分发有了初步的理解,得出了以下结论:

  • Activity接收到触摸事件后,会传递给PhoneWindow,再传递给DecorView,由DecorView调用ViewGroup.dispatchTouchEvent()自顶向下分发ACTION_DOWN触摸事件。
  • ACTION_DOWN事件通过ViewGroup.dispatchTouchEvent()DecorView经过若干个ViewGroup层层传递下去,最终到达View
  • 每个层次都可以通过在onTouchEvent()或者OnTouchListener.onTouch()返回true,来告诉自己的父控件触摸事件被消费。只有当下层控件不消费触摸事件时,其父控件才有机会自己消费。
  • 触摸事件的传递是从根视图自顶向下“递”的过程,触摸事件的消费是自下而上“归”的过程。
  • ACTION_MOVEACTION_UP会沿着刚才ACTION_DOWN的传递路径,传递给消费了ACTION_DOWN的控件,假如该控件没有公告消费这些后序事件,则它们也像ACTION_DOWN一样会向上回溯让其父控件消费。
  • 父控件可以通过在onInterceptTouchEvent()返回true来阻拦事件向其孩子传递。假如在孩子已经消费了ACTION_DOWN事情后才进行阻拦,父控件会发送ACTION_CANCEL给孩子。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 大领导又给小明安排任务——Android触摸事件

发表回复