Skip to content

03_事件分发机制详解

一、Android 事件体系概述

1.1 事件分类

Android 中的输入事件主要分为以下几类:

事件类型说明典型场景
MotionEvent触摸事件点击、滑动、长按
KeyEvent键盘事件物理按键、软键盘
ClipboardEvent剪贴板事件复制粘贴
TrackballEvent轨迹球事件老式设备导航

1.2 事件分发与回调

事件分发机制:从上到下传递事件
事件回调机制:从下到上响应事件

1.3 事件流程概览

Activity

Window

PhoneWindow

DecorView

ViewGroup (布局)

View (子视图)

二、MotionEvent 事件详解

2.1 事件动作类型

java
// MotionEvent 的动作类型
MotionEvent.ACTION_DOWN      // 手指按下
MotionEvent.ACTION_MOVE      // 手指移动
MotionEvent.ACTION_UP        // 手指抬起
MotionEvent.ACTION_CANCEL    // 事件取消
MotionEvent.ACTION_OUTSIDE   // 触摸区域外抬起

// 多点触控
MotionEvent.ACTION_POINTER_DOWN  // 第二个手指按下
MotionEvent.ACTION_POINTER_UP    // 第二个手指抬起

2.2 事件对象结构

java
public class MotionEvent {
    private long mEventTime;      // 事件发生时间
    private long mDownTime;       // 第一个 DOWN 的时间
    private int mAction;          // 动作类型
    private int mMetaState;       // 修饰键状态
    private int mSource;          // 事件来源
    private int mEdgeFlags;       // 边缘标志
    private float mX;             // X 坐标
    private float mY;             // Y 坐标
    private float mPressure;      // 压力值
    private float mSize;          // 接触面积
    private float mTouchMajor;    // 主要接触轴
    private float mTouchMinor;    // 次要接触轴
    private int mToolType;        // 工具类型
    private int mPointerCount;    // 指针数量
    // ...
}

2.3 获取事件信息

java
public class TouchInfoView extends View {
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 获取动作类型
        int action = event.getAction();
        
        // 获取 X、Y 坐标
        float x = event.getX();
        float y = event.getY();
        
        // 获取原始坐标(相对于屏幕)
        float rawX = event.getRawX();
        float rawY = event.getRawY();
        
        // 获取指针数量(多点触控)
        int pointerCount = event.getPointerCount();
        
        // 获取特定指针的 ID
        int pointerId = event.getPointerId(0);
        
        // 获取特定指针的坐标
        float pointerX = event.getX(0);
        float pointerY = event.getY(0);
        
        // 获取压力值(0.0 - 1.0)
        float pressure = event.getPressure();
        
        // 获取事件时间
        long eventTime = event.getEventTime();
        long downTime = event.getDownTime();
        
        return super.onTouchEvent(event);
    }
}

2.4 多点触控处理

java
public class MultiTouchView extends View {
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int pointerCount = event.getPointerCount();
        
        for (int i = 0; i < pointerCount; i++) {
            int pointerId = event.getPointerId(i);
            float x = event.getX(i);
            float y = event.getY(i);
            
            // 处理每个指针
            Log.d("MultiTouch", "Pointer " + pointerId + ": (" + x + ", " + y + ")");
        }
        
        return true;
    }
}

三、事件分发三大方法

3.1 dispatchTouchEvent 事件分发

java
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 1. 调用回调
    if (onInterceptTouchEvent(ev)) {
        // 2. 拦截事件,调用自己的 onTouchEvent
        return onTouchEvent(ev);
    }
    // 3. 不拦截,分发给子 View
    return super.dispatchTouchEvent(ev);
}

3.1.1 View 的 dispatchTouchEvent

java
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    // 1. 检查是否可用
    if (mListener != null) {
        if (mListener.onTouchEvent(event)) {
            return true;
        }
    }
    
    // 2. 检查触摸监听器
    if (onTouchEvent(event)) {
        return true;
    }
    
    // 3. 检查点击监听器
    if (mPrivateFlags & PFLAG_CLICKABLE) != 0) {
        // 处理点击事件
        return performClick();
    }
    
    return false;
}

3.1.2 ViewGroup 的 dispatchTouchEvent

