专栏名称: 安卓开发精选
伯乐在线旗下账号,分享安卓应用相关内容,包括:安卓应用开发、设计和动态等。
目录
相关文章推荐
开发者全社区  ·  北京86年大叔自救式征婚相亲 ·  18 小时前  
开发者全社区  ·  英区金融妲己 ·  昨天  
开发者全社区  ·  中国最难入的IT公司 ·  2 天前  
开发者全社区  ·  你是来开房的,还是来入住的? ·  3 天前  
51好读  ›  专栏  ›  安卓开发精选

Android 事件分发机制

安卓开发精选  · 公众号  · android  · 2019-10-15 11:50

正文

(给 安卓开发精选 加星标)

转自:Ghost_Liu

https://juejin.im/post/5d8dbe79e51d4578282ce224

在我们平时开发中,总是会遇到滑动冲突。那么,如果要解决滑动冲突,首先就要求我们理解Android中一个非常重要的知识点:事件分析机制。


基础知识准备


Android UI层级


在了解事件分发机制之前,我们要知道Android布局层级。我们在写 Activity 的时候通常是通过 setContentView(int layoutResID) 方法设置布局,所以我们先从这个方法入手看一下布局层级。


public void setContentView(@LayoutRes int layoutResID) {    getWindow().setContentView(layoutResID); //1    initWindowDecorActionBar(); //2}
public Window getWindow() { return mWindow;}


由上面的代码我们可以看到,ActivitysetContentView方法实际上是调用了WindowsetContentView方法,让我们看一下Window对象。


/** * 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. * * The only existing implementation of this abstract class is * android.view.PhoneWindow, which you should instantiate when needing a * Window. */public abstract class Window {....}


从注释中我们可以看到 Window 是一个抽象类了,并且它的唯一一个子类是 PhoneWindow ,也就是说上面的方法调用实际上是调用了PhoneWindow的setContent方法,我们来看一下。

 
@Overridepublic void setContentView(int layoutResID) {    if (mContentParent == null) {        installDecor(); //1    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {        mContentParent.removeAllViews();    }    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,                getContext());        transitionTo(newScene);    } else {        mLayoutInflater.inflate(layoutResID, mContentParent);    }    mContentParent.requestApplyInsets();    final Callback cb = getCallback();    if (cb != null && !isDestroyed()) {        cb.onContentChanged();    }    mContentParentExplicitlySet = true;}
private void installDecor() { if (mDecor == null) { mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } else { mDecor.setWindow(this); } if (mContentParent == null) { mContentParent = generateLayout(mDecor); //1 ... }}

在这里我只挑出了主要的代码,第一段代码中我们可以看到通过 installDecor 方法去初始化 DecorView ,通过查看源码我们可以知道 DecorView 实际上是一个 Framelayout 通过第二段代码的1处我们可以看到,其调用了 generateLayout 方法,我们来来看一下这里面做了什么。


protected ViewGroup generateLayout(DecorView decor) {        ...        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {            requestFeature(FEATURE_NO_TITLE);        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {            // Don't allow an action bar if there is no title.            requestFeature(FEATURE_ACTION_BAR);        }        ...        if ((features & (1 << FEATURE_NO_TITLE)) == 0) {            // If no other features and not embedded, only need a title.            // If the window is floating, we need a dialog layout            if (mIsFloating) {                TypedValue res = new TypedValue();                getContext().getTheme().resolveAttribute(                        R.attr.dialogTitleDecorLayout, res, true);                layoutResource = res.resourceId;            } else if ((features & (1




    
 << FEATURE_ACTION_BAR)) != 0) {                layoutResource = a.getResourceId(                        R.styleable.Window_windowActionBarFullscreenDecorLayout,                        R.layout.screen_action_bar);            } else {                layoutResource = R.layout.screen_title; //注释1            }            // System.out.println("Title!");        }        ...}

上面的代码省略了很多,我们从代码中看到 generateLayout 方法中会根据不同的 Future 来给 layoutResource 设置布局。 我们看一下注释1处的布局文件。


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:fitsSystemWindows="true">        <ViewStub android:id="@+id/action_mode_bar_stub"              android:inflatedId="@+id/action_mode_bar"              android:layout="@layout/action_mode_bar"              android:layout_width="match_parent"              android:layout_height="wrap_content"              android:theme="?attr/actionBarTheme" />    <FrameLayout        android:layout_width="match_parent"        android:layout_height="?android:attr/windowTitleSize"        style="?android:attr/windowTitleBackgroundStyle">        <TextView android:id="@android:id/title"            style="?android:attr/windowTitleStyle"            android:background="@null"            android:fadingEdge="horizontal"            android:gravity="center_vertical"            android:layout_width="match_parent"            android:layout_height="match_parent" />    FrameLayout>    <FrameLayout android:id="@android:id/content"        android:layout_width="match_parent"        android:layout_height="0dip"        android:layout_weight="1"        android:foregroundGravity="fill_horizontal|top"        android:foreground="?android:attr/windowContentOverlay" />LinearLayout>

从布局文件中我们看到三个部分


ViewStub:用来显示ActionBar第一个FrameLayout:用来显示标题第二个FrameLayout:用来显示内容

通过上面的源码我们可以看出: Activity中包含一个Window对象,这对象是由PhoneWindow来实现。 PhoneWindow中又包含一个DecorView,并将其作为跟布局。 这个DecorView又分成两个部分,我们平时设置布局时其实是将布局展示在Content区域。





因为DecorView是继承自FrameLayout,而FrameLayout又继承自ViewGroup。 所以我们可以将事件的流向简单的表示出来: Activity->ViewGroup->View。


事件概念


提了很多次事件,事件的具体概念是什么? 其实,这里所说的事件,是指当手指从触碰到手机屏幕到离开手指屏幕所产生的一系列Touch事件,其中也包括手指在手指在屏幕上滑动所对应的操作。 这些Touch事件被封装到一个MotionEvent对象中,接下来让我们看一下这个对象具体是做什么的。


实际上,事件分发机制处理的对象是手指操作屏幕时所产生的一些列
MotionEvent 对象。


MotionEvent事件分类和对应含义


事件分类 对应含义
MotionEvent.ACTION_DOWN A pressed gesture has started(一个按压动作已经开始了)
MotionEvent.ACTION_MOVE A change has happened between ACTION_DOWN and ACTION_UP(在点击和抬起事件中间的一些列操作)
MotionEvent.ACTION_UP A pressed gesture has finished(一个按压动作已经结束)
MotionEvent.ACTION_CANCEL A movement has happened outside of the normal bounds of the UI element(在滑动的过程中超出边界)


上面的表格中只列举了开发中最常用的几种事件类型,其实还有很多事件类型,有兴趣的小伙伴可以自行查看。 MotionEvent对象中不仅封装了事件类型,同时封装了手指在操作过程中的坐标。


涉及到的方法


事件分发机制中主要涉及到三个方法: dispatchTouchEvent()、onTouchEvent()和onInterceptTouchEvent()。


方法名 方法的含义
dispatchTouchEvent() 被用来事件分发,如果事件传递到当前的View,该方法一定会被调用,默认返回false
onInterceptTouchEvent() 该方法被用来判断是否拦截某个事件,在dispatchTouchEvent方法中调用 ,返回值表示是否拦截当前事件,通常存在于ViewGroup,一般View中没有该方法
onTouchEvent() 该方法被用来处理点击事件,返回值表示是否消耗当前的事件,如果不进行消耗,该系事件序列将不会被接受


小结


从上面的分析中我们可以进行一下小结:


当手指触摸屏幕产生的一些列操作会被封装在MotionEvent中,通过Activity、ViewGroup和View调用一系列方法,找到事件的接受者并处理该事件的一个过程。


源码分析


从上面的分析我们已经知道事件的大致流向: Activity -> ViewGroup -> View ,接下来让我们从源码的角度逐个分析。


Activity事件分发


既然事件最初是从Activity中进行传递,所以我们首先到 Activity 中找到 dispatchTouchEvent() 方法:


public boolean dispatchTouchEvent(MotionEvent ev) {    if (ev.getAction() == MotionEvent.ACTION_DOWN) {        onUserInteraction();    }    if (getWindow().superDispatchTouchEvent(ev)) { //1        return true;    }    return onTouchEvent(ev);}

注释1处的 getWindow() 方法获取的是 Window 对象,但是 Window 是一个抽象类,我们看一下它的唯一子类 PhoneWindow superDispatchTouchEvent() 方法


PhoneWindow.superDispatchTouchEvent@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {    return mDecor.superDispatchTouchEvent(event);}
DecorView.superDispatchTouchEventpublic boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event);}

我们看到在 PhoneWindow superDispatchTouchEvent() 方法实际上调用了 DecorView superDispatchTouchEvent() 方法,这个方法内部调用了 super.dispatchTouchEvent() 在基础知识准备中我们知道 DecorView 是继承自 Framelayout ,在 Framelayout 中是没有 dispatchTouchEvent() 的方法,但是 Framelayout 是继承自 ViewGroup 所以,最后还是调用到了 ViewGroup dispatchTouchEvent() ,自此事件从 Activity 传递到了 ViewGroup


ViewGroup事件分发


既然事件已经从 Activity 传递到了 ViewGroup ,让我们看一下 ViewGroup 具体做了什么处理。


@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {    boolean handled = false;    if (onFilterTouchEventForSecurity(ev)) {        final int action = ev.getAction();        final int actionMasked = action & MotionEvent.ACTION_MASK;         //--------------------1----------------------        if (actionMasked == MotionEvent.ACTION_DOWN) {            cancelAndClearTouchTargets(ev);            resetTouchState();        }         //--------------------2----------------------         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);            } else {                intercepted = false;            }        } else {            intercepted = true;        }        //--------------------3----------------------        final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;        TouchTarget newTouchTarget = null;        boolean alreadyDispatchedToNewTouchTarget = false;        if (!canceled && !intercepted) {            if (actionMasked == MotionEvent.ACTION_DOWN                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {                ...                final int childrenCount = mChildrenCount;                if (newTouchTarget == null && childrenCount != 0) {                   ...                    //--------------------4----------------------                    for (int i = childrenCount - 1; i >= 0; i--) {                    ...                        if (!canViewReceivePointerEvents(child)                                || !isTransformedTouchPointInView(x, y, child, null)) {                            ev.setTargetAccessibilityFocus(false);                            continue;                        }                        newTouchTarget = getTouchTarget(child);                        if (newTouchTarget != null) {                            newTouchTarget.pointerIdBits |= idBitsToAssign;                            break;                        }                        //--------------------5----------------------                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {                        ...                        }                    }                }            }        }    }}
@Overridepublic void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); }}
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;}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { ... 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; } ...}

经过一番查找,终于把源码摘清。我这里是Android9.0的源码,如果跟各位的有出入,以各位手上的源码为准。闲话少说,直接看源码中都做了什么。
上面的代码共有五处注释:


注释1:这部分代码主要是清除事件接收目标同时初始化事件状态。我们看到先判断是否是ACTION_DOWN事件,也就是手指按下的事件。如果是,则调清除之前的事件接收目标,同时也初始化了点击状态。这也很好理解,当触发ACTION_DOWN时说明用户重新对手机做了操作,有可能点击了不同的控件,之前事件接收目标及其点击状态都应该重置掉。

注释2:这部分代码主要是用来判断是否要拦截事件。首先判断了是否是ACTION_DOWN事件,或者事件接收目标是否为空。如果这两个条件都不成立,将会标识拦截。在判断条件内我们还看到了FLAG_DISALLOW_INTERCEPT标识,这个标识表示子View是否允许父控件拦截事件,当子View调用requestDisallowInterceptTouchEvent(boolean)方法时设置的。只有当requestDisallowInterceptTouchEvent(boolean)传入值为true时,表示不允许父控件拦截(上面第二段代码)。disallowIntercept默认值为false,也就是子控件没有设置禁止父控件拦截事件。这时,会调用onInterceptTouchEvent()方法(第三段代码).

注释3:这段代码主要来判断是否发生了ACTION_CANCEL事件。这个事件在基础知识准备中已经有所说明,这里就不再详细讲解。


注释4:通过循环遍历找到子View。首先是通过倒序循环遍历的方式找到每一个子View,为什么是倒序呢,。。。。找到子View之后会调用canViewReceivePointerEvents()和isTransformedTouchPointInView()方法来判断子View的状态。这里边包括是否可见、是否正在播放动画、点击事件的坐标是否落在子View的显示范围内。

注释5:这部分代码是ViewGroup事件分发的核心。这里会调用dispatchTransformedTouchEvent()方法(第四段代码),在这个方法中我们可以看到,回去判断是否有子View,如果有,继续调用子View的dispatchTouchEvent()方法;如果没有,则去调用super.dispatchTouchEvent()方法。

我们都知道ViewGroup是继承自View的,所以接下来让我们看一下View的dispatchTouchEvent()方法。


View事件分发


public boolean dispatchTouchEvent(MotionEvent event) {    ...    boolean result = false;    if (onFilterTouchEventForSecurity(event)) {        ...        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 信息,也会调用 OnTouchListener onTouch() 方法。 如果 OnTouchListener 不为空,并且 onTouch() 方法返回true之后,就不会再执行 onTouchEvent() 方法,否则的话还要去执行 onTouchEvent() 方法。 从中我们也可以看出 OnTouchListener 中的 onTouch() 方法优先级要高于 onTouchEvent(MotionEvent) 方法。


onTouchEvent(MotionEvent)


View 的事件分发过程中调用了 onTouchEvent(MotionEvent) 方法,我们来看一下。


public boolean onTouchEvent(MotionEvent event) {    ...    //----------------------1----------------------    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;    //----------------------2----------------------    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;    }    ...    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) {                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {                        if (!focusTaken) {                            if (mPerformClick == null) {                                mPerformClick = new PerformClick();                            }                            if (!post(mPerformClick)) {                            //----------------------3----------------------                                performClickInternal();                            }                        }                    }                }                break;            case MotionEvent.ACTION_DOWN:                if (!clickable) {                    checkForLongClick(0, x, y);                    break;                }                if (performButtonActionOnTouchDown(event)) {                    break;                }                boolean isInScrollingContainer = isInScrollingContainer();                if (isInScrollingContainer) {                    mPrivateFlags |= PFLAG_PREPRESSED;                    if (mPendingCheckForTap == null) {                        mPendingCheckForTap = new CheckForTap();                    }                    mPendingCheckForTap.x = event.getX();                    mPendingCheckForTap.y = event.getY();                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());                } else {                    // Not inside a scrolling container, so show the feedback right away                    setPressed(true, x, y);                    checkForLongClick(0, x, y);                }                break;            ...        }        return true;    }    return false;}
private boolean performClickInternal() { ... return performClick();}
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;}

注释1处检查了该 View 是否是可点击或者是长按点击,并记录下来; 注释2处我们从英文注释中可以看到,虽然一个 View 是一个 disabled 状态,但是只要是 clickable 任然会消费事件,只是不做任何反馈而已。


之后会根据事件类型进行判断,当处于
ACTION_UP 也就是说当手指离开时,会触发 performClickInternal 方法,这个方法去执行内部点击,具体调用方法在最后两段代码中。







请到「今天看啥」查看全文