事件分发机制

23 阅读10分钟

事件分发流程

当一个点击事件产生后,事件总是先传递给Activity, Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件。

它的传递过程遵循如下顺序:Activity -> Window -> DecorView -> 层层ViewGroup -> 子View

如果没有任何View消耗事件,事件会依次往回传递(从子View到父View),最后到Activity的onTouchEvent。

image.png

事件分发的三个核心方法

dispatchTouchEvent

用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

  • 所属类:View/ViewGroup
  • 作用:事件分发入口
  • 返回值意义:true事件被消费,false事件未处理

onInterceptTouchEvent

在dispatchTouchEvent方法中调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

  • 所属类:ViewGroup
  • 作用:是否拦截事件
  • 返回值意义:true拦截,子view不接收,false不拦截

onTouchEvent

在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

  • 所属类:View
  • 作用:事件处理
  • 返回值意义:true事件被处理,false事件未处理

伪代码表示

public boolean dispatchTouchEvent(MotionEvent ev) { 
    boolean consume = false; 
    if (onInterceptTouchEvent(ev)) { 
        consume = onTouchEvent(ev); 
    } else { 
        consume = child.dispatchTouchEvent(ev); 
    } 
    return consume; 
}

对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。

事件分发源码深度剖析

Activity的事件分发入口

// Activity.java - 事件分发的起点
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();  // 空方法,可重写用于监听用户交互
    }
    
    // 1. 将事件交给Window
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;  // Window处理了事件
    }
    
    // 2. Window没处理,Activity自己处理
    return onTouchEvent(ev);
}

// PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);  // 交给DecorView
}

首先事件开始交给Activity所附属的Window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用。

ViewGroup的dispatchTouchEvent()核心逻辑

ViewGroup.dispatchTouchEvent()是事件分发机制中最复杂、最核心的方法

// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean handled = false;
    
    // 1. 安全检查:事件是否被过滤
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        
        // 2. 处理ACTION_DOWN:新的事件序列开始
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);  // 清除之前的触摸目标
            resetTouchState();               // 重置状态
        }
        
        // 3. 检查是否拦截
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {  // 已有触摸目标
            // 检查是否禁止拦截
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);  // 调用拦截方法
                ev.setAction(action);  // 恢复action,防止被修改
            } else {
                intercepted = false;  // 禁止拦截
            }
        } else {
            // 没有触摸目标且不是DOWN事件,直接拦截
            intercepted = true;
        }
        
        // 4. 如果没有拦截且不是取消事件
        if (!intercepted && !canceled) {
            // 5. 如果是DOWN/POINTER_DOWN事件,寻找新的触摸目标
            if (actionMasked == MotionEvent.ACTION_DOWN
                || (actionMasked == MotionEvent.ACTION_POINTER_DOWN
                    && !isMouseEvent)) {
                // 遍历子View,寻找能接收事件的View
                final int childrenCount = mChildrenCount;
                if (childrenCount != 0) {
                    // 从后向前遍历(Z轴顺序,后面的在上层)
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final View[] children = mChildren;
                    
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        
                        // 检查子View是否能接收触摸事件
                        if (!canViewReceivePointerEvents(child)
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                            continue;  // 不能接收或坐标不在范围内,跳过
                        }
                        
                        // 分发给子View
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // 子View处理了事件
                            mLastTouchDownTime = ev.getDownTime();
                            mLastTouchDownIndex = i;
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;  // 找到目标,跳出循环
                        }
                    }
                }
            }
        }
        
        // 6. 如果没有找到目标,自己处理
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 7. 有目标,分发给目标
            TouchTarget predecessor = null;
            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;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
    }
    
    return handled;
}

重点说明

1.拦截判断

ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget ! = null(当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素)。

2.FLAG_DISALLOW_INTERCEPT

FLAG_DISALLOW_INTERCEPT这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件(因为ACTION_DOWN会触发重置状态,导致该标志位无效)。

3.子元素能否接收

当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理。是否能够接收点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。

// view.java
protected boolean canReceivePointerEvents() {
    // 是否在播动画
    return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;   
}  

