Android View 事件体系笔记(二):View 事件分发

2017-11-23  本文已影响0人  Marker_Sky
Android View 事件体系笔记(二).png

声明:本文内容依据《Android开发艺术探索》的思路,基于 API 26 进行总结

一、View 事件分发机制概览

1.1 点击事件传递规则

定义:所谓点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程。即当一个 MotionEvent 产生之后,系统需要把这个事件传递给一个具体的 View。
关于MotionEvent对象的含义及参数可以参考上一篇文章。

点击事件的分发由三个重要的方法共同完成:

伪代码表明三个方法的关系:

// 事件传递其实是MotionEvent对象的传递
public boolean dispatchTouchEvent(MotionEvent event){
    // 标记是否消费事件
    boolean consume = false;
    // 判断当前 View 是否拦截此事件,拦截则进一步处理
    if(onInterceptTouchEvent(event)){
        consume = onTouchEvent(event);
    }else {
        // 不拦截则传递到子 View 接着判定
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}
三个重要方法.png

View 设置 OnTouchListener,回调 onTouch 方法,如果 onTouch 返回 false,则当前 View 的 onTouchEvent 方法会被调用,如果 true,则被拦截。说明 OnTouchListener 优先级高于 onTouchEvent 。
在 onTouchEvent 方法中,如果当前设置的有 OnClickListener,那么它的 onClick 方法会被调用。OnClickListener 优先级最低,处于事件传递的尾端。

1.2 点击事件传递顺序

顺序: Activity --> ViewGroup --> View
这里借用一张图来说明:

事件分发流程图.png
简单解释一下这张图:

二、基于 Android 8.0 源码分析View 事件分发机制

2.1 事件最先传递给 Activity,由其 dispatchTouchEvent 进行派发

也就是上面流程图的第一部分


ActivityToViewGroup.png

android.app.Activity.java

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            // 空方法
            onUserInteraction();
        }
        // 事件交给 Activity 所属的 Window 进行分发,返回 true 事件结束
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        // 如果没有 View 处理事件,调用 Activity 的 onTouchEvent
        return onTouchEvent(ev);
    }
2.2 Window 会将事件传递给 decor view,一般是当前页面的底层容器(setContentView 所设置的 View 的父容器)通过 Activity.getWindow.getDecorView() 可以获得。

(1)Window 是一个抽象类,其 superDispatchTouchEvent 也是一个抽象方法。所以要找到其实现类。

Window 类源码描述(API 26).png
通过 Window 源码描述可知其实现类为 PhoneWindow,描述大意为:Window 类可以控制顶级 View 的外观和行为策略,它的唯一实现位于 android.view.PhoneWindow 中,当你要实例化这个 Window 类的时候,你并不知道它的细节,因为这个类会被重构,只有一个工厂方法可以使用。
(2)PhoneWindow 类是 Window 唯一的实现类,前者直接将事件传递给了 DecorView

PhoneWindow 类 package com.android.internal.policy;

@Override
public boolean superDispatchKeyEvent(KeyEvent event) {
    return mDecor.superDispatchKeyEvent(event);
}

PhoneWindow 类的 mDecor 对象

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    ...
    @Override
    public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }

通过 ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0); 可以获取 Activity 所设置的 View,这个 mDecor 就是 getWindow().getDecorView() 返回的 View,通过 setContentView 设置的 View 是它的一个子 View。目前事件由 mDecor.superDispatchKeyEvent(event) 传递到了 DecorView 这里。

(3)DecorView 是整个Window界面的最顶层View,包含通知栏,标题栏,内容显示栏(就是 Activity setContentView 设置的布局 View)三块区域

public class DecorView extends FrameLayout

    public boolean superDispatchKeyEvent(KeyEvent event) {
        ...
        // 传递给父类的 dispatchKeyEvent
        return super.dispatchKeyEvent(event);
    }

DecorView 的父类为 FrameLayout 且是父 View,事件会一层层传递最终会传递给 View。到这时,事件已经传递到顶级 View 了,也就是在 Activity 中通过 setContentView 设置的 View。顶级 View 也叫根 View,一般来说都是 ViewGroup。

2.3 顶级 View 对点击事件的分发过程

点击事件达到顶级 View (一般是一个 ViewGroup)以后,会调用 ViewGroup 的 dispatchTouchEvent 方法,对应流程图为:

ViewGroup Start.png
之后的逻辑是这样的:
如果顶级 ViewGroup 拦截事件即 onInterceptTouchEvent 返回 true,则事件由 ViewGroup 处理,这时如果 ViewGroup 的 mOnTouchListener 被设置,则 onTouch 会被调用,否则 onTouchEvent 会被调用。
在 onTouchEvent 中,如果设置了 mOnClickListener,则 onClick 会被调用。
如果顶级 View 不拦截事件,则事件会传递给子 View 并调用其 dispatchTouchEvent,接下来会一直处理到结束。
ViewGroup 分发过程.png

