Android读书笔记--从源码角度剖析View事件分发机制

本文由 爱学园平台 进行联合编辑整理输出

原作者:爱学园——莫比乌斯环

       在开始描述问题之前先说点题外话,写这篇文章的初衷一方面为了构建Android知识体系,另一方面是真心觉得这个是Android面试必问的知识点。网上这方面的博客和书籍讲解这方面的知识也不少,讲的也很到位。正所谓只有自己理解了才是自己的,所以在阅读了他们的文章后,加上自己的理解特此记录一篇~,以便加深理解和记忆!如理解有误的地方请留言说明,我们一起探讨,谢谢!

联系方式:邮箱(ixiyan.li@gmail.com

1.必备知识点

       事件的分发说白了,就是用户与应用的交互过程(手指与屏幕接触)中,发生的一系列事件传递与处理过程。

1.1 事件分发涉及的对象--MotionEvent

典型事件类型:

ACTION_DOWN——手指刚触碰屏幕那一刻(按下)
ACTION_MOVE——手指在屏幕上移动(移动)
ACTION_UP——手指抬起那一刻(抬起)

一个事件序列:就是从手指按下 View 开始直到手指离开 View 产生的一系列事件。

ACTION_DOWN-> ACTION_UP
ACTION_DOWN->...ACTION_MOVE...->ACTION_UP

1.2 事件分发涉及的方法

1. dispatchTouchEvent(MotionEvent ev)

用来进行事件分发。返回结果受当前 View 的 onTouchEvent 和子 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。

2. onInterceptTouchEvent(MotionEvent ev)

在上述dispatchTouchEvent方法内部调用,用来进行当前事件是否拦截校验。这里有一点要注意的地方就是如果当前View拦截了某个事件(一般指ACTION_DOWN),那么在同一个事件序列(上面讲过这个概念)当中,此方法不会被再次调用——即不会做二次拦截校验。
注:Activity和View内部没有此方法

3. onTouchEvent(MotionEvent ev)

在上述dispatchTouchEvent方法内部调用,返回结果表示是否消耗当前事件。这里同上也有一点要注意,如果当前方法返回false(不消耗),那么同一个事件序列中,当前View无法再次接收到事件。

上述方法的关系可用下面的一段伪代码表示:

public boolean dispatchTouchEvent(MotionEvetn e){ 
    if(onInterceptTouchEvent(ev)){//是否拦截
        return onTouchEvent(e);//拦截事件处理:是否消耗
    }
    return child.dispatchTouchEvent(e);//不拦截:子类View分发
}

通过上面的伪代码可以大致了解到事件的传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGrouponInterceptTouchEvent方法返回true,就说明拦截当前事件,接着事件就会交给这个ViewGrouponTouchEvent方法处理。反之onInterceptTouchEvent方法返回false,就不拦截当前事件,这时当前事件就会传递给它的子View,接着ViewdispatchTouchEvent方法就会调用,如此反复直到事件最终被处理。

1.3 事件传递过程遵循如下过程

Activity -> Windown(PhoneWindow) -> DecorView(FrameLayout) -> contentView(setContentView) ->..ViewGroup..->View

2. 事件分发源码解析

根据上面了解到的事件传递的过程分析,下面我们就一步一步撕开它神秘的面纱,从内部了解它的调用关系。

2.1 Activity对点击事件的分发过程

点击事件用MontionEvent表示,当一个点击操作发生时,最先传递给当前Activity,由ActivitydispatchTouchEvent方法进行事件分发,具体的工作由Window来完成。Window会将事件传递给DecorViewDecorView一般就是当前界面的底层容器(即setContentView所设置的 View 的父容器),通过Activity.getWindow().getDecorView()可以获得。因此我们先从ActivitydispatchTouchEvent开始分析。

源码-1:Activity#dispatchTouchEvent

/**
* Called to process touch screen events.  You can override this to
* intercept all touch screen events before they are dispatched to the
* window.  Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}    

现在分析上述代码,通过源码了解到事件交给Activity所附属的Window进行分发,如果getWindow().superDispatchTouchEvent(ev)返回true,事件到此结束,返回false,说明下级所有View的onTouchEvent都返回了false,则Activity的onTouchEvent将会被调用(如上)

通过上面了解到getWindow().superDispatchTouchEvent(ev)这个才是分发的关键,看源码:

源码-2:Window#superDispatchTouchEvent


/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
    /**
     * Used by custom windows, such as Dialog, to pass the touch screen event
     * further down the view hierarchy. Application developers should
     * not need to implement or call this.
     *
     */
    public abstract boolean superDispatchTouchEvent(MotionEvent event);
    ...
}