// View.java
public boolean pointInView(float localX, float localY) {
    return localX >= 0 && localX < (mRight - mLeft)
            && localY >= 0 && localY < (mBottom - mTop);
}

// ViewGroup.java
// 检查触摸点是否在子View的范围内
protected boolean isTransformedTouchPointInView(float x, float y, View child,
        PointF outLocalPoint) {
    final float[] point = getTempLocationF();
    point[0] = x;
    point[1] = y;
    
    // 1. 转换坐标到子View的坐标系
    transformPointToViewLocal(point, child);
    
    // 2. 检查是否在子View边界内
    final boolean isInView = child.pointInView(point[0], point[1]);
    
    if (isInView && outLocalPoint != null) {
        outLocalPoint.set(point[0], point[1]);
    }
    return isInView;
}
4.实际分发事件

dispatchTransformedTouchEvent方法,实际上调用的就是子元素的dispatchTouchEvent方法。在它的内部有如下一段内容,而在上面的代码中child传递的不是null,因此它会直接调用子元素的dispatchTouchEvent方法(具体实现之后再关注),这样事件就交由子元素处理了,从而完成了一轮事件分发。

// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        handled = child.dispatchTouchEvent(transformedEvent);
    }
}

如果子元素的dispatchTouchEvent返回true,mFirstTouchTarget就会被赋值,同时跳出for循环。 mFirstTouchTarget真正的赋值过程在addTouchTarget内部完成。

// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
    // ······
    newTouchTarget = addTouchTarget(child, idBitsToAssign); 
    alreadyDispatchedToNewTouchTarget = true; 
    break; // 找到目标,跳出循环
    // ······
}

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

如果没有找到目标,这包含两种情况:第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。在这两种情况下,ViewGroup会自己处理点击事件,这里第三个参数child为null,它会调用super.dispatchTouchEvent(event)。

if (mFirstTouchTarget == null) { 
    handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}

如果找到目标,就转到了View的dispatchTouchEvent方法,即点击事件开始交由View来处理。 执行优先级OnTouchListener.onTouch()> onTouchEvent(),如果OnTouchListener消费了事件,则不会执行onTouchEvent。

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

onTouchEvent()的默认实现

// View.java
public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();
    
    // 1. 检查View是否可用
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }
    
    // 2. 检查是否有触摸代理
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    
    // 3. 检查View是否可点击
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                // 按下状态
                mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                if (!clickable) {
                    setPressed(true, x, y);
                }
                break;
                
            case MotionEvent.ACTION_MOVE:
                // 检查是否还在View范围内
                if (!pointInView(x, y, TOUCH_SLOP)) {
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                }
                break;
                
            case MotionEvent.ACTION_UP:
                // 触发点击事件
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }
                    
                    if (prepressed) {
                        setPressed(true, x, y);
                    }
                    
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // 触发点击
                        removeLongPressCallback();
                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }
                }
                break;
                
            case MotionEvent.ACTION_CANCEL:
                // 取消状态
                setPressed(false);
                removeTapCallback();
                removeLongPressCallback();
                break;
        }
        
        return true;  // 可点击的View默认消耗事件
    }
    
    return false;  // 不可点击的View不消耗事件
}

重要结论

  • 只要View是可点击的(clickable、longClickable、contextClickable),无论是否disable,它的onTouchEvent()就会返回true,表示消费事件。
  • 当ACTION_UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。
public boolean performClick() { 
    final boolean result; 
    final ListenerInfo li = mListenerInfo; 
    if (li ! = null && li.mOnClickListener ! = null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this); result = true; 
    } else { 
        result = false; 
    } 
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result; 
}

事件分发总结

image.png
  • 同一事件序列:从ACTION_DOWN开始,到ACTION_UP或ACTION_CANCEL结束
  • 一旦消费,全程负责:如果View消费了ACTION_DOWN,整个事件序列都会交给它
  • 拦截特权:ViewGroup可以在onInterceptTouchEvent中拦截事件
  • 禁止拦截:子View可以通过requestDisallowInterceptTouchEvent()禁止父View拦截(对ACTION_DOWN无效)
  • OnTouchListener优先:优先级高于onTouchEvent
  • 可点击性决定消费:可点击的View默认消费事件

