Android View的事件分发机制

23 阅读9分钟

一文读懂Android触摸事件分发机制:从源码到实战,彻底搞定滑动冲突


前言

事件分发是Android开发中的"拦路虎",很多工作2-3年的开发者也未必能说清楚。网上文章虽多,但要么太理论化,要么太源码化,看完就忘。

今天,我用讲故事 + 源码分析 + 实战案例的方式,带你彻底搞懂事件分发。

读完这篇文章,你将收获:

  • ✅ 彻底理解三个核心方法的作用和关系
  • ✅ 掌握事件分发的完整流程(向下传递 + 向上回传)
  • ✅ 看懂核心源码,不再死记硬背
  • ✅ 轻松解决各种滑动冲突
  • ✅ 面试时侃侃而谈,惊艳面试官

一、故事开篇:一家公司的任务派发

想象你在一家公司工作,公司结构是这样的:

老板(Activity)
    └── 部门经理(Window)
        └── 项目组长(ViewGroup,比如LinearLayout)
            └── 程序员(View,比如Button)

某天,客户提了一个需求(用户点了一下屏幕),这个任务需要从老板一层层下派,找到能干的员工。

这家公司有三条奇葩规定:

规定对应方法作用
任务派发制度dispatchTouchEvent"这活儿谁来干?"
中途拦截制度onInterceptTouchEvent"这活儿我中途截了,自己干!"(只有组长及以上有这权力)
任务执行制度onTouchEvent"行,这事儿我来搞定!"

这三者的关系,用伪代码一看就懂:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onInterceptTouchEvent(ev)) {  // 要不要中途拦截?
        return onTouchEvent(ev);       // 拦截了,自己处理
    } else {
        return child.dispatchTouchEvent(ev); // 不拦截,下发给小弟
    }
}

二、三个核心方法详解

1. dispatchTouchEvent - 分发者

职责:  事件分发的总入口,决定事件是"自己处理"还是"往下传"。

调用时机:  只要事件能传递到当前View,此方法一定会被调用。

返回值:  true 表示事件被消耗,false 表示没有被消耗(会向上回传)。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 你可以在这里做任何预处理
    Log.d("TAG", "事件到达了:" + this.getClass().getSimpleName());
    
    // 调用父类执行默认分发逻辑
    return super.dispatchTouchEvent(ev);
}

2. onInterceptTouchEvent - 拦截者(只有ViewGroup有)

职责:  决定是否拦截事件,拦截后事件就不再往下传。

调用时机:  在dispatchTouchEvent内部被调用,不是每次事件都会调用。

返回值:  true 表示拦截,false 表示不拦截(默认)。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    switch(ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 通常在DOWN时做初始化
            return false;  // 默认不拦截
            
        case MotionEvent.ACTION_MOVE:
            // 根据滑动距离决定是否拦截
            if (Math.abs(deltaX) > touchSlop) {
                return true;  // 拦截,自己处理
            }
            break;
    }
    return super.onInterceptTouchEvent(ev);
}

3. onTouchEvent - 处理者

职责:  实际处理事件,比如点击、滑动等。

调用时机:  在dispatchTouchEvent内部被调用(要么自己被调用,要么子View返回false后自己被调用)。

返回值:  true 表示消耗了事件,false 表示没消耗(会向上回传)。

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch(event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 手指按下
            return true;  // 我要处理这个事件序列
            
        case MotionEvent.ACTION_MOVE:
            // 手指移动
            performDrag(event);
            return true;
            
        case MotionEvent.ACTION_UP:
            // 手指抬起
            performClick();
            return true;
    }
    return super.onTouchEvent(event);
}

三、完整流程图解

流程图1:正常情况(Button响应点击)

deepseek_mermaid_20260416_032365.png

流程图2:拦截情况(ViewGroup抢活)

deepseek_mermaid_20260416_941d3b.png

流程图3:向上回传(Button搞不定)

deepseek_mermaid_20260416_5e09b3.png


四、核心规则(面试必背)

规则1:事件序列 = 一个完整的手势

从手指按下(DOWN)到抬起(UP),中间可能有多个移动(MOVE),这叫做一个事件序列

ACTION_DOWN → ACTION_MOVE → ACTION_MOVE → ... → ACTION_MOVE → ACTION_UP

重要:  同一个序列的事件,通常会交给同一个View处理。

规则2:一旦拦截,全程接管 ⭐⭐⭐

这是最重要的规则!

如果某个ViewGroup拦截了任意一个事件(DOWN、MOVE或UP),那么这个序列的后续所有事件都归它处理:

  • ❌ 不会再往下传给子View
  • ❌ 不会再调用onInterceptTouchEvent(不用再问了)