java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 事件预处理
    if (onFilterTouchEventForSecurity(ev)) {
        return false;
    }
    
    // 调用点击监听器
    if (mListener != null) {
        if (mListener.onTouchEvent(ev)) {
            return true;
        }
    }
    
    // 获取动作和指针 ID
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;
    
    // DOWN 事件:寻找合适的子 View
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        mTouchTarget = null;
        // 从上到下寻找可接收事件的子 View
        for (int i = getChildCount() - 1; i >= 0; i--) {
            View child = getChildAt(i);
            if (isEventWithin(child, ev)) {
                if (child.dispatchTouchEvent(ev)) {
                    mTouchTarget = child;
                    return true;
                }
            }
        }
    }
    // MOVE 事件:根据拦截结果分发
    else if (actionMasked == MotionEvent.ACTION_MOVE) {
        // 检查是否需要拦截
        if (onInterceptTouchEvent(ev)) {
            // 拦截,分发给自己
            return onTouchEvent(ev);
        }
        // 不拦截,继续分发给 mTouchTarget
        if (mTouchTarget != null) {
            return mTouchTarget.dispatchTouchEvent(ev);
        }
    }
    // UP/CANCEL 事件
    else if (actionMasked == MotionEvent.ACTION_UP 
            || actionMasked == MotionEvent.ACTION_CANCEL) {
        if (mTouchTarget != null) {
            mTouchTarget.dispatchTouchEvent(ev);
            mTouchTarget = null;
        }
    }
    
    return super.dispatchTouchEvent(ev);
}

3.2 onInterceptTouchEvent 事件拦截

java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // 返回 true:拦截事件,后续事件都交给自己的 onTouchEvent 处理
    // 返回 false:不拦截,事件继续向下传递
    return false;
}

3.2.1 拦截时机

java
public class InterceptViewGroup extends ViewGroup {
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 在 DOWN 时决定是否拦截
                // 一旦拦截,后续的 MOVE、UP 都会交给自己的 onTouchEvent
                return false; // 不拦截
            case MotionEvent.ACTION_MOVE:
                // 在 MOVE 时可以改变决定
                if (shouldIntercept(ev)) {
                    return true; // 拦截
                }
                return false;
            default:
                return super.onInterceptTouchEvent(ev);
        }
    }
    
    private boolean shouldIntercept(MotionEvent ev) {
        // 判断是否需要拦截的逻辑
        return false;
    }
}

3.2.2 拦截注意事项

1. DOWN 事件如果返回 false,后续可能不会再调用 onInterceptTouchEvent
2. 拦截后,之前的子 View 会收到 CANCEL 事件
3. 拦截后,所有后续事件都会交给自己的 onTouchEvent

3.3 onTouchEvent 事件处理

java
@Override
public boolean onTouchEvent(MotionEvent event) {
    // 处理触摸事件
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // 处理按下
            break;
        case MotionEvent.ACTION_MOVE:
            // 处理移动
            break;
        case MotionEvent.ACTION_UP:
            // 处理抬起
            break;
        case MotionEvent.ACTION_CANCEL:
            // 处理取消
            break;
    }
    return true; // 消费事件
}

3.3.1 消费事件

java
public class ConsumerView extends View {
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 返回 true:消费事件,事件结束
        // 返回 false:不消费,事件向上回溯
        return true;
    }
}

四、完整事件流程图

4.1 事件传递路径

Activity.dispatchTouchEvent()

Window.superDispatchTouchEvent()

PhoneWindow.superDispatchTouchEvent()

DecorView.dispatchTouchEvent()

ViewGroup.dispatchTouchEvent()
   ├─→ onInterceptTouchEvent() = true
   │    ↓
   │  onTouchEvent()

   └─→ onInterceptTouchEvent() = false

      child.dispatchTouchEvent()

      child.onTouchEvent()

4.2 事件处理顺序

1. Activity.dispatchTouchEvent()
2. Window.superDispatchTouchEvent()
3. DecorView.dispatchTouchEvent()
4. ViewGroup.dispatchTouchEvent()

5. ViewGroup.onInterceptTouchEvent() ← 拦截点
   ↓ (如果拦截)
6. ViewGroup.onTouchEvent()
   ↓ (如果不拦截)
7. ChildView.dispatchTouchEvent()

8. ChildView.onTouchEvent()

4.3 事件回溯机制

当子 View 返回 false 时:
父 ViewGroup.onTouchEvent() ← 回溯

继续向上回溯...

五、滑动冲突与解决

5.1 冲突类型

5.1.1 外部嵌套冲突

父容器可滚动,子 View 也可滚动
例如:ScrollView 嵌套 ListView

5.1.2 内部嵌套冲突

子 View 之间竞争事件
例如:ViewPager 内部嵌套横向滚动的 View

5.2 经典冲突场景

