View事件分发机制分析

View 事件分发是很重要的知识点,只有理解其中的原理 在写代码过程当中更精准的处理代码逻辑,控制好 api 的调用时机。本文经过阅读SDK 28的源码,在这里作一次输出,深刻理解下。android

目录

1、实例引伸编程

2、事件分发原理api

    1. Activity
    1. ViewGroup
    1. View

3、总结bash

1、实例引伸

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        (Button)findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("MainActivity", "click btn");
            }
        });
    }
}
复制代码
# activity_main.xml

<RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <Button
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
</RelativeLayout>
复制代码

以上是最简单的点击按钮点击事件,对咱们应用层开发来说就是点击了一个Button,而后回调到了 listener 中的onClick 方法,但其背后的原理要从触摸到屏幕开始讲起。app

2、事件分发原理

1. Activity

触摸事件首先会达到 Activity 中的 dispatchTouchEvent 方法内,若是你问我触摸屏幕后是怎么到达 Activity 的,这个问题 I don't know!也并非本文谈论的范围。ide

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
复制代码

这里有必要解释一下 MotionEvent 这个对象,这是触摸事件发生后,系统将触摸事件动做(ACTION_DOWN、ACTION_MOVE、ACTION_UP、ACTION_CANCEL)、触摸坐标点、多指触控等信息保存到此对象中,以便传输时操做。而咱们触摸屏幕时是一序列的事件,会有按压而后不停的移动,最后会抬起,这些动做和坐标点都是会变化的,也就是说会产生down + 不少 move + up/cancel 事件,多指触控比较复杂不在本文讨论范围内。post

onUserInteraction 方法是 Activity 内的一个空实现,若是想在触摸屏幕的最初期作一些操做,能够重写此方法。对于 View 事件分发必需要有一个「消费」的概念,触摸事件究竟是在哪一步、哪个组件里被消费了。在这里,若 getWindow().superDispatchTouchEvent(ev) 返回 true 表明事件被某个组件消费了,此时直接返回 true 结束,若是事件没被消费,那么就继续走到 onTouchEvent 方法,Activity 的 onTouchEvent 基本上都会返回 false, 表示没有消费。学习

直接跟到 getWindow().superDispatchTouchEvent(ev) 方法,在 Android 系统中 Window 抽象类惟一的实现类就是 PhoneWindow, 而 PhoneWindow 内部调用了 DecorView.superDispatchTouchEvent(event), 此方法内又调用了 super.dispatchTouchEvent(event), 也就是调到 ViewGroup 的 dispatchTouchEvent 方法。ui

ps: DecorView 就是全部一个页面(也就是setContentView后)的最顶层View。this

至此触摸事件从 Activity 传递到了 ViewGroup 中,这里把 Window 和 DecorView 的调用过程都写在 Activity 范畴内,由于这个流程是很简单的,不必分开。

下图是Activity事件分发调用流程图解:

Activity事件分发

2. ViewGroup

ViewGroup 中有三个关键方法:

  • dispatchTouchEvent 用于触摸事件一开始传递到 ViewGroup 时调用,
  • onInterceptTouchEvent 用于拦截触摸事件,决定是否本身来消费事件。
  • onTouchEvent 用于消费触摸事件。

看源码有些细节是真的看不懂,可是那些细节又不是特别重要,那么就略过好了。。只看重要的调用流程。 因为 dispatchTouchEvent 方法内容不少,所以分几块去看。首先是 ViewGroup 是否须要拦截的部分。

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    // Handle an initial down.
    // 当一个ACTION_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();
    }

    // Check for interception.
    // 此标志位表明本身是否要拦截这个事件
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        // 经过 mGroupFlags 标志位获得是否容许我这个ViewGroup拦截事件
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            // 基本上onInterceptTouchEvent都会返回false,表明不拦截,
            // 除非自定义ViewGroup,重写此方法是解决滑动冲突的重要手段
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }
    
    ...
}
复制代码

上述一段源码作了一些注释,解释了其流程的逻辑。这里有几个重要变量须要解释的。