看上面贴的源码发现贴了好多注释说明,因为这里Window是个抽象类,那么它的实现类是什么呢,是PhoneWindow,为什么呢?到这里您可以详细阅读下上面Window类的说明,发现此处已经指明了Window的唯一实现就是android.view.PhoneWindow,好家伙,隐藏的够深的,那么请移驾,谢谢~

源码-3:PhoneWindow#superDispatchTouchEvent相关代码

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

到这里逻辑就清晰了吧!虽然代码只有一行,但已经足以说明问题了,此处具体逻辑移交给DecorView(这就是我们前面说的窗口的顶级View-->ViewGroup),即Activity#setContentView设置的View就是DecorView的子View。目前事件传递到了DecorView这里,由于DecorVieW即成自FrameLayout且是父View,那么得出结论--最终事件会传递给View,到这一步并不是我们的重点,事件如何通过顶级View进行传递消费才是我们的重头戏,请继续,谢谢~

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

关于点击事件如何在View中进行分发,上面已经做了描述,这里就直接上ViewGroup源码,源码如下:

dispatchTouchEvent方法内容较多分如下几个片段说明:

源码-4:ViewGroup#dispatchTouchEvent——拦截逻辑处理

// 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();
}
// Check for interception.
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); // 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;
}
  1. 是否拦截条件:事件类型为ACTION_DOWN || mFirstTouchTarget != null;
  2. mFirstTouchTarget:每次开始(ACTION_DOWN)都会被初始化为null,当事件由ViewGroup的子元素成功处理时,它指向子元素;
  3. 当事件由ViewGroup拦截时,条件mFirstTouchTarget != null不成立,即当ACTION_MOVEACTION_UP事件到来时,由于第一条拦截条件不满足,则onInterceptTouchEvent不再调用:应证了一旦当前View拦截事件,那么同一事件序列的其它事件都不再进行拦截校验,直接交给它处理。
  4. FLAG_DISALLOW_INTERCEPT标记位:这个标记位一旦设置后(requestDisallowInterceptTouchEvent),ViewGroup将无法拦截除了ACTION_DOWN以外的其它点击事件(ACTION_DOWN事件会重置此标记位,将导致子View中设置的这个标记位无效)。
  5. 面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件,这一点从上面的源码中可以看出来。

源码-5:ViewGroup#dispatchTouchEvent——初始化

// 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);//重置 mFirstTouchTarget = null
    resetTouchState();//重置FLAG_DISALLOW_INTERCEPT标记位
}

从上面的代码可以看出,ViewGroup会在ACTION_DOWN事件到来时会做重置状态的操作,因此子View调用requestDisallowInterceptTouchEvent并不能影响ViewGroupACTION_DOWN事件的处理。

总结:

  1. ViewGroup决定拦截事件(ACTION_DOWN)后,那么后续的点击事件将会默认交给它处理且不再调用它的onInterceptTouchEvent方法。
  2. FLAG_DISALLOW_INTERCEPT这个标志的作用是让ViewGroup不再拦截事件,当然前提是ViewGroup不拦截ACTION_DOWN事件。
  3. FLAG_DISALLOW_INTERCEPT为解决滑动冲突解决提供了新的思路。

源码-6:ViewGroup#dispatchTouchEvent——不拦截,遍历子View

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
    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;
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);

        // If there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }

        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);
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//*子元素调用dispatchTouchEvent方法*
            // 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();
            //保存当前子View:mFirstTouchTarget
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

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

