美文网首页Android
Android事件分发

Android事件分发

作者: Timmy_zzh | 来源:发表于2021-02-27 23:55 被阅读0次

1.前言

Android touch事件分发有几个方向可以深入分析
  1. touch事件是如何从驱动层传递给Framework层的InputManagerService?
  2. WMS是如何通过ViewRootImpl将事件传递到目标窗口的?
  3. touch 事件到达DecorView后,是如何一步步传递到内部的子View中的?
ViewGroup

ViewGroup是一组View的组合,在其内部有可能包含多个子View,当手指触摸屏幕上时,手指所在区域既能在ViewGroup显示范围内,也可能在其内部View控件上。

  • 所以ViewGroup的事件分发的重点是处理当前ViewGroup和子View之间的逻辑关系:
    1. 当前ViewGroup是否需要拦截touch事件?
    2. 是否需要将touch事件继续分发给子View?
    3. 如何将touch事件分发给子View?
View
  • View是一个单纯的控件,不能再被细分,内部也并不会存在子View,所以它的事件分发的重点是当前View如何去处理touch事件,并根据相应的手势逻辑进行一系列的效果展示
    1. 是否存在TouchListener
    2. 是否自己接收处理touch事件(主要逻辑在onTouchEvent方法中)

2.事件分发核心 dispatchTouchEvent

  • 整个View之间的事件分发,实质上就是一个大的递归函数,而这个递归函数就是dispatchTouchEvent方法

    • 在递归的过程中会适时调用onInterceptTouchEvent方法来拦截事件
    • 或者调用 onTouchEvent 方法来处理事件
  • 从宏观角度看,ViewGroup的dispatchTouchEvent方法的源码逻辑如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {
            /**
             * 步骤1:检查当前ViewGroup是否需要拦截事件
             */
        ...
            /**
             * 步骤2:将事件分发给子View
             */
        ...
            /**
             * 步骤3:根据mFirstTouchTarget,再次分发事件
             */
        ...
    }
  • 解析:事件分发主要分为3大步骤:

步骤一:判断当前ViewGroup是否需要拦截此touch事件,如果拦截则此次touch事件不再会传递给子View(或者以CANCEL的方式通知子View

步骤二:如果没有拦截,则将事件分发给子View继续处理,如果子View将此次事件捕获(消耗),则将变量mFirstTouchTarget赋值给捕获touch事件的View

步骤三:根据mFirstTouchTarget重新分发事件。

2.1.步骤一代码具体实现
    /**
     * 步骤1:检查当前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;
         }
     } else {
         intercepted = true;
     }

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }
  • 解析:下面这个if判断标出了是否需要事件拦截的条件:
    • if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
    • 如果事件为DOWN事件,则调用onInterceptTouchEvent方法,返回是否拦截的标记intercepted;
      • ViewGroup的事件拦截方法onInterceptTouchEvent默认返回false,表示不拦截事件
    • 或者mFirstTouchTarget不为null,代表已经有子View捕获了这个事件。
      • 子View的dispatchTouchEvent返回true就是代表捕获touch事件
2.2.步骤二具体代码实现:
     /**
        * 步骤2:将事件分发给子View
        */
        if (!canceled && !intercepted) {
        ...
        //TODO 1.事件分发的前提是事件为DOWN事件
                if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            final int actionIndex = ev.getActionIndex(); // always 0 for down
            final int childrenCount = mChildrenCount;
            //TODO 2.遍历所有子View
            for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = getAndVerifyPreorderedIndex();
                final View child = getAndVerifyPreorderedView();
                //TODO 3.判断事件坐标是否处在当前子View坐标范围内,并且子View没有处在动画状态
                if (!child.canReceivePointerEvents()
                        || !isTransformedTouchPointInView(x, y, child, null)) 
                     continue;
                }
                    
                //TODO 4.调用dispatchTransformedTouchEvent方法将事件分发给子View,如果子View捕获事件成功,则将newTouchTarget赋值给子View
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                      ...
                      newTouchTarget = addTouchTarget(child, idBitsToAssign);
                      alreadyDispatchedToNewTouchTarget = true;
                      break;
                 }
                 ...
              }
            ...
        }
        }

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
  • 解析
    • 1.事件分发的前提是事件为DOWN事件
    • 2.遍历所有子View
    • 3.判断事件坐标是否处在当前子View坐标范围内,并且子View没有处在动画状态
    • 4.调用dispatchTransformedTouchEvent方法将事件分发给子View,如果子View捕获事件成功,则将newTouchTarget赋值给子View
