Skip to content

Android 事件分发机制面试题(带答案版)

一、基础概念类

1. 什么是 Android 事件分发机制?

答案: Android 事件分发机制是指触摸事件(MotionEvent)从屏幕产生后,在 View 层级中传递和处理的整个过程。它包括:

  • 事件产生:用户触摸屏幕产生 MotionEvent
  • 事件传递:从 Activity → Window → ViewGroup → View
  • 事件分发:决定哪个 View 处理事件
  • 事件处理:View 响应事件(如点击、滑动等)

事件类型

java
MotionEvent.ACTION_DOWN     // 手指按下
MotionEvent.ACTION_MOVE     // 手指移动
MotionEvent.ACTION_UP       // 手指抬起
MotionEvent.ACTION_CANCEL   // 事件取消
MotionEvent.ACTION_POINTER_DOWN  // 多点触摸按下
MotionEvent.ACTION_POINTER_UP    // 多点触摸抬起

2. 事件分发的三个核心方法?

答案:

  1. dispatchTouchEvent():事件分发

    • 决定事件是否继续传递
    • 返回 true:事件被消费,停止传递
    • 返回 false:事件未被消费,继续向上传递
  2. onInterceptTouchEvent():事件拦截(ViewGroup 特有)

    • 决定是否拦截事件,不传递给子 View
    • 返回 true:拦截事件,交给自己的 onTouchEvent() 处理
    • 返回 false:不拦截,继续传递给子 View
  3. onTouchEvent():事件处理

    • 实际处理触摸事件
    • 返回 true:事件被消费,不再传递
    • 返回 false:事件未消费,向上传递给父 View

方法调用顺序

Activity.dispatchTouchEvent()

PhoneWindow.superDispatchTouchEvent()

DecorView.dispatchTouchEvent()

ViewGroup.dispatchTouchEvent()

ViewGroup.onInterceptTouchEvent()

View.dispatchTouchEvent()

View.onTouchEvent()

3. 事件分发的流程是怎样的?

答案:DOWN 事件流程

Activity.dispatchTouchEvent()

PhoneWindow.superDispatchTouchEvent()

DecorView(最外层ViewGroup).dispatchTouchEvent()

ViewGroupA.dispatchTouchEvent()

ViewGroupA.onInterceptTouchEvent()  // 询问是否拦截
    ↓ 不拦截,继续向下
ViewGroupB.dispatchTouchEvent()

ViewGroupB.onInterceptTouchEvent()
    ↓ 不拦截,继续向下
View.dispatchTouchEvent()

View.onTouchEvent()

如果所有 View 都不消费 DOWN 事件,事件会反向回溯

View.onTouchEvent()返回false

ViewGroupB.onTouchEvent()返回false

ViewGroupA.onTouchEvent()返回false

Activity.onTouchEvent()

MOVE 和 UP 事件:会沿着 DOWN 事件确定的路径传递,不会重新寻找接收者。

二、核心原理类

4. ViewGroup 的 dispatchTouchEvent() 源码关键逻辑?

答案:关键源码逻辑

java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 1. 检查是否需要拦截
    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);
        } else {
            intercepted = false;
        }
    } else {
        // 不是DOWN事件且没有找到接收者,直接拦截
        intercepted = true;
    }
    
    // 2. 如果不拦截,寻找子View处理
    if (!intercepted) {
        // 遍历所有子View
        for (int i = childrenCount - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            
            // 判断子View是否能接收事件(可见、在触摸区域内)
            if (!canViewReceivePointerEvents(child) 
                || !isTransformedTouchPointInView(x, y, child, null)) {
                continue;
            }
            
            // 分发事件给子View
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                // 子View消费了事件,设置mFirstTouchTarget
                mFirstTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }
        }
    }
    
    // 3. 如果没有子View消费,自己处理
    if (mFirstTouchTarget == null) {
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    } 
    // 4. 有子View消费,继续传递
    else {
        // 将事件传递给mFirstTouchTarget
        TouchTarget target = mFirstTouchTarget;
        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
            }
            target = next;
        }
    }
    
    return handled;
}