package android.view; ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {...} 方法源码

(1)当前 ViewGroup 是否拦截点击事件

// 检查拦截。
final boolean intercepted;
// Step 1 判断按下或者mFirstTouchTarget:
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    // Step 2 判断 mGroupFlags 和 FLAG_DISALLOW_INTERCEPT
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {
        intercepted = onInterceptTouchEvent(ev);
        ev.setAction(action); // 动作改变时重置事件状态
    } else {
        intercepted = false;
    }
} else {
    // 没有触摸目标且不是最初的按下事件,所以
    // 该 ViewGroup 继续处理事件
    intercepted = true;
}

Step 1 判断按下或者mFirstTouchTarget:if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null): 判断当前动作是否为 MotionEvent.ACTION_DOWN 按下,或者 mFirstTouchTarget != null。mFirstTouchTarget 通过查看后面逻辑可以看出指的是如果事件由 ViewGroup 的子元素成功处理,则 mFirstTouchTarget 会被赋值并指向子元素。
也就是说,当 ViewGroup 不拦截事件并将事件交给子 View 去成功处理,ViewGroup 不会再处理除 MotionEvent.ACTION_DOWN 以外的事件。因为此时 mFirstTouchTarget 的值不为 null,同时 MotionEvent.ACTION_MOVE 和 MotionEvent.ACTION_UP 不会再经历 ViewGroup 的 onInterceptTouchEvent(ev) 方法。
Step 2 判断 mGroupFlags 和 FLAG_DISALLOW_INTERCEPT: mGroupFlags 参数不用理会。FLAG_DISALLOW_INTERCEPT 标记一旦被设置,ViewGroup 将无法拦截除 ACTION_DOWN 以外的事件。这个标记的设置一般是子 View 通过 ViewGroup 的 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {...} 方法来设置的。每次按下以后都会清除和重置标记:

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

这个方法是在第(1)步之前的,由于每次重新按下都会清除标记,所以 ViewGroup 总是能调用onInterceptTouchEvent来判断是否拦截事件。如果事件不为 ACTION_DOWN 且其子 View 设置了标记,ViewGroup 就不会再拦截事件而是直接交给子 View 去处理。
结论:

(2)ViewGroup 不拦截事件向下分发的过程

if (newTouchTarget == null && childrenCount != 0) {
    // Step 1:获取点击坐标位置
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    // Find a child that can receive the event.
    // Scan children from front to back.
    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
     // Step 2:遍历 ViewGroup 所有子元素
    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 (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }
        // Step 3:判断是否可以接收点击事件(判断方法为是否在执行动画),
        // 或者坐标是否坐落在子元素的区域内
        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        newTouchTarget = getTouchTarget(child);
        if (newTouchTarget != null) {
            // Child is already receiving touch within its bounds.
            // Give it the new pointer in addition to the ones it is handling.
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }

        resetCancelNextUpFlag(child);
         // Step 4:进行判断,如果 child 不为 null,则 child 进行 dispatchTouchEvent。反之,则调用父 View 的 dispatchTouchEvent。
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            // Child wants to receive touch within its bounds.
            mLastTouchDownTime = ev.getDownTime();
            if (preorderedList != null) {
                // childIndex points into presorted list, find original index
                for (int j = 0; j < childrenCount; j++) {
                    if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
                }
            } else {
                mLastTouchDownIndex = childIndex;
            }
            mLastTouchDownX = ev.getX();
            mLastTouchDownY = ev.getY();
            // Step 5:结合上方 if 返回 true 表示子元素的 dispatchTouchEvent 处理成功,
            // mFirstTouchTarget 就会被赋值同时跳出 for 循环 
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

        // The accessibility focus didn't handle the event, so clear
        // the flag and do a normal dispatch to all children.
        ev.setTargetAccessibilityFocus(false);
    }
    if (preorderedList != null) preorderedList.clear();
}

Step 1: 只是记录点击的坐标。
Step 2: 遍历 ViewGroup 所有子元素。
Step 3: canViewReceivePointerEvents 该子 View 是否正在播放动画。

    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }

isTransformedTouchPointInView 判断点击是否在该子 View 的区域内。
如果子元素满足这两个条件,那么事件就会传递给它来进行处理。
Step 4: dispatchTransformedTouchEvent 方法包含下列语句,也就是根据 child view 是否为 null 来决定是否将事件传递给父 View。

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            ...
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
}

Step 5: addTouchTarget 方法主要是为 mFirstTouchTarget 赋值,表面该事件已经交给子 View 进行处理。如果 mFirstTouchTarget 为 null,ViewGroup 就会默认拦截接下来同一序列的所有点击事件。

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