2.3.步骤三具体代码实现:
     /**
    * 步骤3:根据 mFirstTouchTarget,再次分发事件
    */
        if (mFirstTouchTarget == null) {
        //TODO 1.如果mFirstTouchTarget为null,说明ViewGroup中的子view没有对事件进行捕获,
        // 调用dispatchTransformedTouchEvent方法,传入的参数child为null,最终会调用                                   // ViewGroup自身的onTouchEvent方法
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
    } else {
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            //TODO 1.1.如果当前是DOWN事件,子View捕获了事件,则进入该判断,handled设置为true并返回
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                 handled = true;
            } else {
                 final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                    //TODO 2.mFirstTouchTarget 不为null,说明有子View对touch事件进行了捕获,则直接将当前以及后续的事件交给mFirstTouchTarget指向的View进行处理
                 if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                      handled = true;
                 }
                 ...
       }
   } 

dispatchTransformedTouchEvent方法源码如下:

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

            // 如果cancel为true,则将Action设置为ACTION_CANCEL,并往下传递
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
            ...
                    
        if (child == null) {
            // child为null,则调用ViewGroup的父类(View)的事件分发方法
            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());
            }
                        // child不为null,调用子View的事件分发方法
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        return handled;
    } 
  • 解析:步骤3有2个分支判断
    • 1.如果此时mFirstTouchTarget 为null,说明上述的事件分发中并没有子View对事件进行捕获操作。
      • 这种情况下,直接调用dispatchTransformedTouchEvent 方法,并传入child为null,最终会调用super.dispatchTouchEvent方法,实际上最终会调用自身的onTouchEvent方法,进行touch事件处理。
      • 也就是说:如果没有子View捕获处理touch事件,ViewGroup会通过自身的onTouchEvent方法进行处理
    • 2.如果mFirstTouchTarget 不为null,说明ViewGourp中有子View对touch事件进行捕获,则直接将当前以及后续的事件交给mFirstTouchTarget 指向的View进行处理。
2.4.自定义控件演示touch事件分发流程
  • xml文件设置
<?xml version="1.0" encoding="utf-8"?>
<com.timmy.demopractice.touch.DownInterceptGroup xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    tools:context=".touch.TouchDemoActivity">

    <com.timmy.demopractice.touch.CaptureTouchView
        android:layout_width="300dp"
        android:layout_height="400dp"
        android:background="@color/colorAccent" />

</com.timmy.demopractice.touch.DownInterceptGroup>
  • 自定义ViewGroup和自定义View
/**
 * 查看ViewGroup 事件拦截方法
 */
public class DownInterceptGroup extends FrameLayout {
    private static final String TAG = "DownInterceptGroup";
    //事件分发
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "dispatchTouchEvent :" + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    //事件拦截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(TAG, "onInterceptTouchEvent :" + ev.getAction());
        return super.onInterceptTouchEvent(ev);
    }
}

/**
 * 自定义View,对事件进行捕获
 */
public class CaptureTouchView extends View {
    private static final String TAG = "CaptureTouchView";
    //事件分发
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "dispatchTouchEvent :" + event.getAction());
        boolean result = super.dispatchTouchEvent(event);
        Log.e(TAG, "dispatchTouchEvent result is :" + result);
        return result;
    }

    //事件捕获 -返回true,表示事件消耗
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent :" + event.getAction());
        return true;
    }
}
  • 操作及日志输出

手指触摸CaptureTouchView并滑动一段距离后谈起,最终打印log如下:

com.timmy.demopractice E/DownInterceptGroup: dispatchTouchEvent :ACTION_DOWN
com.timmy.demopractice E/DownInterceptGroup: onInterceptTouchEvent :ACTION_DOWN
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_DOWN
com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_DOWN
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true

...
com.timmy.demopractice E/DownInterceptGroup: dispatchTouchEvent :ACTION_MOVE
com.timmy.demopractice E/DownInterceptGroup: onInterceptTouchEvent :ACTION_MOVE
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_MOVE
com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_MOVE
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true
  
