Android面试题库
Framework源码
Event Dispatch

Android事件分发机制

1. 基础知识

1.1 事件分发的三个阶段

  1. 分发(dispatchTouchEvent)

    • 事件分发的入口
    • 返回true表示消费事件
    • 返回false表示不处理,交由父View处理
  2. 拦截(onInterceptTouchEvent)

    • 只存在于ViewGroup中
    • 返回true表示拦截该事件
    • 返回false表示不拦截,传递给子View
  3. 消费(onTouchEvent)

    • 处理具体的触摸事件
    • 返回true表示消费该事件
    • 返回false表示不消费,交由父View处理

2. 核心问题

2.1 事件分发的流程是怎样的?

答案:

  1. 事件分发的顺序

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;
        if (onInterceptTouchEvent(ev)) {
            consume = onTouchEvent(ev);
        } else {
            consume = child.dispatchTouchEvent(ev);
        }
        return consume;
    }
  2. 事件传递的方向

    • 从Activity -> Window -> DecorView -> ViewGroup -> View
    • 先自上而下分发,再自下而上处理

2.2 如何解决滑动冲突?

  1. 外部拦截法

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (父容器需要当前点击事件) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
        return intercepted;
    }
  2. 内部拦截法

    // 子View中处理
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (父容器需要当前点击事件) {
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

2.3 事件分发过程中的常见问题?

  1. View的滑动失效

    • 原因:父View拦截了事件
    • 解决:重写父View的onInterceptTouchEvent方法
  2. 点击事件的响应延迟

    • 原因:等待双击事件的判定
    • 解决:使用GestureDetector处理

3. 实战示例

3.1 自定义可滑动的ViewGroup

public class CustomScrollView extends ViewGroup {
    private int mLastX;
    private int mLastY;
    private int mMoveX;
    private int mMoveY;
    private VelocityTracker mVelocityTracker;
    private Scroller mScroller;
 
    public CustomScrollView(Context context) {
        this(context, null);
    }
 
    public CustomScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
 
    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }
 
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
 
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                mMoveX = x - mLastX;
                mMoveY = y - mLastY;
                if (Math.abs(mMoveX) > Math.abs(mMoveY)) {
                    intercepted = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
        }
 
        mLastX = x;
        mLastY = y;
        return intercepted;
    }
 
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
 
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                scrollBy(-deltaX, 0);
                break;
            case MotionEvent.ACTION_UP:
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) > 50) {
                    mScroller.fling(getScrollX(), 0, (int) -xVelocity, 0,
                            0, getWidth(), 0, 0);
                    invalidate();
                }
                break;
        }
 
        mLastX = x;
        mLastY = y;
        return true;
    }
 
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
 
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 实现子View的布局
    }
}

3.2 使用示例

<com.example.CustomScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/image"/>
        
</com.example.CustomScrollView>

4. 性能优化

  1. 避免在事件分发过程中进行耗时操作

    • 不要在onTouch系列方法中进行复杂计算
    • 避免创建大量对象
  2. 使用VelocityTracker优化滑动

    • 计算滑动速度
    • 实现惯性滑动
  3. 合理使用事件拦截

    • 避免无谓的事件拦截
    • 在必要时才拦截事件