Android 事件分发机制(通过源码解析,附带记忆图)

来源:互联网 发布:matlab算法工具箱 编辑:程序博客网 时间:2024/05/14 09:16

Android 事件分发机制详解

1、简介

Android事件分发机制不仅是Android开发体系中的重点也是难点,掌握好了事件分发机制也是我们解决自定义控件、view的滑动冲突等问题的基础。接下来我将通过图示流程以及源码两个方面进行讨论。
【话说小波 http://blog.csdn.net/u013277209?viewmode=contents 】

2、初次认识三个方法:

所谓事件的分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递过程就是分发过程。点击事件的分发过程由下面三个很重要的方法来共同完成。
1public boolean dispatchTouchEvent(MotionEvent ev)    用来进行事件的分发。如果事件能够传递给当前的View,那么此事件一定会被调用,返回结果受当前View的onTouchEvent和下级view的dispatchTouchEvent影响,表示是否消耗当前事件。
2public boolean onInterceptTouchEvent(MotionEvent ev)    在方法1内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
3public boolean onTouchEvent(MotionEvent ev)    在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前view无法再次接收事件。

然后我们在ViewGroup用伪代码展现他们三者的关系:

1public boolean dispatchTouchEvent(MotionEvent ev){2boolean consume = false;3if(onInterceptTouchEvent(ev)){4、      consume=onTouchEvent(ev);5、  }else{6、      consume=child.dispatchTouchEvent(ev);7、  }8return consume;9、}

看了上面代码想必大家已经对ViewGroup的传递规则有了大致的了解:对于一个根ViewGroup来说,点击事件产生后,首先会调用它,这时它的dispatchTouchEvent就会调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示他要拦截当前事件,接着这个ViewGroup的onTouchEvent方法就会调用(这里默认它没设置OnTouchListener,具体为什么,这里先不讲),如果onInterceptTouchEvent返回false,就表示它不拦截当前事件,这时事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,然后继续上述过程,直到事件被处理。

2.2、一张图展示事件分发的过程:

事件分发机制整个流程

  1. 如果事件不终止传递,整个事件传递就类似一个U型图,那么整个事件传递是activity→ViewGroup→View由上向下调用dispatchTouchEvent,一直到叶子节点View的时候,在由View→ViewGroup→activity从下往上调用onTouchEvent方法。
  2. dispatchTouchEvent或onTouchEvent一旦返回true,证明事件被消费了,事件就到达了终点。不会再被传递,没有谁能再收到这个事件了
  3. dispatchTouchEvent 和 onTouchEvent return false的时候事件都回传给父控件的onTouchEvent处理。对于dispatchTouchEvent 返回 false 的含义应该是:事件停止往子View传递和分发同时开始往父控件回溯(父控件的onTouchEvent开始从下往上回传直到某个onTouchEvent return true),事件分发机制就像递归,return false 的意义就是递归停止然后开始回溯。对于onTouchEvent return false 就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动。
  4. 上图只是针对ACTION_DOWN事件的流程,如果是ACTION_MOVE和ACTION_UP事件,直接会被消费了ACTION_DOWN事件的那个View的OnTouchEvent方法处理。不会再调用onInterceptTouchEvent方法。

总结一下。规律就是
如果当前Layout intercept了,那么子View和子ViewGroup都没有机会去获得Touch事件了。如果当前Layout并不消费事件的话,这个事件会一直向上冒泡,直到某个父Layout的onTouchEvent消费了这个事件。如果没有任何一个父Layout消费这个事件,那么后续的事件都不会被接受。
如果在冒泡过程中有某个Layout消费了这个事件。那么这个Layout的所有父Layout的intercept仍然会被调用。但是当前Layout的intercept不会再被调用了。直接调用onTouch事件。

另外,对于底层的View来说,有一种方法可以阻止父层的View截获touch事件,就是调用getParent().requestDisallowInterceptTouchEvent(true);方法。一旦底层View收到touch的action后调用这个方法那么父层View就不会再调用onInterceptTouchEvent了,也无法截获以后的action。在实践过程中发现ListView在滚动的时候会调用这个方法。使得action不能被拦截。

2.3、针对上述四点用源码来探究其过程

首先事件从Activity的dispatchTouchEvent()方法开始传递,让我们先来看源码

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

通过上面的代码我们知道,首先activity是交给所附属的Window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有View的onTouchEvent都返回了false,那么activity的onTouchEvent会调用。