...
com.timmy.demopractice E/DownInterceptGroup: dispatchTouchEvent :ACTION_UP
com.timmy.demopractice E/DownInterceptGroup: onInterceptTouchEvent :ACTION_UP
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_UP
com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_UP
com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true
  • 解析
  • 手指触摸时,首先分发的是DOWN事件
    1. 并且先调用DownInterceptGroup 的事件分发方法dispatchTouchEvent,因为是DOWN事件,所以会调用DownInterceptGroup 的拦截方法onInterceptTouchEvent (默认不拦截);
    2. 又因为是DOWN事件,且DownInterceptGroup没有对事件拦截,则会遍历子View,调用dispatchTransformedTouchEvent 方法,并将事件传递给子View。在dispatchTransformedTouchEvent方法中会调用子view的事件分发方法(child.dispatchTouchEvent())
    3. 事件传递到CaptureTouchView的事件分发方法,会调用CaptureTouchView 的dispatchTouchEvent方法,在View的事件分发方法中,会调用CaptureTouchView的onTouchEvent方法,因为该方法返回true,表示子view-CaptureTouchView捕获消费了这个DOWN事件。
    4. 这种情况下,CaptureTouchView会被添加到父视图(DownInterceptGroup)中的mFirstTouchTarget 中。
  • 因此后续的MOVE和UP事件都会经过DownInterceptGroup的拦截方法onInterceptTouchEvent 进行判断。

3.特殊点

3.1.DOWN事件
  • 所有的touch事件都是从DOWN事件开始的,而且DOWN事件的处理结果会直接影响后续MOVE,UP事件的逻辑
  • 在上诉步骤2中,只有DOWN事件会传递给子View进行捕获判断,一旦子View捕获成功,后续的MOVE和UP事件是通过遍历mFirstTouchTarget链表,查找之前接受ACTION_DOWN的子View,并将触摸事件分配给这些子View。
    • 也就是说后续的MOVE,UP等事件的分发交给谁,取决于他们的起始DOWN是由谁捕获的。
3.2.mFirstTouchTarget的作用

mFirstTouchTarget的部分源码如下:

    private static final class TouchTarget {
        public static final int ALL_POINTER_IDS = -1; // all ones

        // The touched child view.
        public View child;

        // The combined bit mask of pointer ids for all pointers captured by the target.
        public int pointerIdBits;

        // The next target in the target list.
        public TouchTarget next;
      
        private TouchTarget() {
        }
    }
  • 解析
    • 从源码中可以看出 mFirstTouchTarget 是一个TouchTarget类型的链表结构。
    • 而这个mFirstTouchTarget 的作用就是用来记录捕获了DOWN事件的View,具体赋值给TouchTarget的child变量。
  • 为什么选择使用链表结构?
    • 因为Android设备是支持多指操作的,每一个手指的DOWN事件都可以当作一个TouchTarget保存起来。
    • 在步骤3中判断如果 mFirstTouchTarget不为null,则再次将事件分发给相应的TouchTarget。

4.容易被遗漏的CANCEL事件

在上面步骤3中,ViewGroup向子View分发事件的代码中,ViewGroup的dispatchTouchEvent方法中存在一段有趣的逻辑:

                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
  • 解析

    • 代码进入到这个地方,说明之前已经有子View捕获了touch事件的DOWN事件,如果变量 intercepted 的值为true时,cancelChild的值也会为true。

    • boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
      
    • 接着会调用dispatchTransformedTouchEvent方法,且参数cancelChild值为true,这种情况下,事件的主导权又重新回到父视图ViewGroup中

      dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)
      
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    ...
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
  ...
}
  • 在dispatchTransformedTouchEvent方法中,参数cancel为true,切child不为null,最终这个事件会被包装为一个ACTION_CANCEL事件传递给child
4.1.触发上述逻辑的情况
  • 在DOWN事件中,父视图ViewGroup的事件拦截方法onInterceptTouchEvent先返回false,代表不拦截down事件,down会传递给子View,子View对事件捕获(子View的dispatchTouchEvent方法返回ture),并且mFirstTouchTarget会进行赋值。
  • 在接下来的MOVE事件分发过程中,因为mFirstTouchTarget不为null,会先调用父容器的事件拦截方法onInterceptTouchEvent,如果父容器对事件进行拦截返回true,则变量intercepted 会为true
  • 此时就会出现上诉逻辑情况,子View就会收到 ACTION_CANCEL的touch事件。