5.2.1 ScrollView 嵌套 RecyclerView

xml
<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:orientation="vertical">

        <TextView android:text="头部内容"/>
        
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <TextView android:text="底部内容"/>

    </LinearLayout>

</ScrollView>

5.2.2 ViewPager 嵌套 ScrollView

xml
<ViewPager
    android:id="@+id/viewPager"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!-- 可滚动内容 -->
    </ScrollView>

</ViewPager>

5.2.3 自定义 View 冲突

java
public class SwipeRefreshLayout extends FrameLayout {
    // 下拉刷新与内部列表的滚动冲突
}

5.3 解决方案

5.3.1 方案一:父控件拦截法

让父控件在适当的时候拦截事件:

java
public class CustomScrollView extends ScrollView {
    
    private View mChildView;
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录 DOWN 时的 Y 坐标
                mLastY = ev.getY();
                // 找到可能接收事件的子 View
                mChildView = findTargetChild(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                float currentY = ev.getY();
                float deltaY = currentY - mLastY;
                
                // 如果子 View 可以滚动,且滚动方向与父 View 一致
                if (mChildView != null && canChildScroll(deltaY)) {
                    // 不拦截,让子 View 处理
                    return false;
                }
                // 否则拦截,父 View 处理
                return true;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
    
    private View findTargetChild(MotionEvent ev) {
        // 找到点击区域的子 View
        return null;
    }
    
    private boolean canChildScroll(float deltaY) {
        // 判断子 View 是否可以滚动
        if (mChildView instanceof NestedScrollingChild) {
            return ((NestedScrollingChild) mChildView)
                    .canScrollVertically((int) deltaY);
        }
        return false;
    }
}

5.3.2 方案二:子控件拦截法

让子控件在需要时请求父控件不拦截:

java
public class CustomRecyclerView extends RecyclerView {
    
    private OnTouchListener mOuterTouchListener;
    
    public void setOnTouchListener(OnTouchListener listener) {
        mOuterTouchListener = listener;
    }
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 如果父容器请求拦截,交给父容器处理
        if (mOuterTouchListener != null) {
            if (mOuterTouchListener.onTouch(this, ev)) {
                return true;
            }
        }
        return super.dispatchTouchEvent(ev);
    }
}

5.3.3 方案三:同时处理法

父控件和子控件同时处理事件:

java
public class NestedViewGroup extends ViewGroup {
    
    private float mLastX, mLastY;
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = ev.getX();
                mLastY = ev.getY();
                return false; // 不拦截,让子 View 先处理
            case MotionEvent.ACTION_MOVE:
                float dx = ev.getX() - mLastX;
                float dy = ev.getY() - mLastY;
                
                // 判断滑动方向
                if (Math.abs(dx) > Math.abs(dy)) {
                    // 横向滑动,拦截
                    return true;
                } else {
                    // 纵向滑动,不拦截
                    return false;
                }
            default:
                return false;
        }
    }
}

5.3.4 方案四:NestedScrolling 机制

Android 5.0 引入的标准解决方案:

java
// 父 View 实现 NestedScrollingParent
public class NestedScrollViewParent extends ViewGroup 
        implements NestedScrollingParent {
    
    @Override
    public boolean onStartNestedScroll(View child, View target, int axes) {
        return (axes & View.SCROLL_AXIS_VERTICAL) != 0;
    }
    
    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        // 接受嵌套滚动
    }
    
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, 
                                  int[] consumed) {
        // 在子 View 滚动前处理
    }
    
    @Override
    public boolean onNestedFling(View target, float velocityX, 
                                 float velocityY, boolean consumed) {
        // 处理快速滑动
        return false;
    }
    
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
                               int dxUnconsumed, int dyUnconsumed) {
        // 在子 View 滚动后处理
    }
}

// 子 View 实现 NestedScrollingChild
public class NestedRecyclerView extends RecyclerView 
        implements NestedScrollingChild {
    
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        // 启用嵌套滚动
    }
    
    @Override
    public boolean startNestedScroll(int axes) {
        // 启动嵌套滚动
        return super.startNestedScroll(axes);
    }
    
    @Override
    public void stopNestedScroll() {
        // 停止嵌套滚动
        super.stopNestedScroll();
    }
    
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                                        int dxUnconsumed, int dyUnconsumed,
                                        int[] offsetInWindow) {
        // 分派嵌套滚动
        return super.dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow);
    }
}

5.4 实际案例分析

5.4.1 SwipeRefreshLayout + RecyclerView

