安卓事件机制原理

868 阅读4分钟

其实要想很好的理解安卓事件机制,最好的方式就是自己撸一个简化的版本。也就是提取阅读源码后的思想。

废话不多说,直接开始。如果没有阅读安卓事件机制源码的,可以参考这篇安卓事件分发机制源码详读 。 我们知道事件是从 Activity 开始分发,最终回到 ViewGroup 的 dispatchTouchEvent(event) 。这里本质上就是模仿,新建一个安卓工程,并添加一个 java 库的 lib, 注意是 java lib。

为了提取思想,我们也是仿造安卓事件机制部分源码。这里也新建一个 MyViewGroup 的 java 类继承自 MyView, 因为事件机制中 ViewGroup 也是继承自 View,这里也是做了相似的操作。我们在使用手势的时候,应该对 MotionEvent 肯定是不陌生的,这个类中封装了当前点相对于控件的 x,y 坐标,以及当前的事件类型,比如 ACTION_DOWN、ACTION_MOVE、ACTION_UP。 当然这里只是简化了的。

首先阐述核心原理,在 MyViewGroup 中保存添加的子视图,当一个事件来的时候先来到 MyViewGroup, 然后倒叙遍历查找容器的的子视图是否有被符合点击范围的,如果符合。则将事件分发给这个子视图,子视图如果是容器,则会继续遍历其子视图。如果是子视图就检查是否拦截事件,如果不拦截就会返回到父视图的 onTouchEvent,父视图可以选择拦截,反之继续往上层容器返回事件。

先来看 MyViewGroup 的 dispatchTouchEvent(event)

 @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        System.out.println("当前视图容器的名称: " + super.name);
        int actionMasked = event.getActionMasked();
        boolean intercepted = false;

        boolean handled = false;
        
        // 当按下事件来
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 容器视图可以重写此方法返回 true 拦截事件
            intercepted = onInterceptTouchEvent(event);
        }

        boolean alreadyDispatchedToNewTouchTarget = false;
        // 事件未被拦截也没有取消
        if (!intercepted && actionMasked != MotionEvent.ACTION_CANCEL) {
            // 获取按下的 x, y 距离视图的左上点
            float x = event.getX();
            float y = event.getY();
            int childrenCount = mChildrenCount;
            MyView[] children = mChildren;
            
            // 采取倒叙遍历的方式,因为一般往后添加的视图都会在最上面可见。
            for (int i = childrenCount - 1; i >= 0; i--) {
                MyView child = children[i];

                // 检查按下的坐标是否在 view 上.
                if (!isTransformedTouchPointInView(x, y, child)) {
                    System.out.println("未点击的view上: " + child.name);
                    continue;
                }

                // 将事件交给 child 来处理,由 child 决定是否消费
                TouchTarget newTouchTarget;
                if (dispatchTransformedTouchEvent(event, child)) {
                    newTouchTarget = addTouchTarget(child);
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }
            }
        }

        // 如果没有事件被消费就会将事件交给容器来处理.
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(event, null);
        } else {
            if (alreadyDispatchedToNewTouchTarget) {
                handled = true;
            }
        }

        return handled;
    }

代码中有详细的注释, 接着来看 dispatchTransformedTouchEvent(event), 这个方法就很简单了,就是根据 child 判断,是否继续传递,还是将事件返回给父视图

private boolean dispatchTransformedTouchEvent(MotionEvent event, MyView child) {
        boolean handled = false;

        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }

        return handled;
    }

怎么检查手势是否在按下的范围呢?isTransformedTouchPointInView(float x, float y, MyView child), 其实也很简单,也就是 x 的起点是否大于左边 mLeft且小于右侧 mRight, y 轴大于顶部 mTop, 小于底部 mBottom。

private boolean isTransformedTouchPointInView(float x, float y, MyView child) {
        if (x >= child.mLeft && x <= child.mRight && y >= child.mTop && y <= child.mBottom) {
            return true;
        }

        return false;
    }

接下来通过调试来验证代码,首先没有任何视图消费事件,看看是不是符合安卓事件机制的分发过程。当运行后,看到如下打印,可以看到如果视图不消费事件就会将事件回传。并调用 视图 onTouchEvent。

第一个验证符合,接下来我们让容器拦截掉事件。看看还会不会往下传递, onInterceptTouchEvent 返回true。

// container 容器返回 true
 public boolean onInterceptTouchEvent(MotionEvent event) {

    return true;
}

可以看到只有 container 容器的 onTouchEvent 被调用。第二个验证符合。

第三个验证来看,子视图如果设置了 setOntouchListener 后还会不会调用 onTouchEvent

  myView.setOnTouchListener(new MyView.OnTouchListener() {
            @Override
            public boolean onTouch(MotionEvent event) {
                System.out.println("myView 视图的 onTouch 方法被调用.");
                return true;
            }
    });

可以看到没有打印任何相关 onTouchEvent 的信息, 我们返回 false 看看。

 myView.setOnTouchListener(new MyView.OnTouchListener() {
            @Override
            public boolean onTouch(MotionEvent event) {
                System.out.println("myView 视图的 onTouch 方法被调用.");
                return false;
            }
    });

是不是就先打印了 onTouch, 然后再打印了 onTouchEvent。 由于没有事件被消费,所以事件最终会被回传到 container 容器。

验证最后一个,就是点击事件肯定是在触摸事件之后被调用,前提是 onTouch 没有拦截。

myView.setOnTouchListener(new MyView.OnTouchListener() {
            @Override
            public boolean onTouch(MotionEvent event) {
                System.out.println("myView 视图的 onTouch 方法被调用.");
                return false;
            }
        });


        myView.setOnClickListener(new MyView.OnClickListener() {
            @Override
            public void onClick(MyView view) {
                System.out.println("myView 视图的 onClick 方法被调用.");
            }
        });

OK, 符合验证。如果 onTouch 返回 true。

myView.setOnTouchListener(new MyView.OnTouchListener() {
            @Override
            public boolean onTouch(MotionEvent event) {
                System.out.println("myView 视图的 onTouch 方法被调用.");
                return true;
            }
        });


        myView.setOnClickListener(new MyView.OnClickListener() {
            @Override
            public void onClick(MyView view) {
                System.out.println("myView 视图的 onClick 方法被调用.");
            }
        });

onClick 方法就不会被调。

这就是事件机制流程,至于很多细节未去处理。比如安卓事件机制维护了一个 TouchTarget, 它本职上是一个链表。通过 mFirstTouchTarget 来指向它的头节点,如果找到一个 view 需要被处理事件的时候就用头节点去记录它。以便后续的事件来的时候可以很快的找到处理的视图。

最后如果想看源码的,可以克隆下载查看,AndroidMotionEvent