CrazyDailyQuestion icon indicating copy to clipboard operation
CrazyDailyQuestion copied to clipboard

2019-8-23:根据这个场景(在ViewGroup A中嵌套了一个VewiGroup B,然后又在ViewGroup B 中嵌套了一个View)谈一下触摸事件的传递机制。

Open WarriorYu opened this issue 5 years ago • 6 comments

WarriorYu avatar Aug 23 '19 00:08 WarriorYu

大家可以根据图片里这个流程来扩展,展开讨论: 触摸事件传递机制

WarriorYu avatar Aug 23 '19 02:08 WarriorYu

onTouchListener、onTouchEvent、以及onClickListener谁的优先级高?

我们知道自定义View时如果有交互,避不开要重写View的onTouchEvent(),项目开发中setOnClickListener()的使用更是家常便饭,除此之外,偶尔也会setOnTouchListener()自定义触摸事件,那么如果我一个自定义View重写了onTouchEvent(),同时又setOnClickListener()以及setOnTouchListener(),它们的执行顺序时怎样的?一定都会执行吗?

我们看到View的dispatchTouchEvent()中有这么一段代码:

            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }

可以明显看到mOnTouchListener#onTouch()onTouchEvent()之前执行,并且它的返回值会影响到onTouchEvent()是否被执行。

在看到onTouchEvent()ACTION_UP事件中有:

public boolean onTouchEvent(MotionEvent event) {
            ...
            switch (action) {
                case MotionEvent.ACTION_UP:
                   ...
                performClickInternal();
                   ...

看到performClickInternal()

    private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

        return performClick();
    }
    
    public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

可以找到mOnClickListener#onClick()的执行,也就是说点击事件的回调是包含在onTouchEvent()中的,综合看来三者的顺序就是先mOnTouchListener#onTouch()并且返回值会影响onTouchEvent()是否执行,而mOnClickListener#onClick()是最后到达的,如果我们自定义View不调用super.onTouchEvent(),那么包括点击事件在内许多View自带的事件处理就被废掉了,就好像玄幻小说里面的经脉尽断的修者,要么牛逼,要么傻逼。

ViewGroup是怎么“放过”那些不在触摸范围内的View的?

我们在开发或者使用Android应用时,通常都是很符合直觉的摸到哪个View就是哪个View,但事实上事件时从外到里传递的,我们赖以为自然的直觉其实是ViewGroup帮我们分配的,那么体现到源码中是怎样的呢?

if (actionMasked == MotionEvent.ACTION_DOWN) {
        ...
      for (int i = childrenCount - 1; i >= 0; i--) {
        ...
        if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
        }
        ...
        
        
    /**
     * Returns true if a child view contains the specified point when transformed
     * into its coordinate space.
     * Child must not be null.
     * @hide
     */
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }

可以看到当按下事件来临时isTransformedTouchPointInView()方法校验了每个View和当前触摸的位置,如果不满足条件,就会跳过这个View的后续操作。

chinwetang avatar Aug 23 '19 13:08 chinwetang

View 的事件分发机制

   什么是事件分发? 事件分发,通俗点就是处理点击事件的流程。

MotionEvent就是我们要分析的点击事件对象,即当一个MotionEvent产生以后,系统需要把这个事件传递个具体的View

一. 点击事件的传递规则

  • 分发:dispatchTouchEvent

    • 如果事件传递给当前View
      • 一定会被调用。
  • 拦截:onInterceptTouchEvent

    • 如果当前 View 拦截了某个事件
      • 同一事件中这个方法不会被再次调用。
  • 消费:onTouchEvent

    • 如果不消耗,则在同一个事件序列中
      • View无法再次收到事件。
     @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
         boolean consume = false;

         if (onInterceptTouchEvent(ev)){
             consume = onTouchEvent(ev);
         }else {

             consume = child.dispatchTouchEvent(ev);
         }

         return  consume;

    }

二. 原理分析