java
// SwipeRefreshLayout 已经实现了 NestedScrollingParent
// RecyclerView 已经实现了 NestedScrollingChild
// 所以可以直接嵌套使用

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/swipe_refresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

5.4.2 ViewPager2 + 嵌套滚动

xml
<androidx.viewpager2.widget.ViewPager2
    android:id="@+id/viewPager"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!-- 内容 -->
    </androidx.core.widget.NestedScrollView>

</androidx.viewpager2.widget.ViewPager2>

5.4.3 CoordinatorLayout + AppBarLayout

xml
<androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:layout_width="match_parent"
            android:layout_height="200dp"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <ImageView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:layout_collapseMode="parallax"/>

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="可滚动内容"/>

    </androidx.core.widget.NestedScrollView>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

六、点击事件处理

6.1 点击监听方式对比

6.1.1 setOnClickListener

java
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 处理点击
    }
});

6.1.2 实现 OnClickListener 接口

java
public class MyActivity extends AppCompatActivity 
        implements View.OnClickListener {
    
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button1:
                break;
            case R.id.button2:
                break;
        }
    }
}

6.1.3 直接重写 onTouchEvent

java
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mDownX = event.getX();
            mDownY = event.getY();
            break;
        case MotionEvent.ACTION_UP:
            // 判断是否在 View 范围内
            if (isInTouchArea(event.getX(), event.getY())) {
                // 处理点击
            }
            break;
    }
    return true;
}

6.2 点击事件触发条件

java
// View 的 performClick 方法
public boolean performClick() {
    if (mListener != null) {
        if (mListener.onClick(this)) {
            return true;
        }
    }
    
    if (isClickable()) {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        playSoundEffect(SoundEffectConstants.CLICK);
    }
    
    return true;
}

6.3 点击与长按冲突

java
public class ClickableView extends View {
    
    private static final long CLICK_TIMEOUT = 300; // 300ms
    private long mDownTime;
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownTime = SystemClock.uptimeMillis();
                // 启动长按检测
                postDelayed(mLongPressRunnable, ViewConfiguration.getLongPressTimeout());
                break;
            case MotionEvent.ACTION_UP:
                // 取消长按检测
                removeCallbacks(mLongPressRunnable);
                
                long duration = SystemClock.uptimeMillis() - mDownTime;
                if (duration < CLICK_TIMEOUT) {
                    // 点击事件
                    performClick();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                removeCallbacks(mLongPressRunnable);
                break;
        }
        return true;
    }
}

七、GestureDetector 与 ScaleGestureDetector

7.1 GestureDetector 使用

java
public class GestureView extends View {
    
    private GestureDetector mGestureDetector;
    
    public GestureView(Context context) {
        super(context);
        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                // 按下
                return true;
            }
            
            @Override
            public void onShowPress(MotionEvent e) {
                // 显示按下效果
            }
            
            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                // 单击
                return true;
            }
            
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, 
                                   float distanceX, float distanceY) {
                // 滚动
                return true;
            }
            
            @Override
            public void onLongPress(MotionEvent e) {
                // 长按
            }
            
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, 
                                  float velocityX, float velocityY) {
                // 快速滑动
                return true;
            }
        });
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }
}

7.2 ScaleGestureDetector 使用

java
public class ScaleView extends View {
    
    private ScaleGestureDetector mScaleDetector;
    
    public ScaleView(Context context) {
        super(context);
        mScaleDetector = new ScaleGestureDetector(context, 
                new ScaleGestureDetector.SimpleOnScaleGestureListener() {
            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                // 缩放
                float scaleFactor = detector.getScaleFactor();
                // 处理缩放
                return true;
            }
        });
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mScaleDetector.onTouchEvent(event);
        return true;
    }
}

7.3 组合使用

java
public class CombinedGestureView extends View {
    
    private GestureDetector mGestureDetector;
    private ScaleGestureDetector mScaleDetector;
    
    public CombinedGestureView(Context context) {
        super(context);
        
        mGestureDetector = new GestureDetector(context, 
                new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, 
                                  float velocityX, float velocityY) {
                // 处理快速滑动
                return true;
            }
        });
        
        mScaleDetector = new ScaleGestureDetector(context,
                new ScaleGestureDetector.SimpleOnScaleGestureListener() {
            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                // 处理缩放
                return true;
            }
        });
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 同时处理多种手势
        mScaleDetector.onTouchEvent(event);
        mGestureDetector.onTouchEvent(event);
        return true;
    }
}