如果所有的子元素都没有处理事件,包含两种情况:第一是 ViewGroup 没有子元素,第二是子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false。在这样的情况下 ViewGroup 会接着处理:

if (mFirstTouchTarget == null) {
    // No touch targets so treat(对待) this as an ordinary(普通) view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
} else {
          ...
}

如果 mFirstTouchTarget 为 null,dispatchTransformedTouchEvent 传递的第三个参数为 null,也就是说会调用 ViewGroup 父类的 dispatchTouchEvent。

2.4 View 对点击事件的处理过程
View 开始处理

package android.view; --> View 源码片段

(1)dispatchTouchEvent 方法

public boolean dispatchTouchEvent(MotionEvent event) {
      ...
      boolean result = false;
      ...
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
        // Step 1:判断有没有设置 OnTouchListener
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        // Step 2:上方 onTouch 返回 true,result 为 true 则不会调用 onTouchEvent
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    ...
    return result;
}

Step 1:首先判断有没有设置 OnTouchListener,如果设置了并且 onTouch 返回 true,则标记 result 为 true,说明 onTouch 消费了事件。
Step 2: result 为 true 则不会调用 onTouchEvent,如果标记 result 为 false 并且 onTouchEvent 返回 true,说明该 View 消费了点击事件,最后标记设置为 true 并返回告知父 View 事件已经被消费了。

(2)onTouchEvent 方法
首先获取当前 View 的状态 clickable(是否可点击),然后再进行判定如果该 View 的状态为 DISABLED(不可用)最后再返回 clickable。有一句重要的注释:一个可点击的不可用的 View 依旧会消费点击事件,它只是不作响应而已。说明不可用的 View 依旧消费点击事件。

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if ((viewFlags & ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
        setPressed(false);
    }
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    // A disabled view that is clickable still consumes the touch
    // events, it just doesn't respond to them.
    return clickable;
}

接下来判定是否设置了代理,这里的 onTouchEvent 的工作机制看起来和 OnTouchListener 类似,不深入研究。

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

接下来看 onTouchEvent 对点击事件的具体处理

// 如果 View 可点击或者可显示悬浮或长按的工具提示
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // 如果是悬浮工具窗的操作就另行处理
            if ((viewFlags & TOOLTIP) == TOOLTIP) {
                handleTooltipUp();
            }
            // 如果 View不可点击移除各种点击回调并跳出
            if (!clickable) {
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;
            }
            boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
            if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                // take focus if we don't have it already and we should in
                // touch mode.
                boolean focusTaken = false;
                if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                    focusTaken = requestFocus();
                }

                if (prepressed) {
                    // 按钮在释放之前我们确实显示了它的点击效果。
                    // 使该按钮显示按下的状态以确保用户能够看到。
                    setPressed(true, x, y);
                }

                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // 通过mHasPerformedLongPress得知这不是长按事件
                    // 说明这是一个单击事件,所以移除长按检测
                    removeLongPressCallback();

                    // 如果处于按下状态只执行点击操作
                    if (!focusTaken) {
                        // 使用一个 Runnable 的 post 方式来调用 performClick 好过直接调用。
                        // 这样不影响该 View 除了点击外的其他的状态的更新
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
                        if (!post(mPerformClick)) {
                            // *重要方法,用来判定是否设置了 OnClickListener 再进行一系列处理
                            performClick();
                        }
                    }
                }

                if (mUnsetPressedState == null) {
                    mUnsetPressedState = new UnsetPressedState();
                }

                if (prepressed) {
                    postDelayed(mUnsetPressedState,
                            ViewConfiguration.getPressedStateDuration());
                } else if (!post(mUnsetPressedState)) {
                    // If the post failed, unpress right now
                    mUnsetPressedState.run();
                }
                // 处理触摸回调
                removeTapCallback();
            }
            mIgnoreNextUpEvent = false;
            break;

        case MotionEvent.ACTION_DOWN:
            ...
            break;

        case MotionEvent.ACTION_CANCEL:
            ...
            break;

        case MotionEvent.ACTION_MOVE:
            ...
            break;
    }

    return true;
}

经过了一系列的判定后来到了一个重要方法 performClick(),这个方法来处理具体的 OnClick 回调等逻辑操作。以下是 performClick 方法:

public boolean performClick() {
    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        // 存在OnClickListener,播放点击音效
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

到这里点击事件分发到了 onClick 函数,接下来就是我们自己去 onClick 方法中去实现逻辑操作了。
View 的 LONG_CLICKABLE 属性默认为 falseCLICKABLE 属性依据是否可点击来确定。通过 setLongClickablesetClickable 分别可以改变两个属性的值,另外,通过 setOnClickListener 会自动将 View 的 CLICKABLE 属性设置为 true,相关源码:

public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

到这里事件传递机制就基本结束了。

三、总结

事件传递机制的一些结论:

上一篇 下一篇

猜你喜欢

热点阅读