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

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

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

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

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

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

拦截事件

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

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())。spa

用一张图总结一下: 3d

图1

  • 图中黑色的箭头表示触摸事件传递的路径,灰色的箭头表示触摸事件消费的回溯路径。onInterceptTouchEvent()返回true,致使onTouchEvent()被调用,由于onTouchEvent()返回true,致使dispatchTouchEvent()返回true
  • 准确的说,拦截触摸事件的受益者是全部上层的ViewGroup(包括本身),由于触摸事件再也不会向下层的View传递。

ACTION_MOVE 、 ACTION_UP

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

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()会将愿意消费触摸事件的孩子存储在触摸链中,当后序事件会分发给触摸链上的对象。code

用两张图总结一下: orm

图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给孩子。
相关文章
相关标签/搜索