八、面试考点总结

8.1 基础知识点

Q1: 事件分发的完整流程是什么?

A:

Activity → Window → PhoneWindow → DecorView → ViewGroup → View

具体流程:

  1. Activity.dispatchTouchEvent()
  2. Window.superDispatchTouchEvent()
  3. DecorView.dispatchTouchEvent()
  4. ViewGroup.dispatchTouchEvent() → onInterceptTouchEvent() → onTouchEvent()
  5. View.dispatchTouchEvent() → onTouchEvent()

Q2: dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 的区别?

A:

  • dispatchTouchEvent: 事件分发,决定是否将事件分发给子 View
  • onInterceptTouchEvent: 事件拦截,ViewGroup 特有,决定是否拦截事件
  • onTouchEvent: 事件处理,真正处理触摸事件的方法

Q3: 事件是如何传递的?

A:

  • 从上到下分发:Activity → ViewGroup → View
  • 从下到上响应:View → ViewGroup → Activity
  • 拦截后改变方向:ViewGroup.onInterceptTouchEvent() = true

8.2 进阶知识点

Q4: 如何解决滑动冲突?

A:

  1. 父控件拦截法: 父控件在适当时候拦截事件
  2. 子控件拦截法: 子控件请求父控件不拦截
  3. NestedScrolling: Android 5.0 标准解决方案

Q5: onInterceptTouchEvent 什么时候会被调用?

A:

  • 每次事件都会调用,直到返回 true
  • 一旦返回 true,后续事件不再调用(除非新的 DOWN 事件)

Q6: 什么是事件回溯?

A: 当子 View 的 onTouchEvent 返回 false 时,事件会向上回溯到父 View 的 onTouchEvent。

8.3 实战题目

Q7: 实现一个可拖拽的 View

java
public class DraggableView extends View {
    private float mDownX, mDownY;
    private float mLastX, mLastY;
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = event.getX();
                mDownY = event.getY();
                mLastX = mDownX;
                mLastY = mDownY;
                break;
            case MotionEvent.ACTION_MOVE:
                float currentX = event.getX();
                float currentY = event.getY();
                float dx = currentX - mLastX;
                float dy = currentY - mLastY;
                // 移动 View
                offsetLeftAndRight((int) dx);
                offsetTopAndBottom((int) dy);
                mLastX = currentX;
                mLastY = currentY;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // 松开处理
                break;
        }
        return true;
    }
}

Q8: 自定义一个可折叠的 View

java
public class FoldableView extends ViewGroup {
    private boolean mIsExpanded = true;
    private int mCollapsedHeight;
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 根据展开状态测量高度
        if (mIsExpanded) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        } else {
            // 测量折叠后的高度
            int height = MeasureSpec.makeMeasureSpec(mCollapsedHeight, MeasureSpec.EXACTLY);
            super.onMeasure(widthMeasureSpec, height);
        }
    }
}

8.4 常见面试题

问题考察点难度
事件分发流程基础理解⭐⭐
onInterceptTouchEvent 原理拦截机制⭐⭐⭐
滑动冲突解决实战应用⭐⭐⭐⭐
NestedScrolling 原理深度理解⭐⭐⭐⭐
GestureDetector 使用手势处理⭐⭐
点击事件触发原理底层原理⭐⭐⭐

九、性能优化

9.1 事件处理优化

java
// ❌ 避免在 onTouchEvent 中进行复杂计算
@Override
public boolean onTouchEvent(MotionEvent event) {
    // 耗时操作...
    return true;
}

// ✅ 使用异步处理
@Override
public boolean onTouchEvent(MotionEvent event) {
    // 快速响应
    post(new Runnable() {
        @Override
        public void run() {
            // 耗时操作...
        }
    });
    return true;
}

9.2 避免内存泄漏

java
public class TouchView extends View {
    
    private OnTouchListener mTouchListener;
    
    public void setOnTouchListener(OnTouchListener listener) {
        // 取消旧的监听器
        if (mTouchListener != null) {
            // 清理资源
        }
        mTouchListener = listener;
    }
    
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        // 清理所有回调
        mTouchListener = null;
    }
}

十、总结

10.1 核心要点

  1. 事件分发三要素: dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent
  2. 传递方向: 从上到下分发,从下到上响应
  3. 滑动冲突: 掌握多种解决方案,优先使用 NestedScrolling
  4. 手势处理: 熟练使用 GestureDetector 和 ScaleGestureDetector

10.2 学习资源


本文档持续更新,欢迎补充和完善。