Android 事件分发机制原理分析:事件分发机制的流程;如何解决事件冲突问题;内部拦截法和外部拦截法;

256 阅读10分钟

目录

  1. 是什么?为什么要学习呢?
  2. 了解一下,事件分发是如何从点击到View这边,会经历那些类
  3. 事件分发,有那些事件?
  4. 我们写一个自定义ViewGroup和自定义View,真实了解一下事件的流程是怎么样的。
  5. dispatchTouchEvent方法的源码介绍:了解这个以后,有助于我们后面解决事件冲突
  6. 如何解决事件冲突问题?

一、是什么?为什么要学习呢?

事件分发机制指的是: Android 系统中处理用户触摸、点击、滑动等输入事件的核心流程,定义了事件如何从系统层传递到应用中的各个组件(如 ActivityViewGroupView),以及如何处理事件的消费、拦截和传递。

了解这个以后,当我们使用嵌套控件出现事件冲突的时候,就可以运用这个知识去解决;还可以实现一些自定义复杂的交互,并且事件分发机制也是常见面试题。

二、了解一下,事件分发是如何从点击到View这边,会经历哪些类?

事件的传递起点是硬件层(触摸屏驱动),经过系统服务处理后到达应用进程。大概介绍一下会经过哪些类和方法,方便我们后面知道如何对事件进行干预。

图片.png

简单总结,就是会经过这些类,最终到达我们的view:

  1. ViewRootImpl → DecorView → Activity → PhoneWindow → ViewGroup → View

接下来我们会重点介绍ViewGroup 和 View。

ViewGroup可以决定是否拦截事件,分发给子view,View最终处理事件的逻辑,因为view下面就不会再有子view了。如果他不处理,就会返回给ViewGroup来处理,如果他也不处理,那么就都不处理。

三、事件分发,有那些事件?

事件类型触发场景关键作用
ACTION_DOWN手指首次触摸屏幕时触发​事件的起点​​,若某个 View 未消费此事件,后续事件(如 MOVEUP)不会传递给它。
ACTION_MOVE手指在屏幕上滑动时触发(可能连续多次)处理滑动、拖拽等连续交互行为。
ACTION_UP手指离开屏幕时触发​事件的终点​​,通常与 ACTION_DOWN 配对使用,完成点击或滑动逻辑。
ACTION_CANCEL事件被上层拦截时触发,如父容器(如 ViewGroup)通过 onInterceptTouchEvent 拦截事件时,子 View 会收到此事件。通知子 View 停止处理当前事件,恢复初始状态(如取消高亮)。

一次完整的事件,ACTION_DOWN和ACTION_UP,只会发生一次,不管是单指,还是多指。而ACTION_MOVE可能会有0次,或者多次。后面,我们会写一个例子看看。

四、接下来,我们写一个自定义ViewGroup和自定义View,真实了解一下事件的流程是怎么样的。

4.1 案例

(1)MyFrameLayout是 ViewGroup,MyButton是子view

<?xml version="1.0" encoding="utf-8"?>
<com.example.rxjavademo2.MyFrameLayout 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:id="@+id/main"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    tools:context=".MainActivity2">

    <com.example.rxjavademo2.MyButton
        android:layout_width="200dp"
        android:background="#D01F1F"
        android:layout_height="200dp"
        android:text="hello Button"/>

</com.example.rxjavademo2.MyFrameLayout>

public class MyFrameLayout extends FrameLayout {

    private static final String TAG = "MyFrameLayout";
    public MyFrameLayout(Context context) {
        super(context);
    }

    public MyFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent: 父容器");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent: 父容器");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(TAG, "dispatchTouchEvent: 父容器");
        return super.dispatchTouchEvent(ev);
    }


}

@SuppressLint("AppCompatCustomView")
public class MyButton extends Button {
    private static final String TAG = "MyButton";
    public MyButton(Context context) {
        super(context);
    }

    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d(TAG, "dispatchTouchEvent: 子view");
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "onTouchEvent: ACTION_DOWN 子view");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "onTouchEvent: ACTION_MOVE 子view");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "onTouchEvent: ACTION_UP 子view");
                break;
            case MotionEvent.ACTION_CANCEL:
                Log.d(TAG, "onTouchEvent: ACTION_CANCEL 子view");
                break;
        }
        return super.onTouchEvent(event);
    }

}
public class MainActivity2 extends AppCompatActivity {
    private static final String TAG = "MyMainActivity2";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main2);
        MyButton myButton = findViewById(R.id.mybutton);

        myButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "onClick: ");
            }
        });
        myButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d(TAG, "onTouch: "+event.getAction());
                return false;
            }
        });
    }
}

图片.png

(2)接下来,我们点击一下控件,看会经历哪些。