5. mFirstTouchTarget 的作用?

答案:mFirstTouchTarget 是一个链表结构,用于记录消费了 DOWN 事件的子 View

作用

  1. 标识事件接收者:记录哪个子 View 消费了 DOWN 事件
  2. 确定事件传递路径:后续的 MOVE、UP 事件都传递给这个目标
  3. 决定是否拦截:当 mFirstTouchTarget != null 时,才会询问是否拦截

重要规则

  • 只有消费了 ACTION_DOWN 事件的 View 才会被记录到 mFirstTouchTarget
  • 如果所有 View 都不消费 DOWN 事件,mFirstTouchTarget = null
  • mFirstTouchTarget = null 时,ViewGroup 会直接拦截后续所有事件

6. requestDisallowInterceptTouchEvent() 的原理?

答案:作用:子 View 请求父 View 不要拦截事件。

原理

java
// 子View调用
parent.requestDisallowInterceptTouchEvent(true);

// ViewGroup中的实现
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if (disallowIntercept != ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;  // 设置标志位
        
        // 递归向上传递
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }
}

在 dispatchTouchEvent 中的使用

java
// 检查是否允许拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
    intercepted = onInterceptTouchEvent(ev);  // 只有标志为false时才询问拦截
} else {
    intercepted = false;  // 不允许拦截
}

重要限制

  • 只对 ACTION_DOWN 之后的事件有效:在 ACTION_DOWN 时标志位会被重置
  • 需要子 View 消费了 DOWN 事件:否则父 View 直接拦截,不会询问

典型应用:HorizontalScrollView 嵌套 ListView,在横向滑动时不让父 View 拦截。

三、拦截机制类

7. onInterceptTouchEvent() 什么时候调用?

答案:调用时机(在 ViewGroup.dispatchTouchEvent() 中):

  1. ACTION_DOWN 事件:一定会调用
  2. 后续事件:只有 mFirstTouchTarget != null 时才会调用
  3. 当子 View 调用 requestDisallowInterceptTouchEvent(true) 时,可能不调用

源码逻辑

java
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    // 这里才会调用 onInterceptTouchEvent
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
    }
} else {
    // 没有子View消费DOWN事件,直接拦截
    intercepted = true;
}

8. 如何正确使用 onInterceptTouchEvent()?

答案:正确使用模式

java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    
    switch (ev.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            // 对于DOWN事件,通常返回false
            // 因为需要让子View有机会处理点击
            intercepted = false;
            
            // 重置状态
            mIsScrolling = false;
            break;
            
        case MotionEvent.ACTION_MOVE:
            // 判断是否满足拦截条件
            if (shouldInterceptMoveEvent(ev)) {
                intercepted = true;
                mIsScrolling = true;
            }
            break;
            
        case MotionEvent.ACTION_UP:
            // 对于UP事件,如果之前没有拦截,这里也不应该拦截
            // 否则子View收不到UP事件,无法触发点击
            intercepted = mIsScrolling;
            break;
    }
    
    return intercepted;
}

重要原则

  1. ACTION_DOWN 一般返回 false:让子 View 有机会处理
  2. ACTION_MOVE 根据业务判断:如滑动距离超过阈值
  3. 一旦拦截,后续事件都交给自己的 onTouchEvent()
  4. 拦截后要消费所有事件,否则事件会丢失

9. 拦截后还能取消拦截吗?

答案:不能直接取消拦截,但可以通过以下方式实现类似效果:

方法1:重置拦截状态(在 DOWN 事件时)

java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        // 每次DOWN事件都重置拦截状态
        mHasIntercepted = false;
        return false;
    }
    
    if (!mHasIntercepted && shouldIntercept(ev)) {
        mHasIntercepted = true;
        return true;
    }
    
    return mHasIntercepted;
}

方法2:通过 requestDisallowInterceptTouchEvent() 子 View 可以在 MOVE 事件中请求父 View 不拦截:

java
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE:
            // 判断是否需要请求父View不拦截
            if (shouldRequestParentNotIntercept(event)) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            break;
            
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            // 恢复
            getParent().requestDisallowInterceptTouchEvent(false);
            break;
    }
    return true;
}

四、事件处理类

10. onTouchEvent() 和 OnTouchListener 的优先级?

答案:优先级顺序

  1. OnTouchListener.onTouch()
  2. View.onTouchEvent()

源码分析(View.dispatchTouchEvent()):

java
public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    
    // 1. 先调用 OnTouchListener
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;  // OnTouchListener 消费了事件
    }
    
    // 2. 如果 OnTouchListener 没有消费,调用 onTouchEvent
    if (!result && onTouchEvent(event)) {
        result = true;  // onTouchEvent 消费了事件
    }
    
    return result;
}

重要规则

  • 如果 OnTouchListener.onTouch() 返回 true,onTouchEvent() 不会被调用
  • 如果 OnTouchListener.onTouch() 返回 false,会继续调用 onTouchEvent()
  • 可以通过设置 OnTouchListener 来拦截事件

11. onClick() 是在哪里触发的?

答案:onClick() 是在 onTouchEvent()ACTION_UP 事件中触发的。

源码分析(View.onTouchEvent()):

java
public boolean onTouchEvent(MotionEvent event) {
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            // 记录按下时间、位置等
            break;
            
        case MotionEvent.ACTION_MOVE:
            // 判断是否滑动出有效区域
            if (!pointInView(x, y, touchSlop)) {
                // 移除点击状态
                removeTapCallback();
            }
            break;
            
        case MotionEvent.ACTION_UP:
            // 检查是否是点击
            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                // 触发点击
                performClick();
            }
            break;
    }
    return true;
}

performClick() 源码

java
public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);  // 调用 onClick
        result = true;
    } else {
        result = false;
    }
    
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

点击条件

  1. DOWN 和 UP 事件都在 View 的区域内
  2. 没有触发长按
  3. 没有滑动出有效区域(考虑 touchSlop)
  4. 设置了 OnClickListener

12. 长按事件是如何实现的?

答案: 长按事件通过 postDelayed() 延迟执行实现。

源码分析

java
// 在 ACTION_DOWN 中
case MotionEvent.ACTION_DOWN:
    mHasPerformedLongPress = false;
    
    // 检查是否支持长按
    if (!clickable) {
        checkForLongClick(0, x, y);
        break;
    }
    
    // 延迟触发长按检查
    postDelayed(mPendingCheckForLongPress,
            ViewConfiguration.getLongPressTimeout());
    break;

// 长按检查 Runnable
private final class CheckForLongPress implements Runnable {
    public void run() {
        if (performLongClick(x, y)) {
            mHasPerformedLongPress = true;
        }
    }
}

// 在 ACTION_UP 中取消长按
case MotionEvent.ACTION_UP:
    removeLongPressCallback();  // 移除长按检查
    break;

长按触发条件

  1. 手指按下超过指定时间(默认 500ms)
  2. 手指没有移动出有效区域
  3. 没有提前抬起手指

长按与点击的冲突处理

  • 如果触发了长按,mHasPerformedLongPress = true
  • 在 ACTION_UP 时,如果 mHasPerformedLongPress = true,不会触发点击

五、嵌套滑动类

13. 什么是嵌套滑动?如何实现?

答案:嵌套滑动:父 View 和子 View 都可以滑动,协同处理滑动事件。

实现方式:使用 NestedScrolling 相关接口(Android 5.0+)

核心接口

  1. NestedScrollingChild:嵌套滑动的子 View(如 RecyclerView)
  2. NestedScrollingParent:嵌套滑动的父 View(如 CoordinatorLayout)
  3. NestedScrollingChild2NestedScrollingParent2:改进版本

工作流程

1. 子View开始滑动
2. 子View询问父View是否要消费滑动距离
3. 父View消费部分滑动距离
4. 子View消费剩余滑动距离
5. 滑动结束,通知父View

代码示例(父 View):

java
public class NestedParentLayout extends ViewGroup implements NestedScrollingParent2 {
    