// 示例:在MOVE时拦截
case MotionEvent.ACTION_MOVE:
    if (Math.abs(deltaX) > 10) {
        return true;  // 拦截!
    }
    // 后果:后续的MOVE和UP都不会再传给子View了

规则3:不消费DOWN,后果很严重 ⭐⭐⭐

如果一个View的onTouchEvent在DOWN事件时返回false

  • ❌ 这个序列的后续事件(MOVE、UP)都不会再给它了
  • 🔄 事件会回传给父View处理

为什么?  因为ViewGroup内部维护了一个mFirstTouchTarget链表,只有消费了DOWN的View才会被记录进去。

// 源码验证
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
    // 只有子View返回true,才会被记录
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
}

// 后续事件判断
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    // 有记录才会继续分发
} else {
    intercepted = true;  // 没有记录,直接拦截,不再分发
}

规则4:View默认"能吃下"事件

只要View满足以下任一条件:

  • clickable = true(可点击)
  • longClickable = true(可长按)

那么它的onTouchEvent默认返回true(消耗事件)。

注意:  enable = false(禁用状态)也不影响这个规则!

// Button默认clickable = true
// TextView默认clickable = false

button.setOnTouchListener(...);  // 会自动设置clickable = true

规则5:优先级(重要!)

OnTouchListener.onTouch > onTouchEvent > OnClickListener.onClick
button.setOnTouchListener(new OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // 如果这里返回true,onTouchEvent和onClick都不会执行
        return true;
    }
});

button.setOnClickListener(v -> {
    // 只有当onTouch返回false,且onTouchEvent处理了,才会执行
});

规则6:子View可以请求父View别拦截

// 在子View中调用
getParent().requestDisallowInterceptTouchEvent(true);

作用:  告诉父View:"这活我自己干,你别抢!"

注意:  这招对DOWN事件无效,因为每次DOWN事件开始时,系统会重置这个标志位。

// 源码证明
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // DOWN事件会重置状态
    cancelAndClearTouchTargets(ev);
    resetTouchState();  // 这里会清除FLAG_DISALLOW_INTERCEPT标志
}

五、源码核心解读

1. Activity → ViewGroup

// Activity.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;  // 有人处理了
    }
    return onTouchEvent(ev);  // 没人处理,Activity自己处理
}

// PhoneWindow.superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);  // mDecor是顶级View
}

2. ViewGroup.dispatchTouchEvent(核心逻辑)

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 1. 判断是否拦截
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);  // 询问是否拦截
        } else {
            intercepted = false;
        }
    } else {
        intercepted = true;  // 没有目标且不是DOWN,直接拦截
    }
    
    // 2. 如果不拦截,分发给子View
    if (!intercepted) {
        for (int i = childrenCount - 1; i >= 0; i--) {
            final View child = children[i];
            if (isTransformedTouchPointInView(x, y, child, null)) {
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    // 子View处理了,记录下来
                    mFirstTouchTarget = addTouchTarget(child, idBitsToAssign);
                    break;
                }
            }
        }
    }
    
    // 3. 如果没有子View处理,自己处理
    if (mFirstTouchTarget == null) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null, ...);
    }
    
    return handled;
}

3. View.dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    
    // 优先给OnTouchListener处理
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;  // OnTouchListener消费了
    }
    
    // OnTouchListener没消费,交给onTouchEvent
    if (!result && onTouchEvent(event)) {
        result = true;
    }
    
    return result;
}

六、实战:解决滑动冲突

场景1:ViewPager + RecyclerView(经典嵌套滑动)

问题描述:  ViewPager里嵌套了纵向滚动的RecyclerView,用户斜着滑时,两个控件都想抢事件。

期望行为:

  • 左右滑 → ViewPager翻页
  • 上下滑 → RecyclerView滚动

解决方案:  在RecyclerView中判断滑动方向

public class CustomRecyclerView extends RecyclerView {
    private float lastX, lastY;
    private int touchSlop;
    
    public CustomRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = ev.getX();
                lastY = ev.getY();
                // DOWN事件:先请求父View不要拦截
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
                
            case MotionEvent.ACTION_MOVE:
                float deltaX = ev.getX() - lastX;
                float deltaY = ev.getY() - lastY;
                
