千家信息网

Android中自定义view中事件分发机制与处理的示例分析

发表于:2026-11-02 作者:千家信息网编辑
千家信息网最后更新 2026年11月02日,这篇文章将为大家详细讲解有关Android中自定义view中事件分发机制与处理的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。题引事件只有一个,多个人想要处
千家信息网最后更新 2026年11月02日Android中自定义view中事件分发机制与处理的示例分析

这篇文章将为大家详细讲解有关Android中自定义view中事件分发机制与处理的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。

    题引

    事件只有一个,多个人想要处理,处理的对象不是我们想给的对象就是事件冲突。

    如上图,RecyclerView 的父布局是ViewPager,左右滑动时没问题,上下滑动时RecyclerView好像没收到滑动事件一样,无法达到我们预期的效果。我们的触摸被封装成MotionEvent事件传递,在多个层级中它是如何传递的呢?又是根据什么来确定哪个view处理这个事件的呢,咱们抽丝剥茧一步步揭开她的面纱!

    Activity对事件的分发过程

    追溯本源,寻找事件分发的开始。

    当一个点击操作发生时,事件最先传递给当前的Activity,由Activity的dispatchTouchEvent进行分发

    public boolean dispatchTouchEvent(MotionEvent ev) {        if (ev.getAction() == MotionEvent.ACTION_DOWN) {            onUserInteraction();        }        if (getWindow().superDispatchTouchEvent(ev)) {            return true;        }        return onTouchEvent(ev);    }

    这里的getWindow返回的window类只有一个实现,PhoneWindow

     private DecorView mDecor    public boolean superDispatchTouchEvent(MotionEvent event) {        return mDecor.superDispatchTouchEvent(event);    }

    我们继续看 DecorView的superDispatchTouchEvent方法实现

    public boolean superDispatchTouchEvent(MotionEvent event) {        return super.dispatchTouchEvent(event);    }

    DecorView 继承于 ViewGroup。此时应该理解了,Activity 的 事件分发交给了 DecorView 处理,而 DecorView 又是什么

    DecorView是activity窗口的根视图,是一个FrameLayout,DecorView内部又分为两部分,一部分是ActionBar,另一部分是ContentParent,即activity在setContentView对应的布局。如此一来,事件分发从系统层面开始向我们写的布局分发事件!

    事件分发是一个递归的过程,主要涉及三个函数

    • dispatchTouchEvent

    • onInterceptTouchEvent

    • onTouchEvent

    三者关系

    public boolean dispatchTouchEvent(MotionEvent ev){        boolean result = false;        if(onInterceptTouchEvent(ev)){  //      如果拦截则交给自己的 onTouchEvent 处理事件                result = onTouchEvent(ev);        }else{                //      如果不拦截,交给子布局分发,这是个层层递归过程                result = chlid.dispatchTouchEvent(ev);        }        return result;}

    直接撸源码是一件很痛苦的事情,多种可能的发生让源码可读性很差。下面我们会从某一种特定逻辑下分析,这样会清晰很多。每次只分析一种情境!

    父布局拦截的分发处理过程

    父布局拦截我们分两步,ACTION_DOWN、ACTION_MOVE

    ACTION_DOWN 事件

    进入 ViewGroup 的 dispatchTouchEvent 方法内

       if (actionMasked == MotionEvent.ACTION_DOWN) {       // Throw away all previous state when starting a new touch gesture.       // The framework may have dropped the up or cancel event for the previous gesture       // due to an app switch, ANR, or some other state change.       cancelAndClearTouchTargets(ev);       resetTouchState();   }

    因为是 ACTION_DOWN 事件,先清空状态,一个是TouchTarget的状态,一个是 mGroupFlags。这个用不到继续走

    // Check for interception.final boolean intercepted;//      因为是第一次过来         mFirstTouchTarget = null ,且是 ACTION_DOWN 事件,走入 if 内if (actionMasked == MotionEvent.ACTION_DOWN         || mFirstTouchTarget != null) {     //        咱们走的是父布局拦截事件,子布局用尚方宝剑,disallowIntercept =false     final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;     if (!disallowIntercept) {       //      咱们在这拦截, intercepted = true         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; }

    代码注释的比较全,这边主要是判断本view是否拦截,如果拦截 intercepted = true 。所以后面的遍历子view分发都进不去

    //    intercepted = true , 进不去if (!canceled && !intercepted) {        //      这是一个遍历子 view 接盘的故事        for (int i = childrenCount - 1; i >= 0; i--) {        }}

    一直往下走

    // Dispatch to touch targets.if (mFirstTouchTarget == null) {    // No touch targets so treat this as an ordinary view.    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);}

    符合这个条件,深入 dispatchTransformedTouchEvent 函数,第三个参数是 null

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,         View child, int desiredPointerIdBits) {          final boolean handled;     if (child == null) {          //      ===== 执行位置 ====          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());          }          handled = child.dispatchTouchEvent(transformedEvent);      }}

    第三个参数传的是 null ,即 child = null 。调用 super 的 dispatchTouchEvent 。ViewGroup 的 super 即是 View。

    handled = View.dispatchTouchEvent(event);

    深入 View 的 dispatchTouchEvent 方法,主要处理逻辑是下面两段代码

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

    我们可以得出结论:onTouch 比 onTouchEvent 优先级高,如果onTouch 拦截事件则 onTouchEvent 无法接到事件。这也是为什么我们在onTouch方法返回true后onClick事件失效的原因。onTouchEvent 的逻辑比较简单,此处不做分析

    这里要说明一点,事件分发机制的分发其实有两种含义。一是事件在不同view之间的分发,父布局到子布局的分发;二是事件在view中对不同监听的分发,onTouch、onClick、onLongClick 在分发时也是有顺序的。

    到这里父布局拦截的DOWN事件算结束了,下面是MOVE事件,继DOWN后的滑动,这是个连续的过程

    ACTION_MOVE 事件

    手指点击后开始滑动,继续分发move事件

    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 {    // There are no touch targets and this action is not an initial down    // so this view group continues to intercept touches.    intercepted = true;}

    actionMasked = ACTION_MOVE ,mFirstTouchTarget = null ,直接走 else 模块,即 intercepted = true。

    // intercepted = true , 进不去if (!canceled && !intercepted) {        for (int i = childrenCount - 1; i >= 0; i--) {        }}

    同样的分发子view的模块我们依旧进不去

    // Dispatch to touch targets.if (mFirstTouchTarget == null) {    // No touch targets so treat this as an ordinary view.    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);}

    到这里就跟之前的逻辑完全一样了,至此父布局的拦截过程结束
    总结:

    • ViewGroup 的 onTouchEvent 方法直接调用父类(View)的实现

    • 父布局一旦拦截Down事件,后续的move事件都直接由父布局执行

    这么分析的好处是咱们的状态是确定的,分析代码不会有太多可能性搞乱逻辑,下面是父布局不拦截的情况下事件分发

    父布局不拦截时的分发处理过程

    父布局不拦截,咱们按照正常流程走一遍,还是按上面那个思路,先 down 后 move

    ACTION_DOWN

    进入 GroupView 的 dispatchTouchEvent 方法后依旧西先是清空状态,然后判断当前布局是否拦截

    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 {    // There are no touch targets and this action is not an initial down    // so this view group continues to intercept touches.    intercepted = true;}

    咱们的设定是不拦截,所以 intercepted = false。下面是遍历子view的代码

    final View[] children = mChildren;for (int i = childrenCount - 1; i >= 0; i--) {    final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);    //      逆序拿到一个 child ,即从最上层的子view开始往内层遍历    final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);        //      判断触点的位置是否在view的范围之内或者view是否在播放动画,如果都不满足则直接遍历下一个    if (!child.canReceivePointerEvents()|| !isTransformedTouchPointInView(x, y, child, null)) {        continue;    }    newTouchTarget = getTouchTarget(child);    if (newTouchTarget != null) {        // Child is already receiving touch within its bounds.        // Give it the new pointer in addition to the ones it is handling.        newTouchTarget.pointerIdBits |= idBitsToAssign;        break;    }    resetCancelNextUpFlag(child);    //      dispatchTransformedTouchEvent 函数是处理分发的函数,父布局处理用的也是这个    //      如果子view消费了事件则给标志位赋值,并 break 结束循环,如果没有消费则继续循环寻找分发    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {                                 注释1        // Child wants to receive touch within its bounds.        mLastTouchDownTime = ev.getDownTime();        if (preorderedList != null) {            // childIndex points into presorted list, find original index            for (int j = 0; j < childrenCount; j++) {                if (children[childIndex] == mChildren[j]) {                    mLastTouchDownIndex = j;                    break;                }            }        } else {            mLastTouchDownIndex = childIndex;        }        mLastTouchDownX = ev.getX();        mLastTouchDownY = ev.getY();        //      如果子view消费了事件则给 alreadyDispatchedToNewTouchTarget  和 mFirstTouchTarget 赋值        //      保存 child        newTouchTarget = addTouchTarget(child, idBitsToAssign);                                                  注释2        alreadyDispatchedToNewTouchTarget = true;        break;    }    // The accessibility focus didn't handle the event, so clear    // the flag and do a normal dispatch to all children.    ev.setTargetAccessibilityFocus(false);}

    分析上面干了啥

    1. 从最上层的子view开始往内层遍历

    2. 判断当前的view在位置上是否满足触点位置

    3. 调用 dispatchTransformedTouchEvent 判断是否子view消费了事件

    如果消费了事件则记录 mFirstTouchTarget 和标志位,并跳出循环

    如果没有没有消费事件则继续循环

    注释1的逻辑 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,         View child, int desiredPointerIdBits) {          final boolean handled;     if (child == null) {          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());          }          //  ===== 执行位置 ====          handled = child.dispatchTouchEvent(transformedEvent);      }}

    这次过来 child != null ,调用的是 child.dispatchTouchEvent(event) 。child 可能是View,也可能是 ViewGroup。如果是 ViewGroup 又是一个递归的过程 。层层的递归返回 handled 告诉父布局是否消费了事件!

    再看注释2的逻辑

     private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {     final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);     //        此时 mFirstTouchTarget = null     target.next = mFirstTouchTarget;     mFirstTouchTarget = target;     return target; }

    给 mFirstTouchTarget 赋值,下次 move 事件过来时 mFirstTouchTarget 就是有值的了!!即

    • target.next = null

    • mFirstTouchTarget = newTouchTarget

    • 保存 child 在 target 中

    至此 ACTION_DOWN 事件结束

    ACTION_MOVE

    继上面点击后开始滑动

    if (actionMasked == MotionEvent.ACTION_DOWN) {      cancelAndClearTouchTargets(ev);      resetTouchState();  }

    move事件不会重置,继续走

     if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)

    记得down事件中给mFirstTouchTarget 赋过值嘛,虽然不是down事件依旧可以进入此方法。也就是说这里依旧会判断父布局是否要拦截子view,这里也是以后咱们处理事件冲突的重点。当前的逻辑是不拦截,所以 intercepted = false

    if (actionMasked == MotionEvent.ACTION_DOWN       || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)        || actionMasked == MotionEvent.ACTION_HOVER_MOVE)

    只有ACTION_DOWN事件才会进行分发,所以不会进入遍历子view的逻辑代码!MOVE事件不会分发事件!

    //  mFirstTouchTarget  有值,走else模块if (mFirstTouchTarget == null) {    // No touch targets so treat this as an ordinary view.    handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} 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;        //      alreadyDispatchedToNewTouchTarget  是 false        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {            handled = true;        } else {                //      此处的结果是 false            final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;            //      在这里被分发处理 child就是我们要分发的对象            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;    }}

    alreadyDispatchedToNewTouchTarget 在每次进来时都会重置为 false ,最后又会调用 dispatchTransformedTouchEvent 处理分发

    if (child == null) {    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());    }        //      递归调用它来分发    handled = child.dispatchTouchEvent(transformedEvent);}

    至此move事件也结束,做个总结

    • DOWN 事件是事件分发,寻找接盘的 child 并保存在 mFirstTouchTarget 中

    • MOVE 事件虽然不需要遍历寻找接盘的view,但还可以被ViewGroup拦截的(比如ViewPager包裹着RecyclerView,DOWN事件时被RecyclerView拦截,横向滑动时被抛弃,这时候ViewPager是可以拦截横向滑动接盘的)

    解决冲突方案

    滑动冲突解决方案有两种:内部拦截、外部拦截。顾名思义,内部拦截是在子View中写逻辑拦截,外部拦截则是从父布局下手解决问题

    都以ViewPager包裹RecyclerView滑动冲突为例

    外部拦截

    public class BadViewPager extends ViewPager {    private int mLastX, mLastY;    public BadViewPager(@NonNull Context context) {        super(context);    }    public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {        super(context, attrs);    }    // 外部拦截法:父容器处理冲突    // 我想要把事件分发给谁就分发给谁    @Override    public boolean onInterceptTouchEvent(MotionEvent event) {        int x = (int) event.getX();        int y = (int) event.getY();        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN: {                mLastX = (int) event.getX();                mLastY = (int) event.getY();                break;            }            case MotionEvent.ACTION_MOVE: {                int deltaX = x - mLastX;                int deltaY = y - mLastY;                if (Math.abs(deltaX) > Math.abs(deltaY)) {       //      横向滑动时拦截                    return true;                }                break;            }            case MotionEvent.ACTION_UP: {                break;            }            default:                break;        }        return super.onInterceptTouchEvent(event);    }}

    内部拦截

    ViewPager 代码

    public class BadViewPager extends ViewPager {    private int mLastX, mLastY;    public BadViewPager(@NonNull Context context) {        super(context);    }    public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {        super(context, attrs);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent event) {        if (event.getAction() == MotionEvent.ACTION_DOWN){            super.onInterceptTouchEvent(event);            //      此处是重点            return false;        }        return true;    }}

    RecyclerView 代码

    public class MyListView extends ListView {    public MyListView(Context context) {        super(context);    }    public MyListView(Context context, AttributeSet attrs) {        super(context, attrs);    }    // 内部拦截法:子view处理事件冲突    private int mLastX, mLastY;    @Override    public boolean dispatchTouchEvent(MotionEvent event) {        int x = (int) event.getX();        int y = (int) event.getY();        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN: {                getParent().requestDisallowInterceptTouchEvent(true);                break;            }            case MotionEvent.ACTION_MOVE: {                int deltaX = x - mLastX;                int deltaY = y - mLastY;                if (Math.abs(deltaX) > Math.abs(deltaY)) {                    getParent().requestDisallowInterceptTouchEvent(false);                }                break;            }            case MotionEvent.ACTION_UP: {                break;            }            default:                break;        }        mLastX = x;        mLastY = y;        return super.dispatchTouchEvent(event);    }}

    此处一定要注意,父布局在 ACTION_DOWN 时一定要返回false。原因如下:

    当分发DOWN事件时,执行了 resetTouchState(); 函数

    private void resetTouchState() {        clearTouchTargets();        resetCancelNextUpFlag(this);        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;        mNestedScrollAxes = SCROLL_AXIS_NONE;    }

    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT

    在判断父布局拦截时

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

    即 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT & FLAG_DISALLOW_INTERCEPT != 0 ==》false
    使用 if 语句永远是true,在这里ViewPager会拦截事件,所以RecyclerView无法上下滑动。所以内部拦截时要修改父布局的 onInterceptTouchEvent 函数!

    关于"Android中自定义view中事件分发机制与处理的示例分析"这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。

    事件 布局 处理 逻辑 分析 过程 代码 冲突 消费 函数 方法 位置 注释 递归 状态 循环 机制 三个 又是 只有 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 亿人帮互联网科技有限公司 万易互联网科技 青岛互联网科技公司 狮子座互联网科技有限公司 互联网之后的科技是什么 顶级互联网科技成果有哪些 互联网科技公司有风险吗 崇明区互联网教育科技售后服务 中国互联网公司排名锤子科技 广州新华互联网科技学校电竞方面 西安小鹰速贷互联网科技有限公司 合肥互联网科技公司公章图片 南通创西澳互联网科技有限公司 小米互联网科技园入驻 宁波跨境商互联网科技有限公司 信承诺互联网科技有限公司 同城互联网络科技 广州三三互联网科技有限公司 安徽零酒互联网科技有限公司 恒生科技互联网指数十大成分股 互联网核心科技是什么 没上市的科技互联网公司 互联网的科技创新 老年人脱离互联网科技的作文 青岛中康互联网医疗科技有限公司 互联网科技寓意 互联网科技项目计划书材料类 全球顶尖互联网科技公司 青岛科技大学互联网大赛研究生 国大互联网教育科技有限公司工作
    0