Android事件分发机制

MotionEvent事件类型

  • Down:手指初次触摸到屏幕触发
  • Move:手指滑动触发,多次触发
  • Up:手指离开屏幕触发
  • Cancel:事件被上层拦截时触发

事件分发机制(源码)

首先,了解一下事件分发的路径:

Activity -> (PhoneWindow -> DecorView) -> ViewGroup -> View

触摸事件从Activity开始分发,几经周转,最关键的地方还是ViewGroup,具体可以看详细源码

!!!建议在看的过程中打开源码一起看!!!

事件分发

ViewGroup#dispatchTouchEvent()

接下来,主要结合源码中关键的部分带大家走一下DownMove的事件分发流程

Down

  • 第一块代码(重置Down事件状态、拦截判断)

    • 初始化Down事件,这里resetTouchState()重置了状态
    java 复制代码
    // Handle an initial down.
    if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev);
    resetTouchState();               /* `Down`事件一定!disallowIntercept = true的原因 */
    }
    • 判断父容器是否拦截事件
      • 相关方法:requestDisallowInterceptTouchEvent()onInterceptTouchEvent()
    java 复制代码
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) { /* `Down`事件一定会到这里来,原因是resetTouchState() */
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
        intercepted = false;
        }
    } else {                             /* `Down`后面的事件都会到这里来 */
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
    }
  • 第二块代码(遍历子View,分发事件)

    • 遍历子View,分发事件。如果事件被拦截,不走这块代码
    java 复制代码
    if(!canceled && !intercepted){ /* 具体代码被分为以下3部分 */ }
    • 遍历子View,构建一个装有子View的数组
    java 复制代码
    final ArrayList<View> preorderedList = buildTouchDispatchChildList(); //具体代码看源码
    • 倒叙访问,顶层优先,Android的xml布局中顶层的控件布局在最下方
    • 将事件分发给子View,看子View是否处理
    java 复制代码
    //2.倒叙访问,顶层优先。判断View能否接收事件 -VISIBLE Animtion -View是否包含触摸点
    for(){                          
        if (!child.canReceivePointerEvents()
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }
    //3.子View是否处理事件,dispatchTransformedTouchEvent()调用child.dispatchTouchEvent()
    //                                                     -> true当前child响应 
    //                                                     -> false循环下一个child
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { 
    //      返回true -> 修改参数记录,后面判断有用
    //      newTouchTarget = addTouchTarget(child, idBitsToAssign) = mFirstmFirstTouchTarget != null
    //      target.next = null
            newTouchTarget = addTouchTarget(child, idBitsToAssign); 
            alreadyDispatchedToNewTouchTarget = true;
            break;
        } 
    }
    • 当子View处理事件返回为True,参数的变化
    java 复制代码
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget; == null
        mFirstTouchTarget = target;
        return target;
    }
  • 第三块代码

    • 如果子View没有处理事件,由ViewGroup调用父类View的dispatchTouchEvent()方法
    • 进入if语句的情况:父容器拦截、子View全部不处理、第一个Move事件被取消
    java 复制代码
    if(mFirstTouchTarget == null){ /* 被拦截/子View全部不处理 */
            handled = dispatchTransformedTouchEvent(ev, canceled, null, //这里child传入null
                            TouchTarget.ALL_POINTER_IDS);               //是否处理 -> super().dispatchTouchEvnet()
    }
    • 如果子View处理了事件
    java 复制代码
    else{
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;       /* 第二块代码中子View已经处理了`Down`事件 */ 
                } else {
                    // 这段代码用于被拦截的`Move`事件取消和`Move事件的分发`,故这里不展示
                }
                predecessor = target;
                target = next;
            }
        }

Move