                // 判断滑动方向
                if (Math.abs(deltaX) > Math.abs(deltaY) + touchSlop) {
                    // 横向滑动:交给ViewPager处理
                    getParent().requestDisallowInterceptTouchEvent(false);
                    return false;  // 自己不处理
                } else if (Math.abs(deltaY) > touchSlop) {
                    // 纵向滑动:自己处理,阻止父View拦截
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

场景2:可滑动的ViewPager + 可滑动的内部View(水平冲突)

问题描述:  ViewPager里嵌套了横向滚动的RecyclerView,用户想滑动内部列表时,ViewPager总是抢走事件。

期望行为:

  • 在内部RecyclerView上滑动 → 内部列表滚动
  • 在空白区域滑动 → ViewPager翻页

解决方案:  使用requestDisallowInterceptTouchEvent

public class HorizontalRecyclerView extends RecyclerView {
    
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 告诉ViewPager:先别拦截,让我看看
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
                
            case MotionEvent.ACTION_MOVE:
                // 如果已经滑动到边界,让ViewPager处理
                if (!canScrollHorizontally(deltaX > 0 ? 1 : -1)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
        }
        return super.onTouchEvent(e);
    }
}

场景3:自定义下拉刷新布局

public class PullToRefreshLayout extends FrameLayout {
    private float lastY;
    private boolean isRefreshing = false;
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (isRefreshing) {
            return false;  // 刷新中,不拦截
        }
        
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastY = ev.getY();
                return false;  // 不拦截,让子View先处理
                
            case MotionEvent.ACTION_MOVE:
                float deltaY = ev.getY() - lastY;
                // 下拉且子View已经到顶部,拦截事件自己处理
                if (deltaY > 20 && !canChildScrollUp()) {
                    return true;  // 拦截,开始下拉刷新
                }
                break;
        }
        return false;
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 处理下拉刷新的动画逻辑
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                float deltaY = event.getY() - lastY;
                // 移动下拉头
                moveHeader(deltaY);
                return true;
                
            case MotionEvent.ACTION_UP:
                // 执行刷新或回弹
                if (isReadyToRefresh()) {
                    startRefresh();
                } else {
                    resetHeader();
                }
                return true;
        }
        return super.onTouchEvent(event);
    }
    
    private boolean canChildScrollUp() {
        View child = getChildAt(0);
        if (child instanceof ViewGroup) {
            return child.canScrollVertically(-1);
        }
        return false;
    }
}

七、常见问题 FAQ

Q1:onInterceptTouchEvent 为什么不是每次事件都会被调用?

A:因为一旦ViewGroup决定拦截,后续事件就不会再调用它了。

// 源码中的判断
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    intercepted = onInterceptTouchEvent(ev);  // 只有这里会调用
} else {
    intercepted = true;  // 直接拦截,不调用
}

Q2:requestDisallowInterceptTouchEvent 为什么对 DOWN 无效?

A:因为每次DOWN事件开始时,系统会重置状态。

if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev);
    resetTouchState();  // 这里会清除 FLAG_DISALLOW_INTERCEPT
}

Q3:一个View既设置了OnTouchListener又设置了OnClickListener,会怎样?

A:先执行OnTouchListener.onTouch,如果返回false才执行onTouchEvent,然后才可能触发onClick

// 执行顺序
OnTouchListener.onTouch() 
    → 如果返回false → onTouchEvent() 
        → 如果消费了且是UP事件 → onClick()

Q4:View的enable=false时,还能收到点击事件吗?

A:能收到,但不会响应点击。只要clickable=trueonTouchEvent仍然返回true

// 源码验证
if ((viewFlags & ENABLED_MASK) == DISABLED) {
    // 即使disabled,只要clickable或longClickable为true,仍然返回true
    return (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}

Q5:如何调试事件分发问题?

A:使用adb命令或自定义日志。

// 方法1:打印事件日志
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Log.d("Event", getClass().getSimpleName() + " dispatch: " + ev.getAction());
    return super.dispatchTouchEvent(ev);
}

// 方法2:使用dumpsys
adb shell dumpsys input

// 方法3:使用Layout Inspector查看View层级

八、记忆口诀

事件分发三方法,分发拦截和处理
Activity先收到,一层一层往下递

领导能拦也能放,员工能吃不放弃
一旦拦截全接管,后面不再问你哩

搞不定就往回传,最终老板兜个底
监听优先于回调,onClick排最后滴

DOWN事件最特殊,不消费就没后续
子View想抢控制权,请求父View别拦截

九、总结对比表

对比项dispatchTouchEventonInterceptTouchEventonTouchEvent
谁有View和ViewGroup只有ViewGroupView和ViewGroup
作用分发事件拦截事件处理事件
默认返回值取决于内部逻辑falsetrue(如果clickable)
调用时机每次事件都会有条件地调用事件交给当前View时
是否必须实现一般不重写根据需要重写根据需要重写
规则说明
传递顺序Activity → Window → ViewGroup → View(由外向内)
回传顺序View → ViewGroup → Window → Activity(由内向外)
拦截后果后续事件全部自己处理,不再往下传
DOWN不消费后续事件收不到,向上回传
优先级OnTouchListener > onTouchEvent > OnClickListener
默认消费clickable或longClickable为true时,onTouchEvent返回true