4.2.经典例子
  • 当在ScrollView中添加自定义View时,ScrollView默认在DOWN事件中并不会进行拦截,事件会被传递给ScrollView内的子控件。
  • 当手指进行滑动并达到一定的距离后,ScrollView 的事件拦截方法onInterceptTouchEvent 就会返回true,并处罚ScrollView的滑动效果。
  • 当ScrollView进行滚动的瞬间,内部的子View会接收到一个CANCEL事件,并丢失touch焦点。

如下代码:

<?xml version="1.0" encoding="utf-8"?>
<com.timmy.demopractice.touch.InterceptScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/colorPrimary"
    tools:context=".touch.TouchDemoActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.timmy.demopractice.touch.CaptureTouchView
            android:layout_width="300dp"
            android:layout_height="400dp"
            android:layout_margin="40dp"
            android:background="@color/colorAccent" />

        <com.timmy.demopractice.touch.CaptureTouchView
            android:layout_width="300dp"
            android:layout_height="400dp"
            android:layout_margin="40dp"
            android:background="@color/colorAccent" />

        ...
    </LinearLayout>
</com.timmy.demopractice.touch.InterceptScrollView>
  • 自定义InterceptScrollView 和 CaptureTouchView
/**
 * 查看ViewGroup 事件拦截方法
 */
public class InterceptScrollView extends ScrollView {
    private static final String TAG = InterceptScrollView.class.getSimpleName();

    //事件分发
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(TAG, "dispatchTouchEvent :" + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    //事件拦截
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean interceptor = super.onInterceptTouchEvent(ev);
        Log.e(TAG, "onInterceptTouchEvent :" + ev.getAction() + " ,interceptor:" + interceptor);
        return interceptor;
    }
}

/**
 * 自定义View,对事件进行捕获
 */
public class CaptureTouchView extends View {
    private static final String TAG = CaptureTouchView.class.getSimpleName();

    //事件分发
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(TAG, "dispatchTouchEvent :" + event.getAction());
        boolean result = super.dispatchTouchEvent(event);
        Log.e(TAG, "dispatchTouchEvent result is :" + result);
        return result;
    }

    //事件捕获 -返回true,表示事件消耗
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(TAG, "onTouchEvent :" + event.getAction());
        return true;
    }
}
  • 解析

    • InterceptScrollView继承自ScrollView,并监听事件的分发过程;CaptureTouchView的onTouchEvent返回ture,表示他会将接收到的touch事件进行捕获消费。
    • 当手指点击屏幕时DOWN事件会被传递给 CaptureTouchView,手指滑动时InterceptScrollView会进行滑动
      • 刚开始MOVE事件还是由CaptureTouchView 来消费处理,当滚动了一段距离后,InterceptScrollView在事件拦截方法会返回true,而CaptureTouchView 会接收到一个CANCEL事件,并不再接受后续的touch事件,具体log如下:
    com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_DOWN
    com.timmy.demopractice E/InterceptScrollView: onInterceptTouchEvent :0 ,interceptor:false
    com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_DOWN
    com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_DOWN
    com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true
    ...
    com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_MOVE
    com.timmy.demopractice E/InterceptScrollView: onInterceptTouchEvent :2 ,interceptor:false
    com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_MOVE
    com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_MOVE
    com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true
    ...
    com.timmy.demopractice E/InterceptScrollView: onInterceptTouchEvent :2 ,interceptor:true
    com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent :ACTION_CANCEL
    10124-10124/com.timmy.demopractice E/CaptureTouchView: onTouchEvent :ACTION_CANCEL
    com.timmy.demopractice E/CaptureTouchView: dispatchTouchEvent result is :true
    com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_MOVE
    com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_MOVE
    com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_MOVE
    com.timmy.demopractice E/InterceptScrollView: dispatchTouchEvent :ACTION_UP
    
    总结:

    touch事件分发机制主要在ViewGroup的dispatchTouchEvent方法中,主要分3部分:

    1. 判断事件是否需要拦截 -》 主要根据onInterceptTouchEvent 方法的返回值来决定是否拦截
    2. 在DOWN事件中将touch事件分发给子View,—》这一过程如果有子View捕获消费了touch事件,会对变量 mFirstTouchTarget 进行赋值
    3. 最后一步,DOWN,MOVE,UP事件会根据 mFirstTouchTarget是否为null,决定是自己处理touch事件,还是再次分发给子View

相关文章

网友评论

    本文标题:Android事件分发

    本文链接:https://www.haomeiwen.com/subject/vmuwfltx.html