mFirstTouchTarget:此对象是一个单链表结构,存储这一系列的事件(ACTION_DOWN、ACTION_MOVE...、ACTION_UP)发生时所涉及到的子View,所以触摸事件 ACTION_DOWN 发生后若是这个对象仍是为null,那么就表示 ViewGroup 没有将事件传递到子View。

mGroupFlags:mGroupFlags 能够理解为不少个标志位的组合。mGroupFlags & FLAG_DISALLOW_INTERCEPT != 0 表示这个标志位组合内有「不容许拦截事件」这个标志位(相似于Map中找一个Key是否存在)。对于位运算本人一直很疑惑,虽然说这些不必定都须要看懂,可是这些判断逻辑的标志位看不懂就很难受。。反正在看位运算的时候千万不要按一向的逻辑在脑海里把数值转换成十进制的,就用二进制去理解,这里推荐一篇位操做文章。

...
    
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
        ...
        
        if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }
        
        ...

        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            // Child wants to receive touch within its bounds.
            // if 代码块内主要保存了一些变量,设置标志位
            // 记录找到的子View,以便以后的事件序列能够直接使用目标View
            ...
            break;
        }
    }
    
    ...
}
复制代码

这里省略了不少杂七杂八的代码,关键仍是在于遍历 ViewGroup 的全部子 View, 经过 isTransformedTouchPointInView 方法找到点击时坐标落在哪一个子 View 上,跟进 dispatchTransformedTouchEvent 看看:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    ...
    
    final boolean handled;
    // If the number of pointers is the same and we don't need to perform any fancy // irreversible transformations, then we can reuse the motion event for this // dispatch as long as we are careful to revert any changes we make. // Otherwise we need to make a copy. final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { // View来处理事件 handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; // 这里就是将触摸点的坐标转换成子视图的坐标 event.offsetLocation(offsetX, offsetY); // 子视图处理事件 handled = child.dispatchTouchEvent(event); // 又将触摸坐标还原,以前转换的坐标只适合那个子视图 event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); } ... return handled; } 复制代码

这里的英文注释解释的很清晰,这个方法的主要做用就是将触摸事件转换成子View相对父容器的坐标,并过滤一些不相关的触摸点(因为不讨论多点触控因此没必要纠结),若是没有子视图,那么就会传到 View 的 dispatchTouchEvent 方法(要知道 ViewGroup 就是继承自 View)。最后返回的 handled 表明是否被处理了,也就是事件是否被消费了。

以上几块代码在 ViewGroup.dispatchTouchEvent 方法中是针对 ACTION_DOWN 这个动做所作的处理,所以还须要作其余动做的处理,其实彻底是相似的,只是操做更简单了:

...

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else {
    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it.  Cancel touch targets if necessary.
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) {
                handled = true;
            }
            
            ...
        }

    }
}

...
复制代码

代码大体意思就是若是没有找到对应的子 View 即 mFirstTouchTarget = null, 那么交给 View.dispatchTouchEvent 处理;若是以前的 ACTION_DOWN 动做已经找到了子 View,那么就继续给它处理。

ViewGroup.onInterceptTouchEvent 方法,这个方法默认基本不作什么事,通常会返回 false;但它是解决滑动冲突的关键方法,遇到滑动冲突时,须要重写此方法。

ViewGroup 的 onTouchEvent 彻底是继承了 View 的 onTouchEvent 方法,所以处理方式和 View 彻底相同,此方法在 View 小节分析。

ViewGroup事件分发调用流程图解:

ViewGroup事件分发

3. View

View 中有两个关键方法:

  • dispatchTouchEvent 用于触摸事件传递到 View 时触发。
  • onTouchEvent 用于消费触摸事件。
/**
 * Pass the touch screen motion event down to the target view, or this
 * view if it is the target.
 *
 * @param event The motion event to be dispatched.
 * @return True if the event was handled by the view, false otherwise.
 */
public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }

    ...
    return result;
}
复制代码

首先判断 OnTouchListener 是否为空,再判断这个 View 是否能够用(即setEnable属性,默认都是true),而后调用 OnTouchListener.onTouch 方法执行咱们自定义的触摸操做,若是此方法返回 true, 则表明事件被消费,接下来不须要执行 onTouchEvent; 若是咱们使其返回 false, 那么能够继续传递给 onTouchEvent 去消费。跟进 View.onTouchEvent 看看:

public boolean onTouchEvent(MotionEvent event) {

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

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                    ...
                    
                    // Use a Runnable and post this rather than calling
                    // performClick directly. This lets other visual state
                    // of the view update before click actions start.
                    if (mPerformClick == null) {
                        mPerformClick = new PerformClick();
                    }
                    if (!post(mPerformClick)) {
                        performClick();
                    }
                    
                    ...
        }

        return true;
    }

    return false;
}
复制代码

其中最关键的部分就是咱们最经常使用的 click 事件,一些长按等事件的逻辑这里就再也不分析。 经过属性判断 View 是否可点击,而且在手指抬起时即 ACTION_UP 执行 performClick 方法,其内部就是判断用户是否设置了 OnClickListener 监听器,若是有则调用 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;
    }
    return result;
}
复制代码

View 的事件分发大体流程就是这样了。其中处理的优先级是:

  • 若是用户设置了 OnTouchListener, 那么就会调用 onTouch 方法,而且若是 onTouch 方法返回true, 那么就不会执行 onTouchEvent 了,也就不会执行 onClick 了;
  • 若是来到 onTouchEvent 方法,那么就有机会去执行 OnClickListener.onClick 方法,除非你执行了长按之类的操做;
  • 最后回调到 onClick;
  • onTouch -> onTouchEvent -> onClick

View 事件分发调用流程图解:

View事件分发

3、总结

事件分发

触摸事件会通过如下几个组件:Activity、Window、DecorView、ViewGroup、View。

  • 当用户点击屏幕时,触摸事件 MotionEvent 最早传递到 Activity.dispatchTouchEvent 方法,而后传递到 PhoneWindow.superDispatchTouchEvent 方法,紧接着传到 DecorView.dispatchTouchEvent 方法,而后直接调用了父类 ViewGroup.dispatchTouchEvent 方法。
  • 在 ViewGroup 的 dispatchTouchEvent 中主要作了如下几件事:当 ACTION_DOWN 事件来的时候,判断如今的 ViewGroup 是否拦截这个事件,而 onInterceptTouchEvent 方法通常返回 false; 一样地,针对 ACTION_DOWN 事件,会遍历一遍 ViewGroup 的全部子 View, 点击若是落在某个子 View 上,那么就将触摸事件传递给子 View 的 dispatchTouchEvent 方法,若是没有找到子 View 那就直接交给父类 View.dispatchTouchEvent 处理事件;当 ACTION_MOVE 或 ACTION_UP 等事件来的时候,依然会传给子 View 或 父类 View 实现的 dispatchTouchEvent, 只是这个过程不用再拦截了,只要 down 的时候拦截了,那么都会交由此 View 拦截,除非调用了 requestDisallowInterceptTouchEvent;
  • 最后事件会来到 View, dispatchTouchEvent 主要去找是否有 OnTouchListener 监听,若是有则调用 onTouch 方法,并根据此方法的返回值决定是否执行 onTouchEvent 方法,onTouchEvent 方法内部会判断是否有 OnClickListener 监听,若是有则调用 onClick。
  • 若是事件达到了子 View,而子 View 并无去消费它,那么这个事件会抛到上一层,若是每层的父视图都不消费事件,那么最后会交给 Activity 执行 onTouchEvent 方法。

事件分发的理解是经过《Android开发艺术探索》(好书) + View事件分发(好文)。

理解事件分发机制的原理后,忽然发现,源码的设计都是很巧妙的,有些业务场景咱们也能够采用这种从上到下委托的方式去设计代码不是吗?所以看源码能提升自身的代码质量,这点是毋庸置疑的。后来搜索了下,这就是责任链模式啊。。

其实源码中的注释很是详细、清晰,比咱们平时接触的业务代码不知道清晰多少倍,但有一点让大多数人望而却步,那就是英语。英语对编程来讲过重要了,所以本人如今已经从新开始学习英语了。。看不懂的注释就配合着翻译强行去看。

相关文章
相关标签/搜索