接着我们去看一下window是怎么传给ViewGroup的,我们发现window是一个抽象类,我们在activity的attach方法里面发现,它的唯一实现是在android.policy.PhoneWindow这个类

final void attach(Context context, ActivityThread aThread,            Instrumentation instr, IBinder token, int ident,            Application application, Intent intent, ActivityInfo info,            CharSequence title, Activity parent, String id,            NonConfigurationInstances lastNonConfigurationInstances,            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {        ...        mWindow = new PhoneWindow(this);        ...    }

接下来我们去看一下PhoneWindow这个类,然后我们在PhoneWindow里面找到了这个方法

@Override    public boolean superDispatchTouchEvent(MotionEvent event) {        return mDecor.superDispatchTouchEvent(event);    }

PhoneWindow又把事件传给了DecorView,那么这个Decorview又是个什么呢?我们发现DecorView是PhoneWindow的一个内部类,它继承了FrameLayout,由此可知它也是一个ViewGroup。那么,DecroView到底充当了什么样的角色呢?其实,DecorView是整个ViewTree的最顶层View,它是一个FrameLayout布局,代表了整个应用的界面。下面我们看一下源码

protected ViewGroup generateLayout(DecorView decor) {        // Apply data from current theme.        // 从主题文件中获取样式信息        TypedArray a = getWindowStyle();        ...        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(...){            ...        }        // Inflate the window decor.        // 加载窗口布局        int layoutResource;        int features = getLocalFeatures();        // System.out.println("Features: 0x" + Integer.toHexString(features));        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {            layoutResource = R.layout.screen_swipe_dismiss;        } else if(...){            ...        }        View in = mLayoutInflater.inflate(layoutResource, null);    //加载layoutResource        decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); //往DecorView中添加子View,即mContentParent        mContentRoot = (ViewGroup) in;        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); // 这里获取的就是mContentParent        if (contentParent == null) {            throw new RuntimeException("Window couldn't find content container view");        }        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {            ProgressBar progress = getCircularProgressBar(false);            if (progress != null) {                progress.setIndeterminate(true);            }        }        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {            registerSwipeCallbacks();        }        // Remaining setup -- of background and title -- that only applies        // to top-level windows.        ...        return contentParent;    }

由以上代码可以看出,该方法还是做了相当多的工作的,首先根据设置的主题样式来设置DecorView的风格,比如说有没有titlebar之类的,接着为DecorView添加子View,而这里的子View则是我们在activity里面放的布局view,如果上面设置了FEATURE_NO_ACTIONBAR,那么DecorView就是我们在activity里面设置的view。
上面解释了什么是Decorview,让我们言归正传,继续追踪事件的传递,我们到了DecorView的superDispatchTouchEvent方法。

public boolean superDispatchTouchEvent(MotionEvent event) {            return super.dispatchTouchEvent(event);        }

我们知道Decorview是继承FrameLayout的,所以上面调用了super.dispatchTouchEvent(event),最终我们恍然大悟,饶了这么一大圈,最终传到了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();    }

在上面代码中,ViewGroup会在ACTION_DOWN事件到来时重置状态操作,把mFirstTouchTarget置为null。由于一个完整的事件序列是以DOWN开始,以UP结束,所以如果是DOWN事件,那么说明是一个新的事件序列,所以需要初始化之前的状态。这里的mFirstTouchTarget非常重要,后面会说到当ViewGroup的子元素成功处理事件的时候,mFirstTouchTarget会指向子元素,这里要留意一下。

第二段代码:

 // 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;            }

从上面代码可以看出,ViewGroup会在如下两种情况下会判断是否要拦截当前事件,ACTION_DOWN或者mFirstTouchTarget!=null的情况下,这里介绍一下mFirstTouchTarget,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,一旦事件由ViewGroup拦截时,mFirstTouchTarget==null,那么当ACTION_MOVE和ACTION_UP事件到来时,ViewGroup的onInterceptTouchEvent将不会再调用,并且同一序列中的其它事件都会默认交由它处理。而当子View消费了ACTION_DOWN事件,那么ACTION_MOVE和ACTION_UP过来时,依然要调用onInterceptTouchEvent进行询问是否要拦截,一旦拦截,那么整个事件序列的事件都交给父控件(ViewGroup)的onTouchEvent方法处理。用一句话总结为:父亲吃到肉了,绝不会再给儿子,儿子吃到肉了,父亲还可能抢。

但是这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。ViewGroup将无法拦截除了ACTION_DOWN以外的其它事件。针对于于这个标记位作如下总结:

  1. 子view不希望父view拦截事件可以调用mParent.requestDisallowInterceptTouchEvent(true)
  2. viewgroup的mGroupFlags在下一个cycle来临的时候,FLAG_DISALLOW_INTERCEPT标志位会被清零
  3. requestDisallowInterceptTouchEvent这个函数一般不是自己调用的,而是给子View调用的
  4. requestDisallowInterceptTouchEvent是解决滑动冲突的大杀器,目前大部分原生控件都是使用

接着再向下看,当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理

                        final View[] children = mChildren;                        for (int i = childrenCount - 1; i >= 0; i--) {                            final int childIndex = customOrder                                    ? getChildDrawingOrder(childrenCount, i) : i;                            final View child = (preorderedList == null)                                    ? children[childIndex] : preorderedList.get(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)) {                                // 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();                                newTouchTarget = addTouchTarget(child, idBitsToAssign);                                alreadyDispatchedToNewTouchTarget = true;                                break;                            }

上面代码逻辑也很清晰,首先遍历ViewGroup的所有子元素,然后判断子元素是否能接收到点击事件。如果能,那么事件就会传递给它来处理,如果子元素的dispatchTouchEvent返回true,那么mFirstTouchTarget就会被赋值,同时跳出for循环。

我们来看一下View对事件的处理过程
注意这里的View不包含ViewGroup,先来看它的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event) {        ...        boolean result = false;        if (mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onTouchEvent(event, 0);        }        final int actionMasked = event.getActionMasked();        if (actionMasked == MotionEvent.ACTION_DOWN) {            // Defensive cleanup for new gesture            stopNestedScroll();        }        if (onFilterTouchEventForSecurity(event)) {            //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;            }        }        if (!result && mInputEventConsistencyVerifier != null) {            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);        }        // Clean up after nested scrolls if this is the end of a gesture;        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest        // of the gesture.        if (actionMasked == MotionEvent.ACTION_UP ||                actionMasked == MotionEvent.ACTION_CANCEL ||                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {            stopNestedScroll();        }        return result;    }

View对点击事件处理过程比较简单了,因为View是单独的一个元素,它没有子元素,所以没法向下传递事件,它只能自己处理事件。从上面的源码可以看出,首先要判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch返回true,那么onTouchEvent就不会调用了,可见OnTouchListener的优先级比onTouchEvent的优先级要高。

接下来我们来看onTouchEvent的实现

if ((viewFlags & ENABLED_MASK) == DISABLED) {            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {                setPressed(false);            }            // A disabled view that is clickable still consumes the touch            // events, it just doesn't respond to them.            return (((viewFlags & CLICKABLE) == CLICKABLE                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);        }        ...        if (((viewFlags & CLICKABLE) == CLICKABLE ||                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {            switch (action) {                case MotionEvent.ACTION_UP:                    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) {                            // 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;                case MotionEvent.ACTION_DOWN:                    mHasPerformedLongPress = false;                    if (performButtonActionOnTouchDown(event)) {                        break;                    }                    // Walk up the hierarchy to determine if we're inside a scrolling container.                    boolean isInScrollingContainer = isInScrollingContainer();                    // For views inside a scrolling container, delay the pressed feedback for                    // a short period in case this is a scroll.                    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);                    }                    break;                case MotionEvent.ACTION_CANCEL:                    setPressed(false);                    removeTapCallback();                    removeLongPressCallback();                    mInContextButtonPress = false;                    mHasPerformedLongPress = false;                    mIgnoreNextUpEvent = false;                    break;                case MotionEvent.ACTION_MOVE:                    drawableHotspotChanged(x, y);                    // Be lenient about moving outside of buttons                    if (!pointInView(x, y, mTouchSlop)) {                        // Outside button                        removeTapCallback();                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {                            // Remove any future long press/tap checks                            removeLongPressCallback();                            setPressed(false);                        }                    }                    break;            }            return true;        }

从上面代码可以看出,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent返回true,不管它是不是DISABLE状态。

注意:View的LONG_CLICKABLE属性默认为false,而CLICKABLE属性和具体的view有关,比如可点击的view(如button)的CLICKABLE属性默认为true,而不可点击的view(如textview)默认属性为false。通过setClickable和setLongClickable可以分别改变view的CLICKABLE和LONGCLICKABLE状态,另外setOnClickListener可以自动把CLICKABLE设为true,setOnLongClickListener可以自动把LONG_CLICKABLE设为true

参考文献:《Android开发艺术探索》
http://www.jianshu.com/p/e99b5e8bd67b

0 2
原创粉丝点击