Move事件会多次触发

  • 第一块代码(判断Move事件是否被拦截)

    • Move事件不会像Down事件初始化,重置状态
    java 复制代码
    // Handle an initial down.
    if (actionMasked == MotionEvent.ACTION_DOWN) { /* Move事件不会进到这里初始化和重置状态 */
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    
    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 {															  
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }
  • 第二块代码

    • Move不会再分发事件
    java 复制代码
    boolean alreadyDispatchedToNewTouchTarget = false;
    if(!canceled && !intercepted){
        if (actionMasked == MotionEvent.ACTION_DOWN          /* `Move`事件不会再分发事件 */
                            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { 
        }
    }
  • 第三块代码

    • else中对Move事件进行分发
    java 复制代码
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while(target != null){     /* 单指只循环1次,while用来处理多指操作 */
         if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;	     /* `Down`事件在第二块代码中处理过了,直接handled=true */
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;  /* 判断是否被上层拦截,取消子View的`Move`事件 */
                    if (dispatchTransformedTouchEvent(ev, cancelChild, /* `Move`事件分发 */
                            target.child, target.pointerIdBits)) {
                        handled = true;
                }
                predecessor = target;
                target = next;
    }
    • 如果子View的事件(一般是Move事件,用来处理冲突)被上层拦截,或因其它原因取消,则回收当前子视图,触摸目标从下一个子视图继续
    • target是在Down事件中记录的消费事件的子View链表,用于多点触控
      • 多点触控:Button(A、B)先按下A,不松开A再按下B,然后松开A
    java 复制代码
    if (cancelChild) {
            if (predecessor == null) {
                mFirstTouchTarget = next;
            } else {
                predecessor.next = next;
            }
            target.recycle();
            target = next;
            continue;
        }
    }

事件处理

View#dispatchTouchEvent()

  • 事件处理相关方法执行顺序:onTouch()>onTouchEvent()>onClick()
java 复制代码
public boolean dispatchTouchEvent(MotionEvent event) {
    //初始result值为false
    boolean result = false;
    
	if (onFilterTouchEventForSecurity(event)) {

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {  // 优先执行自己重写的onTouch
            result = true;
        }
        
        if (!result && onTouchEvent(event)) { //由于Java的短路与,!result=false,则后面不执行
            result = true;
        }
    }
    return result;
}

public boolean onTouchEvent(MotionEvent event) {
	switch(action){
		case MotionEvent.ACTION_UP:
                performClickInternal() -> performClick() //此处为简写,源码中是这样调用过去的
    }
}

public boolean  performClick(){
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }
 return result;
}

冲突处理

对于ViewPagerListView,Google工程师是对其事件冲突做过处理的,我们不妨学一下如何处理

MyViewPagerMyListView的父容器为例 -外部:MyViewPager -内部:MyListView

内部拦截法

子控件拿到Down事件,决定Move事件是否允许父容器拦截

具体实现

  • MyViewPager中不拦截Down事件

    • 首先让我们看一种感觉可以但实际上行不通的方式,为什么内部拦截法不能只在内部重写?
    kotlin 复制代码
    class MyListView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    ) : ListView(context, attrs, defStyleAttr) {
        override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
            ev?.let {
                when (it.action) {
                    MotionEvent.ACTION_DOWN -> {
                        /* 这种方法并不能保证`Down`事件不会被父容器拦截,`Down`事件会重置状态 */
                        parent.requestDisallowInterceptTouchEvent(true)
                    }
                }
                
            }
            return super.dispatchTouchEvent(ev)
        }
    }
    • 下面才是可行的方式,因为重置状态后一定会进入onInterceptTouchEvent()方法判断拦截
    kotlin 复制代码
    class MyViewPager @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null
    ) : ViewPager(context, attrs) {
        override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
            if (ev?.action == MotionEvent.ACTION_DOWN) {
                super.onInterceptTouchEvent(ev)
                return false
            }
            return true
        }
    }
  • MyListView中拿到Down事件,决定Move事件的去向

    • 在子View的dispatchTouchEvent()方法中可以调用getParent().requestDisallowInterceptTouchEvent()方法允许父容器拦截Move事件
    kotlin 复制代码
    class MyListView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    ) : ListView(context, attrs, defStyleAttr) {
        private var mLastX = 0f;
        private var mLastY = 0f;
        override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
            ev?.let {
                when (it.action) {
                    MotionEvent.ACTION_DOWN -> {
                        /* 这种方法并不能保证`Down`事件不会被父容器拦截,`Down`事件会重置状态 */
                        parent.requestDisallowInterceptTouchEvent(true)
                        mLastX = ev.x
                        mLastY = ev.y
                    }
    
                    MotionEvent.ACTION_MOVE -> {
                        /* 作出判断,当为横向滑动时允许父容器拦截事件 */
                        if (abs(ev.x - mLastX) > abs(ev.y - mLastY)) {
                            parent.requestDisallowInterceptTouchEvent(false)
                        }
                    }
                }
    
            }
            return super.dispatchTouchEvent(ev)
        }
    
    }

Move事件流程