图片.png

当我们点击的时候,就是按下和抬起,每一次事件,父容器都会经过dispatchTouchEvent、onInterceptTouchEvent。子view是处理的,所以他会经过dispatchTouchEvent、onTouchEvent,然后具体的事件。我们会发现父容器的onTouchEvent不会执行,因为他交给子view来处理事件了,一个事件只能有一个view来消费。

每一个事件都要经过父容器到子view,都是从上往下,上指的是父容器,下指的是子view。

了解了这个以后,我们后面就知道,如果我不想子view处理事件,就可以拦截的。后续我们会讲到。

(3)我们在看看ACTION_MOVE,我们点击的时候滑动一下。

图片.png 之前我们就说过ACTION_MOVE会经历多次。

4.2 三个方法

1. dispatchTouchEvent(MotionEvent ev)

  • ​事件分发的入口方法​​,所有触摸事件都会首先进入该方法。
  • ​决定事件是否继续传递​​,或由当前 View/ViewGroup 消费。

2. onInterceptTouchEvent(MotionEvent ev)

  • ​仅存在于 ViewGroup 中​​,用于判断是否拦截事件。
  • 默认不拦截(返回 false),子 View 优先处理事件。

3. onTouchEvent(MotionEvent event)

  • ​最终处理事件的方法​​,用于响应点击、触摸等操作。
  • 在 View 和 ViewGroup 中均可重写。

五、dispatchTouchEvent方法的源码介绍:了解这个以后,有助于我们后面解决事件冲突

5.1 ViewGroup的dispatchTouchEvent

ViewGroup的dispatchTouchEvent我们可以简单分为三个部分

(1)拦截检查​

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onFilterTouchEventForSecurity(ev)) {
        // 🌟1. 检查是否需要拦截事件(调用 onInterceptTouchEvent)
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            // 🌟允许子 View 通过 requestDisallowInterceptTouchEvent 禁止父容器拦截
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev); // 关键拦截方法
                ev.setAction(action); // 恢复事件 Action(防止被修改)
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true; // 非 DOWN 事件且无目标 View,直接拦截
        }

       ...
}

这里主要是检查,父容器是否拦截事件,如果拦截,就不会再子view中寻找可接收事件的,直接就执行的onTouchEvent方法

(2)子view的分发逻辑

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onFilterTouchEventForSecurity(ev)) {
        // 1. 检查是否需要拦截事件(调用 onInterceptTouchEvent)


        // 🌟2. 若未拦截,寻找可接收事件的子 View
        if (!canceled && !intercepted) {
            // 🌟遍历子 View,找到能处理事件的子 View(mFirstTouchTarget)
            for (int i = childrenCount - 1; i >= 0; i--) {
                final View child = getChildAt(i);
                if (child.dispatchTouchEvent(ev)) { // 递归调用子 View 的分发
                    // 找到目标子 View,记录到 mFirstTouchTarget 链表
                    break;
                }
            }
        }
.....
}

这里的逻辑就是寻找可接收事件的子 View,然后记录起来

(3)分发事件给目标子 View 或自身处理

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onFilterTouchEventForSecurity(ev)) {
        // 1. 检查是否需要拦截事件(调用 onInterceptTouchEvent)


        // 2. 若未拦截,寻找可接收事件的子 View
   

        // 🌟3. 分发事件给目标子 View 或自身处理
        if (mFirstTouchTarget == null) {
            // 🌟没有子 View 消费事件,调用自身 onTouchEvent
            handled = super.dispatchTouchEvent(ev);
        } else {
            // 🌟将事件分发给已找到的子 View(通过链表传递)
            // 处理多点触控和后续事件(如 MOVE、UP)
        }
    }
    return handled;
}

若子 View 消费了 ACTION_DOWN 事件,会被记录到 mFirstTouchTarget,后续事件(如 MOVEUP)直接分发给它。

5.2 View的dispatchTouchEvent

View 的 dispatchTouchEvent 主要处理自身的事件分发,​​没有子 View 参与​​。

(1) 优先处理 OnTouchListener
public boolean dispatchTouchEvent(MotionEvent event) {
    // 1. 检查是否设置了 OnTouchListener,并优先调用 onTouch()
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null 
        && (mViewFlags & ENABLED_MASK) == ENABLED 
        && li.mOnTouchListener.onTouch(this, event)) {
        return true; // OnTouchListener 消费事件
    }

    // 2. 若未消费,调用 onTouchEvent()
    if (onTouchEvent(event)) {
        return true; // 自身消费事件
    }
    return false; // 未消费,事件回传给父容器
}
(1) onTouchEvent

在 View.onTouchEvent 中处理具体的事件,如点击、长按等逻辑:

public boolean onTouchEvent(MotionEvent event) {
    // 检查是否可点击(如 Button 默认可点击)
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                performClick(); // 触发点击事件
                break;
            case MotionEvent.ACTION_DOWN:
                // 处理按下状态(如背景变化)
                break;
            // 其他事件处理...
        }
        return true; // 可点击 View 默认消费事件
    }
    return false;
}

5.4 接下来,我们看看三个面试题:事件冲突的

比如ViewPage+ListView结合使用,那么肯定会出现事件冲突,ViewPage是左右滑动,ListView是上下滑动,如下三种情况,属于哪种滑动?

(1)父容器onInterceptTouchEvent 返回 true 时 ,只能如何滑动?

结论,只能左右滑动,父容器消费事件。

因为onInterceptTouchEvent 返回 true,则 intercepted = true,后续流程直接跳过分发子 View。- 不再遍历子 View,直接调用 super.dispatchTouchEvent(ev)(即 View.dispatchTouchEvent),事件由父容器的 onTouchEvent 处理。

(2)父容器onInterceptTouchEvent 返回 false 时,只能如何滑动?

结论,只能上下滑动,子view消费事件。

不拦截以后,会遍历子 View,找到消费 ACTION_DOWN 的 View,并记录到 mFirstTouchTarget。后续事件直接分发给 mFirstTouchTarget 对应的子 View。

(3)父容器 onInterceptTouchEvent 返回 false,子 View dispatchTouchEvent 返回 false,只能如何滑动?

结论,左右滑动,父容器消费事件【因为我们这里用的是viewpager,他是消费了,如果是其他的,可能不消费,直接返回给activity,也就是不消费事件,因为找不到消费事件的view】

  • 若子 View 的 dispatchTouchEvent 返回 false,父容器调用自身 onTouchEvent。父容器可能在 onTouchEvent 中根据滑动方向处理事件。

六、如何解决事件冲突问题?

比如ViewPage+ListView结合使用,解决冲突就是:当用户上下滑动的时候,把时间给Listview处理,左右滑动,给ViewPager处理。

两个问题:

  1. 如何判断是上下滑动,还是左右滑动,
  2. 如何将事件给到其他view呢?

两种方法:

  1. 外部拦截法:
  2. 内部拦截法:

6.1 外部拦截法

由 ​​父容器(ViewPager)​​ 决定是否拦截事件,​​重写 onInterceptTouchEvent​。

public class CustomViewPager extends ViewPager {
    private float mStartX, mStartY;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mStartX = ev.getX();
                mStartY = ev.getY();
                // 不拦截 DOWN 事件,保证子 View 能接收后续事件
                return false;
            case MotionEvent.ACTION_MOVE:
                float dx = ev.getX() - mStartX;
                float dy = ev.getY() - mStartY;
                if (Math.abs(dx) > Math.abs(dy)) {
                    // 水平滑动,拦截事件,由 ViewPager 处理
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
}

我们知道move事件是会有多次的,所以当我们在move检测到是左右滑动时,拦截事件不让子view处理。那么就会调用自身的onTouchEvent.

外部拦截法的优点就是,逻辑简单,直接在父容器处理拦截,子 View(ListView)无需修改​​。

6.2 内部拦截法(子 View 请求父容器不要拦截)​

由 ​​子 View(ListView)​​ 通过 requestDisallowInterceptTouchEvent ​​动态请求父容器不拦截​​。

  1. ​父容器(ViewPager)默认不拦截事件​​:

    public class CustomViewPager extends ViewPager {
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            // 默认不拦截,除非子 View 允许
            return ev.getAction() == MotionEvent.ACTION_MOVE;
        }
    }
    

那么为什么不直接返回false,因为如果直接返回false,那么父容器就会一直给子view发,子view又允许父容器,但父容器就得不到执行。

  1. ​子 View(ListView)动态控制拦截​​:

    public class CustomListView extends ListView {
        private float mLastX, mLastY;
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            float x = event.getX();
            float y = event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    // 禁止父容器拦截 DOWN 事件
                    getParent().requestDisallowInterceptTouchEvent(true);
                    mLastX = x;
                    mLastY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    float dx = x - mLastX;
                    float dy = y - mLastY;
                    if (Math.abs(dx) > Math.abs(dy)) {
                        // 水平滑动时,允许父容器拦截
                        getParent().requestDisallowInterceptTouchEvent(false);
                    } 
                    mLastX = x;
                    mLastY = y;
                    break;
            }
            return super.dispatchTouchEvent(event);
        }
    }
    

    需要 在 MOVE 时根据滑动方向动态调整父容器的拦截权限,当子 View 未禁止父容器拦截时(如水平滑动),父容器仍能正常处理滑动切换,并且不给子view分发事件。