    @Override
    public boolean onStartNestedScroll(View child, View target, int axes, int type) {
        // 决定是否参与嵌套滑动
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }
    
    @Override
    public void onNestedScrollAccepted(View child, View target, int axes, int type) {
        // 嵌套滑动被接受
    }
    
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
        // 子View滑动前,父View先消费部分距离
        if (dy > 0 && getScrollY() < mMaxScrollY) {
            int consumeY = Math.min(dy, mMaxScrollY - getScrollY());
            scrollBy(0, consumeY);
            consumed[1] = consumeY;  // 告诉子View已消费的距离
        }
    }
    
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, 
                              int dxUnconsumed, int dyUnconsumed, int type) {
        // 子View滑动后,父View消费剩余距离
        if (dyUnconsumed < 0) {
            int consumeY = Math.max(dyUnconsumed, -getScrollY());
            scrollBy(0, consumeY);
        }
    }
    
    @Override
    public void onStopNestedScroll(View target, int type) {
        // 嵌套滑动结束
    }
}

14. 传统滑动冲突 vs 嵌套滑动?

答案:传统滑动冲突解决方案

  1. 外部拦截法:父 View 的 onInterceptTouchEvent() 中判断
  2. 内部拦截法:子 View 的 dispatchTouchEvent() 中配合 requestDisallowInterceptTouchEvent()

问题

  • 逻辑复杂,容易出错
  • 只能一个 View 消费事件
  • 滑动不连贯

嵌套滑动的优势

  1. 协同工作:父 View 和子 View 可以同时消费滑动距离
  2. 滑动连贯:用户体验更好
  3. 解耦:父 View 和子 View 独立实现
  4. 标准化:Android 官方支持

使用建议

  • Android 5.0+:使用嵌套滑动
  • 低版本:使用传统解决方案
  • 复杂场景:CoordinatorLayout + Behavior

六、滑动冲突类

15. 常见的滑动冲突场景?

答案:场景1:外部滑动方向与内部滑动方向不一致

  • 例如:ViewPager 嵌套 ListView(横向 vs 纵向)
  • 解决方案:根据滑动角度判断方向

场景2:外部滑动方向与内部滑动方向一致

  • 例如:ScrollView 嵌套 ListView(都是纵向)
  • 解决方案:根据业务逻辑分配滑动距离

场景3:多层嵌套滑动

  • 例如:DrawerLayout + ViewPager + RecyclerView
  • 解决方案:使用嵌套滑动或事件分发协调

16. 如何解决 ViewPager 嵌套 ListView 的滑动冲突?

答案:方案1:外部拦截法(推荐)

java
public class CustomViewPager extends ViewPager {
    private float mLastX;
    private float mLastY;
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        
        float x = ev.getX();
        float y = ev.getY();
        
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 不拦截DOWN,让子View有机会处理点击
                intercepted = false;
                break;
                
            case MotionEvent.ACTION_MOVE:
                float deltaX = x - mLastX;
                float deltaY = y - mLastY;
                
                // 判断滑动方向
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    // 横向滑动,拦截
                    intercepted = true;
                } else {
                    // 纵向滑动,不拦截
                    intercepted = false;
                }
                break;
                
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        
        mLastX = x;
        mLastY = y;
        
        return intercepted;
    }
}

方案2:内部拦截法

java
public class CustomListView extends ListView {
    private float mLastX;
    private float mLastY;
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        float y = ev.getY();
        
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 请求父View不拦截DOWN事件
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
                
            case MotionEvent.ACTION_MOVE:
                float deltaX = x - mLastX;
                float deltaY = y - mLastY;
                
                // 如果是纵向滑动,自己处理
                // 如果是横向滑动,让父View处理
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    // 横向滑动,允许父View拦截
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
        }
        
        mLastX = x;
        mLastY = y;
        
        return super.dispatchTouchEvent(ev);
    }
}

17. 如何解决 ScrollView 嵌套 ListView 的滑动冲突?

答案:问题:两者都是纵向滑动,ListView 无法完整滑动。

解决方案1:自定义 ListView(测量所有 Item)

