likes
comments
collection
share

Android触摸事件分发的“递”与“归”(一)

作者站长头像
站长
· 阅读数 5

这是Android触摸事件系列文章的第一篇。

大领导安排任务会经历一个“递”的过程:大领导先把任务告诉小领导,小领导再把任务告诉小明。也可能会经历一个“归”的过程:小明告诉小领导做不了,小领导告诉大领导任务完不成。然后,就没有然后了。。。。

Android触摸事件和领导安排任务的过程很相似,也会经历“递”和“归”。这一篇会试着阅读源码来分析ACTION_DOWN事件的这个递归过程。

(ps: 下文中的 粗斜体字 表示引导源码阅读的内心戏)

分发触摸事件起点

写一个包含ViewGroupViewActivity的demo,并在所有和touch有关的方法中打log。当触摸事件发生时,Activity.dispatchTouchEvent()总是第一个被调用,就以这个方法为切入点:

public class Activity{
    private Window mWindow;
    
    //'分发触摸事件'
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //'让PhoneWindow帮忙分发触摸事件'
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    
    //'获得PhoneWindow对象'
    public Window getWindow() {
        return mWindow;
    }
    
    final void attach(...) {
        ...
        //'构造PhoneWindow'
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...
    }
}

Activity将事件传递给PhoneWindow

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    //'一个窗口的顶层视图'
    private DecorView mDecor;
    
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //'将触摸事件交给DecorView分发'
        return mDecor.superDispatchTouchEvent(event);
    }
}

//'DecorView继承自ViewGroup'
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks{
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //'事件最终由ViewGroup.dispatchTouchEvent()分发触摸事件'
        return super.dispatchTouchEvent(event);
    }
}
  • PhoneWindow继续将事件传递给DecorView,最终调用了ViewGroup.dispatchTouchEvent()
  • 至此可以做一个简单的总结:触摸事件的传递从Activity开始,经过PhoneWindow,到达顶层视图DecorViewDecorView调用了ViewGroup.dispatchTouchEvent()

触摸事件之“递”

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!canceled && !intercepted) {
            ...
            //'遍历孩子'
            for (int i = childrenCount - 1; i >= 0; i--) {
                //'按照索引顺序或者自定义绘制顺序遍历孩子'
                final int childIndex = customOrder
                      ? getChildDrawingOrder(childrenCount, i) : I;
                final View child = (preorderedList == null)
                      ? children[childIndex] : preorderedList.get(childIndex);
                ...
                							
                //'如果孩子不在触摸区域则直接跳过'
                if (!canViewReceivePointerEvents(child)
                      || !isTransformedTouchPointInView(x, y, child, null)) {
                      ev.setTargetAccessibilityFocus(false);
                      continue;
                }
                ...
                //'转换触摸坐标并分发给孩子(child参数不为null)'
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    //这里的代码也很关键,先埋伏笔1
                }
                 ...
            }
        }
        if (mFirstTouchTarget == null) {
                //这里的代码也很关键,先埋伏笔2
        } else {
            //这里的代码也很关键,先埋伏笔3
        }
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        ...
        //'进行必要的坐标转换然后分发触摸事件'
        if (child == null) {
            //这里的代码也很关键,先埋伏笔4
        } else {
            //'将ViewGroup坐标系转换为它孩子的坐标系(坐标原点从ViewGroup左上角移动到孩子左上角)'
            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;
    }
}

果然没猜错!父控件在ViewGroup.dispatchTouchEvent()中会遍历孩子并将触摸事件分发给被点中的子控件,如果子控件还有孩子,触摸事件的“递”将不断持续,直到叶子结点。 最终View类型的叶子结点调用的是View.dispatchTouchEvent()

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            //'1.通知触摸监听器OnTouchListener'
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            //'2.调用onTouchEvent()'
            //'只有当OnTouchListener.onTouch()返回false时,onTouchEvent()才有机会被调用'
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ...
        //'返回值就是onTouch()或者onTouchEvent()的返回值'
        return result;
    }
    
    ListenerInfo mListenerInfo;
    
    //'监听器容器类'
    static class ListenerInfo {
        ...
        private OnTouchListener mOnTouchListener;
        ...
    }
    
    //'设置触摸监听器'
    public void setOnTouchListener(OnTouchListener l) {
        //'将监听器存储在监听器容器中'
        getListenerInfo().mOnTouchListener = l;
    }
    
    //'获得监听器管理实例'
    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
}
    
  • View.dispatchTouchEvent()是传递触摸事件的终点,消费触摸事件的起点。
  • 消费触摸事件的标志是调用OnTouchListener.onTouch()View.onTouchEvent(),前者优先级高于后者。只有当没有设置OnTouchListener或者onTouch()返回false时,View.onTouchEvent()才会被调用。
  • 读到这里,画一张图总结一下触摸事件之“递”: Android触摸事件分发的“递”与“归”(一)
  • 图中 ViewGroup 层后面的 N 表示在 Activity 层和 View 层之间可能有多个 ViewGroup 层。
  • 图中自上而下一共有三类层次,触摸事件会从最高层次开始沿着箭头往下层传递。
  • 为简单起见,图中省略了另一种触摸事件的处理方式:OnTouchListener.onTouch
  • 图示触摸事件的传递只是众多传递场景中的一种:被点击的 View 嵌套在 ViewGroup 中,ViewGroup 在 Activity 中。

触摸事件之“归”

触摸事件之所以在“递”之后还会发生“归”是因为:分发触摸事件的函数还没有执行完。沿着刚才调用链相反的方向重新看一遍源码:

