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
    }
}

事件分发机制(图解)

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

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

相关推荐
SRC_BLUE_1738 分钟前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道4 小时前
Android打包流程图
android
镭封5 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰4875 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛6 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二6 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
奶茶喵喵叫6 小时前
Android开发中的隐藏控件技巧
android
Winston Wood8 小时前
Android中Activity启动的模式
android
众乐认证8 小时前
Android Auto 不再用于旧手机
android·google·智能手机·android auto
三杯温开水8 小时前
新的服务器Centos7.6 安卓基础的环境配置(新服务器可直接粘贴使用配置)
android·运维·服务器