java
public class NoScrollListView extends ListView {
    
    public NoScrollListView(Context context) {
        super(context);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 测量所有Item的高度
        int expandSpec = MeasureSpec.makeMeasureSpec(
            Integer.MAX_VALUE >> 2, 
            MeasureSpec.AT_MOST
        );
        super.onMeasure(widthMeasureSpec, expandSpec);
    }
}

解决方案2:禁用 ScrollView 的滑动

java
public class CustomScrollView extends ScrollView {
    private boolean mEnableScroll = true;
    
    public void setEnableScroll(boolean enable) {
        mEnableScroll = enable;
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!mEnableScroll) {
            return false;  // 不拦截,交给子View处理
        }
        return super.onInterceptTouchEvent(ev);
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!mEnableScroll) {
            return false;  // 不处理触摸事件
        }
        return super.onTouchEvent(ev);
    }
}

解决方案3:使用 NestedScrollView(推荐,Android 5.0+)

xml
<androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
        
</androidx.core.widget.NestedScrollView>

七、高级话题类

18. ACTION_CANCEL 什么时候触发?

答案:触发时机

  1. 父 View 拦截事件:子 View 收到 ACTION_DOWN 后,父 View 拦截后续事件
  2. 触摸区域改变:手指移出 View 边界
  3. 系统事件:来电、弹出对话框等系统事件中断

源码触发

java
// 在 ViewGroup.dispatchTouchEvent() 中
if (actionMasked == MotionEvent.ACTION_DOWN) {
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

// 当父View拦截时,给子View发送CANCEL
if (mFirstTouchTarget != null) {
    // 发送CANCEL给之前接收事件的子View
    MotionEvent cancelEvent = MotionEvent.obtain(ev);
    cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
    mFirstTouchTarget.child.dispatchTouchEvent(cancelEvent);
    cancelEvent.recycle();
}

处理 ACTION_CANCEL

java
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_CANCEL:
            // 重置状态,如取消高亮、停止动画等
            resetState();
            return true;
    }
    return false;
}

19. 多点触控的事件分发?

答案:事件类型

  • ACTION_POINTER_DOWN:非第一个手指按下
  • ACTION_POINTER_UP:非最后一个手指抬起

获取手指信息

java
@Override
public boolean onTouchEvent(MotionEvent event) {
    int action = event.getActionMasked();  // 使用getActionMasked()
    int pointerIndex = event.getActionIndex();  // 触发事件的手指索引
    int pointerId = event.getPointerId(pointerIndex);  // 手指ID
    
    switch (action) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_POINTER_DOWN:
            // 新手指按下
            handlePointerDown(pointerId, 
                event.getX(pointerIndex), 
                event.getY(pointerIndex));
            break;
            
        case MotionEvent.ACTION_MOVE:
            // 可能有多个手指移动
            for (int i = 0; i < event.getPointerCount(); i++) {
                int pid = event.getPointerId(i);
                float x = event.getX(i);
                float y = event.getY(i);
                handlePointerMove(pid, x, y);
            }
            break;
            
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_POINTER_UP:
            // 手指抬起
            handlePointerUp(pointerId);
            break;
    }
    return true;
}

追踪手指:使用 SparseArrayMap 存储每个手指的状态。

20. 如何实现双击事件?

答案:实现原理:通过记录两次点击的时间间隔和位置。

java
public class DoubleClickView extends View {
    private long mFirstClickTime = 0;
    private long mSecondClickTime = 0;
    private float mFirstClickX, mFirstClickY;
    private static final int DOUBLE_CLICK_INTERVAL = 300;  // 双击间隔(ms)
    private static final int DOUBLE_CLICK_DISTANCE = 20;   // 双击最大距离(像素)
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            long currentTime = System.currentTimeMillis();
            float currentX = event.getX();
            float currentY = event.getY();
            
