初心在,仍是少年。代码如诗,微笑多。朝着星辰大海,继续前进...

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

随笔 659 ℃ 0评论

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

  1. 大领导给小明安排任务——Android触摸事件
  2. 大领导又给小明安排任务——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)) {
            returntrue;
        }
        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 {

    // This is the top-level view of the window, containing the window decor.
    //一个窗口的顶层视图
    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()

触摸事件之“递”

  • 在分析View绘制时,也遇到过“dispatchXXX”函数ViewGroup.dispatchDraw(),它用于遍历孩子并触发它们自己绘制自己。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;
          ...
          // Perform any necessary transformations and dispatch.
          //进行必要的坐标转换然后分发触摸事件
          if (child == null) {
              //这里的代码也很关键,先埋伏笔3
          } 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()才会被调用。
  • 读到这里,画一张图总结一下触摸事件之“递”: 图1
  • 图中ViewGroup层后面的N表示在Activity层和View层之间可能有多个ViewGroup层。
  • 图中自上而下一共有三类层次,触摸事件会从最高层次开始沿着箭头往下层传递。
  • 为简单起见,图中省略了另一种触摸事件的处理方式:OnTouchListener.onTouch
  • 图示触摸事件的传递只是众多传递场景中的一种:被点击的View嵌套在ViewGroup中,ViewGroup在Activity中。

触摸事件之“归”

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

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
   /**
     * Implement this method to handle touch screen motion events.
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     * 返回true表示触摸事件被消费,否则表示未被消费
     */
    public boolean onTouchEvent(MotionEvent event) {
       ...
       if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            //省略了对不同触摸事件的默认处理
            ...
            //只要控件是可点击的,就表示触摸事件已被消费
            returntrue;
        }
        //若控件不可点击则不消费触摸事件
        returnfalse;
    }
}

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

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    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()不会被调用
              returntrue;
          }
          return onTouchEvent(ev);
      }

    }

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

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

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

同样的套路,用图片总结下触摸事件之“归”:

图2

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

图3

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

图4

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

总结

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

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

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

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

                ##文章来源


                    - 文章来源: [掘金](https://juejin.im)


                    -原文链接: [点击访问](https://juejin.im/post/5c78fb9be51d457143523547)