这里介绍的是内部拦截法Move事件的分发流程,因为感觉比较特殊

  • 第一个Move事件会被取消

    • 第一块代码:Move事件被拦截,不进入第二块代码
    java 复制代码
    if (actionMasked == MotionEvent.ACTION_DOWN) { /* Move事件不重置状态 */
        cancelAndClearTouchTargets(ev);
        resetTouchState();                               
    }
    
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) { 
            /* 调用了requestDisallowInterceptTouchEvent(false)进入这里 */
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {															   /* `Down`后面的事件都会到这里来 */
        intercepted = true;
    }
    • 第三块代码:Move事件被取消,更新mFirstTouchTarget = null

    其实也挺合乎道理,你从整体上看事件处理的对象:MyListView->MyViewPager,那么记录的触摸目标是不是也要改变,要重新给它赋值,所以先把它置为空

    java 复制代码
    if (mFirstTouchTarget == null) {    /* mFirstTouchEvent!=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; /* =null */
            /* 第二块代码前alreadyDispatchedToNewTouchTarget=false,第二块代码中修改为true,第一个`Move`不曾进入第二块代码 */
            //  alreadyDispatchedToNewTouchTarget = false
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { 
                handled = true;
            } else {
            /* intercepted = true -> cancelChild = true -> 第一个`Move`事件被取消 */
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next; /* =null */ 
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }
    
    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);    /* listView -> `Move`事件Cancel处理 */
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event); 
            }
            event.setAction(oldAction);
            return handled;
        }
    }
  • 第二个Move事件找到处理对象

    • 第一块代码:父容器MyViewPager拦截Move事件,不走第二块代码
    java 复制代码
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {/* `Move`事件且前一个`Move`事件置空触摸目标,走else */
    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 { /* 第一个Move事件被子View取消,第二个`Move`事件直接被父容器拦截,不走第二块代码 */
        intercepted = true;
    }
    • 第三块代码:父容器MyViewPagerMove事件交给自己处理
    java 复制代码
    if(mFirstTouchTarget == null){  /* 三种情况:被拦截/子View全部不处理/第一个Move事件取消 */
        handled = dispatchTransformedTouchEvent(ev, canceled, null, 
                        TouchTarget.ALL_POINTER_IDS);          

总结(内部拦截法实现原理)

子View必须拿到Down事件才能处理Move事件,父容器可以在没有拿到Down事件的情况下由子View调用requestDisallowInterceptTouchEvent()被允许拦截Move事件

外部拦截法

由父容器根据情况判断是否拦截事件

  • 重写父容器MyViewPageronInterceptTouchEvent()方法
kotlin 复制代码
class MyViewPager @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : ViewPager(context, attrs) {
    private var mLastX = 0f;
    private var mLastY = 0f;
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        super.onInterceptTouchEvent(ev)
        ev?.let {
            when (it.action) {
                MotionEvent.ACTION_DOWN -> {
                    mLastX = ev.x
                    mLastY = ev.y
                }
                /* 根据情况判断是否拦截事件,如果是横向滑动则拦截事件 */
                MotionEvent.ACTION_MOVE -> {
                    if (abs(ev.x - mLastX) > abs(ev.y - mLastY)) {
                        return true
                    }
                }

            }
        }
        return false
    }
}

事件分发机制(图解)

图解的话,直接看不容易懂,还是建议先看源码,再结合图理清思路

我的分享到这里就结束啦,有问题的地方欢迎大佬指正,喜欢的小伙伴可以点个赞赞哦^_^

相关推荐
仙魁XAN3 小时前
Unity 之 【Android Unity FBO渲染】之 [Unity 渲染 Android 端播放的视频] 的一种方法简单整理
android·unity·共享纹理·fbo·视频渲染
陆业聪4 小时前
从状态管理到性能优化:全面解析 Android Compose
android·ui
轻口味7 小时前
Android JobScheduler介绍
android
技术无疆7 小时前
TitleBar:打造高效Android标题栏的新选择
android·java·ui·android studio·android-studio
时差freebright11 小时前
【Visual Studio 报错】vs 在使用二进制写入文件时弹窗报错:使用简体中文 gb2312 编码加载文件
android·java·visual studio
兜兜 里 有糖12 小时前
laravel 查询数据对象转数组
android·java·laravel
薛文旺13 小时前
Android生成Java AIDL
android
人民的石头13 小时前
Android 源码多个Launcher设置默认Launcher
android
jiet_h13 小时前
Android Kotlin 中的 `groupBy` 方法详解
android·开发语言·kotlin
ccy加油呀13 小时前
pytest 接口测试
android·pytest