public class View{
    //'返回true表示触摸事件被消费,否则表示未被消费'
    public boolean onTouchEvent(MotionEvent event) {
       ...
       if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            //'省略了对不同触摸事件的默认处理'
            ...
            //'只要控件是可点击的,就表示触摸事件已被消费'
            return true;
        }
        //'若控件不可点击则不消费触摸事件'
        return false;
    }
}

View.dispatchTouchEvent()调用了View.onTouchEvent()后并没有执行完。View.onTouchEvent()的返回值会影响View.dispatchTouchEvent()的返回值:

public class View  {
    public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        boolean result = false;
        ...
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            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;
            }
        }
        //'返回当前View是否消费触摸事件的布尔值'
        return result;
    }

同样的,ViewGroup.dispatchTouchEvent()调用了View.dispatchTouchEvent()后也没有执行完,View.dispatchTouchEvent()的返回值会影响ViewGroup.dispatchTouchEvent()的返回值:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    //'触摸链头结点'
    private TouchTarget mFirstTouchTarget;
    ...
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!canceled && !intercepted) {
            ...
            //'遍历孩子'
            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);
        } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                //'遍历触摸链分发触摸事件给所有想接收的孩子'
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        //'如果已经将触摸事件分发给新的触摸目标,则返回true'
                        handled = true;
                    } else {
                        //'这里的代码很重要,继续埋伏笔,待下一篇分析。'
                    }
                    predecessor = target;
                    target = next;
                }
        }
        ...
        //'返回触摸事件是否被孩子或者自己消费的布尔值'
        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 {
            //将触摸事件分发给孩子
        }
        ...
        return handled;
    }
    
    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     * '添加View到触摸链头部'
     * @param child  View
     * @param pointerIdBits
     * @return 新触摸目标
     */
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
}
  • 上面这段代码补全了上一节中买下的伏笔。原来当孩子愿意消费触摸事件时,ViewGroup会将其接入“触摸链”,如果触摸链中没有结点则表示没有孩子愿意消费事件,此时ViewGroup只能自己消费事件。ViewGroupView的子类,他们消费触摸事件的方式一摸一样,都是通过View.dispatchTouchEvent()调用View.onTouchEvent()OnTouchListener.onTouch()
  • 沿着回溯链,再向上“归”一步:
public class Activity {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            //如果布局中有控件愿意消费触摸事件,则返回true,onTouchEvent()不会被调用
            return true;
        }
        return onTouchEvent(ev);
    }
}

ViewViewGroupActivity,虽然它们分发触摸事件的逻辑不太一样,但基本结构都和上面这段代码神似,用伪代码可以写成:

//“递”
if(分发事件给孩子){
    如果孩子消费了事件 直接返回(将触摸事件被消费这一事实往上传递)
}
//“归”
如果孩子没有消费事件,则自己消费事件

“分发事件给孩子”这个函数的调用表示“递”,即将触摸事件传递给下层。“分发事件给孩子”这个函数的返回表示“归”,即将触摸事件的消费结果回溯给上层,以便上层采取进一步的行动。

同样的套路,用图片总结下触摸事件之“归”: Android触摸事件分发的“递”与“归”(一)

  • 这张图是对图1描述场景的补全。图中黑色的线表示触摸事件的传递路径,灰色的线表示触摸事件回溯的路径。
  • 因为View.onTouchEvent()返回true,表示消费触摸事件,所以ViewGroup.onTouchEvent()以及Activity.onTouchEvent()都不会被调用。

Android触摸事件分发的“递”与“归”(一)

  • 这张图是对图1描述场景的扩展。图中黑色的线表示触摸事件的传递路径,灰色的线表示触摸事件回溯的路径。
  • 图示所对应的场景是:被点击的View不消费触摸事件,而ViewGrouponTouchEvent()中返回true自己消费触摸事件。

Android触摸事件分发的“递”与“归”(一)

  • 这张图是对图1描述场景的扩展。图中黑色的线表示触摸事件的传递路径,灰色的线表示触摸事件回溯的路径。
  • 图示所对应的场景是:被点击的ViewViewGroup都不消费触摸事件,最后只能由Activity来消费触摸事件。

总结

  • Activity接收到触摸事件后,会传递给PhoneWindow,再传递给DecorView,由DecorView调用ViewGroup.dispatchTouchEvent()自顶向下分发ACTION_DOWN触摸事件。
  • ACTION_DOWN事件通过ViewGroup.dispatchTouchEvent()DecorView经过若干个ViewGroup层层传递下去,最终到达ViewView.dispatchTouchEvent()被调用。
  • View.dispatchTouchEvent()是传递事件的终点,消费事件的起点。它会调用onTouchEvent()OnTouchListener.onTouch()来消费事件。
  • 每个层次都可以通过在onTouchEvent()OnTouchListener.onTouch()返回true,来告诉自己的父控件触摸事件被消费。只有当下层控件不消费触摸事件时,其父控件才有机会自己消费。
  • 触摸事件的传递是从根视图自顶向下“递”的过程,触摸事件的消费是自下而上“归”的过程。

读到这里可能对于触摸事件还充满诸多疑问:

  1. ViewGroup层是否有办法拦截触摸事件?
  2. ACTION_DOWN只是触摸序列的起点,后序的ACTION_MOVEACTION_UPACTION_CANCEL是如何传递的?

这些问题会在下一篇继续分析。

转载自:https://juejin.cn/post/6844903788826853384
评论
请登录