源码-7:ViewGroup#dispatchTouchEvent——子View下发主要逻辑调用

/**
 * Transforms a motion event into the coordinate space of a particular child view,
 * filters out irrelevant pointer ids, and overrides its action if necessary.
 * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
 */
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We do not need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    //...
}

View是否能够接收点击事件有以下两点衡量:

  • 子元素是否在播放动画
  • 点击事件的坐标是否落在子元素的区域内

上面这部分代码说明的是ViewGroup不拦截情况下,事件向子View下发的过程.即主要调用方法为dispatchTransformedTouchEvent,它的内部实际上调用的就是子元素的dispatchTouchEvent方法(可通过上面的源码-7看得出来).通过具体分析可看出,如果child.dispatchTouchEvent(event)返回true,那么mFirstTouchTarget(addTouchTarget方法内部操作)就会被赋值同时跳出for循环,这里是否对mFirstTouchTarget赋值,将会影响ViewGroup的拦截策略,如下所示:

源码-8:ViewGroup#dispatchTouchEvent——赋值mFirstTouchTarget

/**
 * Adds a touch target for specified child to the beginning of the list.
 * Assumes the target child is not already present.
 */
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

mFirstTouchTarget如果为null,将会默认拦截接下来同一序列的所有事件。(不做二次拦截校验)

遍历所有子元素,都没有处理包含两种情况:

  1. ViewGroup没有子元素;
  2. 子元素处理了点击事件,但是所有的子元素都没有消耗事件。

此时ViewGroup将会调用super.dispatchTouchEvent(evet),这一点可以从上述源码-8可以看出,很显然这里ViewGroup继承自View,所以这里就转到ViewdispatchTouchEvent方法,即点击事件交由View处理,那么请继续看下面的分析。

2.3 View对点击事件的处理过程

View(不包含ViewGroup)对点击事件的处理稍微简单,它没有onInterceptTouchEvent方法且无法向下传递事件,只能自己处理,请看它的dispatchTouchEvent方法,如下:

源码-9:View#dispatchTouchEvent——View点击事件处理

/**
* 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 the event should be handled by accessibility focus first.
//...
boolean result = false;
//...
if (onFilterTouchEventForSecurity(event)) {
    if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
        result = true;
    }
    //noinspection SimplifiableIfStatement
    ListenerInfo li = mListenerInfo;
    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的onTouchonTouchEvent(event)优先级高,如果设置了OnTouchListenermOnTouchListener.onTouch返回true那么onTouchEvent(event)将不会调用,反之将会调用onTouchEvent(event),见下文:

源码-10:View#onTouchEvent——点击事件具体处理

public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
    
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 does not respond to them.
    return clickable;
}
if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}
    
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {
        case MotionEvent.ACTION_UP:
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            if ((viewFlags & TOOLTIP) == TOOLTIP) {
                handleTooltipUp();
            }
            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 do not have it already and we should in
                // touch mode.
                boolean focusTaken = false;
                if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                    focusTaken = requestFocus();
                }
    
                if (prepressed) {
                    // The button is being released before we actually
                    // showed it as pressed.  Make it show the pressed
                    // state now (before scheduling the click) to ensure
                    // the user sees it.
                    setPressed(true, x, y);
                }
    
                if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                    // This is a tap, so remove the longpress check
                    removeLongPressCallback();
    
                    // Only perform take click actions if we were in the pressed state
                    if (!focusTaken) {
                        // 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();
                        }
                    }
                }
    
                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;
    
    //...
        }
    
    return true;
    }
    
    return false;
}

从上面的代码看出:影响事件的消耗因素有两个:CLICKABLELONG_CLICKABLE只要有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,实际调用方法为performClick();,在其内部调用OnClickListener#onClick方法。

到此点击事件的分发机制的源码分析就完了,但是Android
的学习才刚开始,还有很长的路要走,下面附上从别处盗来的图,觉得不错可以看下

2.4 View事件分发流程图示例图

事件分发流程图

参考相关文章与相关书籍

Android 事件分发

Android事件分发机制解析

书籍:任玉刚的《Android开发艺术探索》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容