三. View的滑动冲突

1. 出现场景

2. 处理规则

3. 解决方式

MicroKibaco avatar Aug 25 '19 14:08 MicroKibaco

首先分析一下Down事件。事件传递顺序是从底层一层一层往上传的,从A到B,到C,每一层View都有消费事件的机会,如果大家都不消费,又会一层一层回传回去,从C到B到A,最终到最底层的Activity消费掉事件。

从源码角度上来细看Down事件:

  1. 首先会调用ViewGroup A的dispatchTouchEvent
  2. dispatchTouchEvent中会通过onInterceptTouchEvent判断是否拦截此事件
  3. 如果拦截直接调用ViewGroup的onTouchEvent
  4. 如果不拦截就通过child.dispatchTouchEvent遍历调用每个在触摸范围内的子View,让子View自己处理是否消费事件。
  5. 如果子View都不消费,就调用super.dispatchTouchEvent,实际上就是调用ViewGroup的onTouchEvent.

dispatchTouchEvent主要分发事件,真正做事件处理是在onTouchEvent中,如果onTouchEvent返回true就代表view消费了这个事件,并且也将消费这组事件序列中剩余的事件。

Down事件走完流程后,会找到最终消费这个事件序列的View。后面的事件就会直接被该View接管。所以接下来的Move、Up事件,默认都是从底层一层一层传到该View。

正常的事件流程就是这样了。但是还有一种情况,就是底层的ViewGroup可能最开始不会拦截事件,将Down以及一些Move事件给子view消费,但是当ViewGroup的onInterceptTouchEvent中检测到手指滑动达到一定的条件时,就可以在onInterceptTouchEvent返回true,以拦截这个事件以及这组事件序列中剩余事件。一旦onInterceptTouchEvent返回true,该ViewGroup就默认拦截事件,不会再调用onInterceptTouchEvent,它的子 view也不能再消费事件了。此时View Group抢夺了子View的事件,子View会收到一个Cancel事件。

skylarliuu avatar Aug 25 '19 14:08 skylarliuu

事件分发机制其实整体来说涉及但View的触摸反馈还是比较简单的,只要注意一点,重写onTouchEvent,然后明白它的核心思想就是: 是否为消费事件取决于 ACTION_DOWN 事件是否返回为true,那么我们如何获取MotionEvent呢,这里要讲的有两个API,第一个是 getActionMasked ,而另外一个是 getAction,如果你不知道用哪个,用getActionMasked 就ok了。
那么ACTION_DOWN 事件具体干了一件什么事情呢?如果在滑动事件中,切换至按压状态,并注册按下计时器。当按下进入按压状态并移动ACTION_MOVE,重绘Ripple Effect,然后ACTION_MOVE是设计控件移动的反馈事件,而ACTION_CANCLE是切换抬起状态,并清除一切状态

MicroKibaco avatar Oct 22 '19 06:10 MicroKibaco

ViewGroup事件分发体系稍微有点复杂,相比单view的事件体系而言,,除了重写onTouchEvent以外,还重写了onInterceptTouchEvent,onInterceptTouchEvent不用在第一时间返回true,只有当事件被拦截的时候返回true就行了。那么从源码层次怎么去理解ViewGroup的时间分发体系呢?说白点,其实自定义view我们知道,关于viewGroup的子view都是按层级遍历解析的,那么同理,viewGroup的触摸反馈流程也是由Activity.dispatchTouchEvent 触发,然后历经 window,再到view层次,具体怎么做呢,我简单说一下: 递归Activty的dispatchTouchEvent,然后在通过调用viewGroup的dispatchTouchEvent,然后去观察ViewGroup的onInterceptTouchEvent,然后层层遍历最终child dispatchTouchEvent 和 view的onTouchEvent,最后所有子view的时间体系走完之后就走Activity的onTouchEvent

MicroKibaco avatar Oct 22 '19 06:10 MicroKibaco