            if (mFirstClickTime == 0) {
                // 第一次点击
                mFirstClickTime = currentTime;
                mFirstClickX = currentX;
                mFirstClickY = currentY;
            } else {
                // 第二次点击
                mSecondClickTime = currentTime;
                
                // 检查是否满足双击条件
                if (mSecondClickTime - mFirstClickTime < DOUBLE_CLICK_INTERVAL
                    && Math.abs(currentX - mFirstClickX) < DOUBLE_CLICK_DISTANCE
                    && Math.abs(currentY - mFirstClickY) < DOUBLE_CLICK_DISTANCE) {
                    
                    // 触发双击
                    performDoubleClick();
                    
                    // 重置
                    resetClickState();
                } else {
                    // 不是有效的双击,重新开始
                    mFirstClickTime = currentTime;
                    mFirstClickX = currentX;
                    mFirstClickY = currentY;
                }
            }
            
            // 延迟重置(防止间隔时间过长)
            postDelayed(() -> {
                if (System.currentTimeMillis() - mFirstClickTime > DOUBLE_CLICK_INTERVAL) {
                    resetClickState();
                }
            }, DOUBLE_CLICK_INTERVAL);
        }
        
        return true;
    }
    
    private void resetClickState() {
        mFirstClickTime = 0;
        mSecondClickTime = 0;
    }
    
    private void performDoubleClick() {
        // 触发双击事件
        if (mOnDoubleClickListener != null) {
            mOnDoubleClickListener.onDoubleClick(this);
        }
    }
}

八、性能优化类

21. 事件分发中的性能优化?

答案:优化点1:减少 onInterceptTouchEvent() 的计算

java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // 快速判断,减少计算
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        return false;  // 快速返回
    }
    
    // 复杂计算放在MOVE事件中
    if (ev.getAction() == MotionEvent.ACTION_MOVE) {
        if (shouldIntercept(ev)) {
            return true;
        }
    }
    
    return false;
}

优化点2:使用 hitRect 进行快速区域检测

java
// 在自定义ViewGroup中
private Rect mHitRect = new Rect();

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    float x = ev.getX();
    float y = ev.getY();
    
    // 快速判断是否在View范围内
    getHitRect(mHitRect);
    if (!mHitRect.contains((int)x, (int)y)) {
        return false;
    }
    
    // 继续分发...
    return super.dispatchTouchEvent(ev);
}

优化点3:避免在事件分发中创建对象

java
// 错误做法:每次事件都创建新对象
@Override
public boolean onTouchEvent(MotionEvent event) {
    PointF point = new PointF(event.getX(), event.getY());  // 创建对象
    // ...
}

// 正确做法:复用对象
private PointF mTempPoint = new PointF();

@Override
public boolean onTouchEvent(MotionEvent event) {
    mTempPoint.set(event.getX(), event.getY());  // 复用对象
    // ...
}

优化点4:使用 GestureDetector 处理复杂手势

java
private GestureDetector mGestureDetector;

public MyView(Context context) {
    super(context);
    mGestureDetector = new GestureDetector(context, new GestureListener());
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    return mGestureDetector.onTouchEvent(event);
}

private class GestureListener extends GestureDetector.SimpleOnGestureListener {
    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, 
                           float distanceX, float distanceY) {
        // 处理滑动
        return true;
    }
}

九、实战场景类

22. 如何实现一个可拖拽的 View?

答案:

java
public class DraggableView extends View {
    private float mLastX, mLastY;
    private boolean mIsDragging = false;
    
    public DraggableView(Context context) {
        super(context);
        init();
    }
    
    private void init() {
        // 设置可点击,否则可能接收不到DOWN事件
        setClickable(true);
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                mIsDragging = false;
                break;
                
            case MotionEvent.ACTION_MOVE:
                float deltaX = x - mLastX;
                float deltaY = y - mLastY;
                
                // 判断是否开始拖拽(超过阈值)
                if (!mIsDragging) {
                    float distance = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY);
                    if (distance > ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
                        mIsDragging = true;
                        // 请求父View不拦截事件
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
                
                // 执行拖拽
                if (mIsDragging) {
                    // 更新位置
                    setTranslationX(getTranslationX() + deltaX);
                    setTranslationY(getTranslationY() + deltaY);
                    
                    // 或者通过LayoutParams更新
                    // ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
                    // params.leftMargin += deltaX;
                    // params.topMargin += deltaY;
                    // setLayoutParams(params);
                }
                
                mLastX = x;
                mLastY = y;
                break;
                
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mIsDragging) {
                    // 拖拽结束处理
                    handleDragEnd();
                    mIsDragging = false;
                } else {
                    // 点击处理
                    performClick();
                }
                break;
        }
        
