跳至主要內容

Android事件分发流程梳理

PPLong大约 4 分钟学习

Android事件分发流程梳理

View点击事件

事件流程:DOWN --> MOVE(许多个) --> UP or Cancel(非人为)

事件分发的本质:将点击事件MotionEvent传递到具体某个View进行处理的流程

传递事件的对象:Activity——ViewGroup——View

涉及方法:

方法作用
dispatchTouchEvent传递点击事件
onTouchEvent处理点击事件
onInterceptTouchEvent判断是否拦截某个事件

事件分发流程

Activity事件分发流程

// Activity    
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    // mWindow is PhoneWindow	
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    // 如果没人处理, 那就Activity就自己处理
    return onTouchEvent(ev);
}
// PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
// DecorView : ViewGroup
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    final Window.Callback cb = mWindow.getCallback();
    return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
        ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
// ViewGroup.dispatchTouchEvent 做了很多事,这里先不说
// 如果上述步骤中有一步返回false,则进入Activity.onTouchEvent中
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}
	

总结一下就是Activity负责将事件经由Window与DecorView传递给根布局ViewGroup

需要注意的是Activity与DecorView和Window的关系是: Activity包含Window Window包含DecorView。Activity在Create时创建Window,在setContentView方法中创建DecorView并添加自定义布局到DecorView中。DecorView比较熟悉了,每次打开AS Layout Inspector时查看Component Tree顶层就是DecorView

ViewGroup事件分发流程

省略以及内联了部分代码

// ViewGroup

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    // ViewGroup每次事件分发时,都要调用onInterceptTouchEvent询问是否拦截事件
    // disallowIntercept:是否禁用事件拦截的功能(默认是false 不禁用),可通过调用requestDisallowInterceptTouchEvent()修改
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    // 允许Intercept
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); 
    } else {
        intercepted = false;
	}
    // ...
    // 仅当事件未被取消且事件未被拦截时进行子View的事件传递
    if (!canceled && !intercepted) {
    	for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
            final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);
            // 只有View可接受事件并且点击事件坐标落在View的区域内才可以
            if (!child.canReceivePointerEvents()
                || !isTransformedTouchPointInView(x, y, child, null)) {
                ev.setTargetAccessibilityFocus(false);
                continue;
            }
            // 将具体事件交给每个Child View去处理,返回true代表view消费了该事件
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }
            // 没有可处理的child,则只能自己处理
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                                      TouchTarget.ALL_POINTER_IDS);
            }

        }
    }
    
}

public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
        && ev.getAction() == MotionEvent.ACTION_DOWN
        && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
        && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}
// 只有VISIBLE的View才能接受事件,INVISIBLE不能处理事件
protected boolean canReceivePointerEvents() {
    return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
// 真正进行child view事件分发流程的方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                              View child, int desiredPointerIdBits) {
    // ... 
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }
    transformedEvent.recycle();
    return handled;
}

理解onInterceptTouchEvent: 个人理解是在ViewGroup中对事件进行一些预先处理和判断,判断是否要传递给Child

核心过程是:ViewGroup判断是否拦截以及寻找可传递事件的子View。并且需要注意,事件分发只会分发给一个Child,如果同一层ViewGroup下有两个有重叠区域的View,则点击该重叠区域,则只会按照child中排序对第一个View进行点击事件处理!

ViewGroup事件传递
ViewGroup事件传递
方法调用说明图
方法调用说明图

View事件分发流程

public boolean dispatchTouchEvent(MotionEvent event) {
	boolean result = false;
    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
        result = true;
    }
    ListenerInfo li = mListenerInfo;
    // 是否ENABLE且实现OnTouchListener, 调用onTouch方法
    if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }
    // 执行onTouchEvent方法
    if (!result && onTouchEvent(event)) {
        result = true;
    }
}

View中onTouch与onTouchEvent方法执行顺序:

View中如果实现了OnTouchListener接口且返回为true时,就不会再调用onTouchEvent方法,但如果返回为false,则会继续执行onTouchEvent方法

img
img

Recyclerview滑动嵌套冲突

根源:在Dragging时,父级RecyclerView onInterceptTouchEvent中返回true,导致父级RecyclerView不分发该事件给子View并自行处理了该事件

解决办法:

  1. 自定义父RecyclerView并重写onInterceptTouchEvent()方法
  2. 重写子布局的dispatchTouchEvent()方法,调用getParent().requestDisallowInterceptTouchEvent(true),通知通知父层ViewGroup不要拦截点击事件
  3. 实现子布局OnTouchListener接口,在ACTION_DOWN时请求父布局不拦截事件:getParent().requestDisallowInterceptTouchEvent(true)

2和3的做法是可行的,因为Recyclerview中仅当state为dragging时才进行事件拦截,ACTION_DOWN与UP均不会被拦截,因此事件可以传递给子View,从而调用requestDisallowInterceptTouchEvent方法