滑动冲突解决方案

滑动冲突的三种类型

类型场景示例
内外滑动方向不同外部和内部滑动方向垂直ViewPager内嵌ListView
内外滑动方向相同外部和内部滑动方向一致ScrollView内嵌ListView
嵌套组合以上两种组合多层嵌套

解决方案一:外部拦截法(父View主导)

父View根据需要决定是否拦截,重写onInterceptTouchEvent()

public class ParentView extends ViewGroup {
    private float mLastX, mLastY;
    private float mInterceptX, mInterceptY;
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        float x = ev.getX();
        float y = ev.getY();
        
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;  // 必须返回false,否则子View无法接收事件
                mLastX = x;
                mLastY = y;
                break;
                
            case MotionEvent.ACTION_MOVE:
                float deltaX = x - mLastX;
                float deltaY = y - mLastY;
                
                // 判断滑动方向:水平滑动距离 > 垂直滑动距离
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    intercepted = true;  // 水平滑动,父View拦截
                } else {
                    intercepted = false; // 垂直滑动,父View不拦截
                }
                
                mLastX = x;
                mLastY = y;
                break;
                
            case MotionEvent.ACTION_UP:
                intercepted = false;  // UP事件不拦截,让子View处理点击
                break;
        }
        
        return intercepted;
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 处理父View的滑动逻辑
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                float deltaX = event.getX() - mInterceptX;
                // 水平滑动处理
                scrollBy(-(int) deltaX, 0);
                mInterceptX = event.getX();
                break;
        }
        return true;
    }
}

外部拦截法要点

  • ACTION_DOWN必须返回false
  • ACTION_MOVE根据业务逻辑判断
  • ACTION_UP一般返回false

内部拦截法(子View主导)

子View通过requestDisallowInterceptTouchEvent()控制父View的拦截:

public class ChildView extends ViewGroup {
    private float mLastX, mLastY;
    
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        float x = ev.getX();
        float y = ev.getY();
        
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // ❗禁止父View拦截
                getParent().requestDisallowInterceptTouchEvent(true);
                mLastX = x;
                mLastY = y;
                break;
                
            case MotionEvent.ACTION_MOVE:
                float deltaX = x - mLastX;
                float deltaY = y - mLastY;
                
                // 判断如果是垂直滑动,允许父View拦截
                if (Math.abs(deltaY) > Math.abs(deltaX)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                
                mLastX = x;
                mLastY = y;
                break;
                
            case MotionEvent.ACTION_UP:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
        }
        
        return super.dispatchTouchEvent(ev);
    }
}

// 父View需要配合修改
public class ParentView extends ViewGroup {
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;  // 默认拦截非DOWN事件
        }
    }
}

内部拦截法要点

  • 子View在ACTION_DOWN时请求禁止拦截
  • 子View在ACTION_MOVE中根据条件决定是否允许拦截
  • 父View需要配合,默认拦截非DOWN事件

性能优化

减少事件分发层级

每多一层ViewGroup,事件分发就要多遍历一次。

<!-- 优化前:多层嵌套 -->
<LinearLayout>
    <LinearLayout>
        <LinearLayout>
            <TextView />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

<!-- 优化后:减少层级 -->
<ConstraintLayout>
    <TextView />
</ConstraintLayout>

避免在onTouchEvent中频繁创建对象

// 错误:每次触摸都创建新对象
@Override
public boolean onTouchEvent(MotionEvent event) {
    Rect rect = new Rect();  // 频繁创建
    getDrawingRect(rect);
    return rect.contains((int) event.getX(), (int) event.getY());
}

// 正确:复用对象
private Rect mTempRect = new Rect();

@Override
public boolean onTouchEvent(MotionEvent event) {
    getDrawingRect(mTempRect);
    return mTempRect.contains((int) event.getX(), (int) event.getY());
}