        return true;
    }
    
    @Override
    public boolean performClick() {
        super.performClick();
        // 处理点击
        return true;
    }
}

23. 如何实现一个侧滑删除控件?

答案:

java
public class SwipeDeleteLayout extends ViewGroup {
    private View mContentView;  // 内容View
    private View mDeleteView;   // 删除按钮View
    
    private float mLastX, mLastY;
    private boolean mIsSwiping = false;
    private int mDeleteWidth;
    private Scroller mScroller;
    
    public SwipeDeleteLayout(Context context) {
        super(context);
        init();
    }
    
    private void init() {
        mScroller = new Scroller(getContext());
        setClickable(true);
    }
    
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // 假设第一个子View是内容,第二个是删除按钮
        mContentView = getChildAt(0);
        mDeleteView = getChildAt(1);
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 测量所有子View
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        
        // 内容View的宽度就是整个View的宽度
        int width = mContentView.getMeasuredWidth();
        int height = mContentView.getMeasuredHeight();
        setMeasuredDimension(width, height);
        
        // 保存删除按钮的宽度
        mDeleteWidth = mDeleteView.getMeasuredWidth();
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 布局内容View(铺满)
        mContentView.layout(0, 0, r - l, b - t);
        
        // 布局删除按钮(在右侧外面)
        mDeleteView.layout(r - l, 0, r - l + mDeleteWidth, b - t);
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        float x = ev.getX();
        float y = ev.getY();
        
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                intercepted = false;
                break;
                
            case MotionEvent.ACTION_MOVE:
                float deltaX = Math.abs(x - mLastX);
                float deltaY = Math.abs(y - mLastY);
                
                // 判断是否是横向滑动
                if (deltaX > ViewConfiguration.get(getContext()).getScaledTouchSlop()
                    && deltaX > deltaY) {
                    intercepted = true;  // 拦截事件
                    mIsSwiping = true;
                }
                break;
        }
        
        return intercepted;
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                if (mIsSwiping) {
                    float deltaX = x - mLastX;
                    float scrollX = getScrollX() - deltaX;
                    
                    // 限制滑动范围
                    scrollX = Math.max(0, Math.min(scrollX, mDeleteWidth));
                    
                    scrollTo((int) scrollX, 0);
                    
                    mLastX = x;
                }
                break;
                
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (mIsSwiping) {
                    // 根据滑动距离决定打开还是关闭
                    int scrollX = getScrollX();
                    if (scrollX > mDeleteWidth / 2) {
                        // 打开删除按钮
                        mScroller.startScroll(scrollX, 0, 
                            mDeleteWidth - scrollX, 0, 300);
                    } else {
                        // 关闭删除按钮
                        mScroller.startScroll(scrollX, 0, 
                            -scrollX, 0, 300);
                    }
                    invalidate();
                    
                    mIsSwiping = false;
                }
                break;
        }
        
        return true;
    }
    
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
    
    // 关闭删除按钮
    public void closeDelete() {
        mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 300);
        invalidate();
    }
}

十、总结

事件分发机制的核心要点:

  1. 理解三个核心方法:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent
  2. 掌握事件传递流程:从 Activity 到 View 的传递路径
  3. 理解拦截机制:onInterceptTouchEvent 的作用和调用时机
  4. 熟悉滑动冲突解决方案:外部拦截、内部拦截、嵌套滑动
  5. 掌握常见手势实现:点击、长按、双击、拖拽等
  6. 注意性能优化:减少对象创建、快速区域检测等

这些知识点涵盖了事件分发机制的各个方面,掌握后可以应对绝大多数面试问题。