您的位置:首页 > 产品设计 > UI/UE

事件分发的源码解析

2016-12-28 17:14 337 查看
经过上一节的讲解,我们本节从Android源码来分析

1、activity对点击事件的分发过程

点击事件有motionevent来表示,当一个点击操作发生时,事件最先传递给当前的activity,有activity的dispatchTouchevent来进行事件分发,具体的工作由activity的内部window来完成,window会将事件传递给decor view,decorview一般就是当前界面的底层容器(即setcontetview所设置view的父容器),通过Activity.getwindow.decorview()来获得。我们先从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 implem2、entation 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);

    }

activity先把事件交给window进行分发,如果返回true,那就说明事件已经被处理了,整个事件循环就结束了。如果返回false,那么说明事件没有被处理,所有的view的ontouchevent返回都为false,事件将要交给activity的ontouchevent来处理。

接下来,我们看看window是如何传递给viewgroup的.通过源码我们知道window是个抽象类,superDispatchTouchEvent是个抽象方法,因此我们必须找到window的实现类才行,他的唯一实现类是phonewindow 这个可以从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.

 *

 * <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 {

他的大概意思是window可以控制顶级view的外观和行为策略,他的唯一实现位于Android.policy.phoneWindow这个类。尽管实列化的时候此类会被重构,但仅是重构而已,功能是类似的,他处理点击事件如下所示

  public boolean superDispatchTouchEvent(MotionEvent event){

return mdecor.superDispatchTouchEvent(event);

};

这里可以看清楚我们的phonewindow把点击事件传递给decorview ,我们来看看decorview是什么呢?

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {}


1. PhoneWindow根据Manifest的主题或者是特定的PhoneWindow设置去生成一个DevorView的布局,作为跟视图(Root View)。

2. Activity调用setContentView()方法把用户自定义的Layout XML文件作为内容视图(Content View), 当然其内部是调用PhoneWindow的setContentView()方法。

3. 经过上两步,UI视图就已经形成了,那么当UI每次被刷新的时候,View Tree就会像上面所说的那样被Traverse。
到这里我们的事件传递到顶级view了,即在activity中通过setcntentview 设置的view了,另外顶级view又叫做根view,顶级view一般都是viewgroup;下面我们来看看顶级view对点击事件 的分发过程。
2、顶级view对事件的分发过程解析

关于事件如何在顶级view中分发,我们上一篇文章已经讲过了,这里来回忆一下;当点击事件到达顶级view也就是viewgroup之后,会触发view的dispatchTouchEvent方法之后的逻辑是这样的:如果顶级viewgroup拦截事件即onintercepttouchevent方法返回true;则事件有viewgroup处理,如果viewgroup的montouchlistener被设置,则ontouch方法会被调用,否则ontouchevent会被调用,如果顶级viewgroup不拦截事件,则事件会传递给他所在点击事件上的子view,这时子view上的dispatchtouchevent方法会被调用,这是顶级view的事件传递给子view,接下来的传递过程和顶级view的传递过程一致。

首先来看看viewgroup对点击事件的分发过程,主要实现在dispatchtouchevent的方法中,但这个方法比较长,我们分段分析,先看看下边的他描述的是view是否拦截当前点击事件的逻辑

 // 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有两种情况会判断是否拦截当前事件:事件类型为actiondown或者mfirsttouchtarget!=null;actiondown好理解,但是mfirsttouchtarget!=null又该怎么理解呢?这个从后面的代码逻辑可以看出来,当事件由viewgroup的子元素成功处理时,mfirsttouchtarget会被赋值并指向子元素,换个方式来说,当viewgroup不拦截事件,并将事件交由子元素处理时mfirsttouchtarget!=null,反过来一旦事件被当前viewgroup拦截时,mfirsttouchtarget!=null就不成立,那么当actionmove和actionup事件到来时,由于actionMasked
== MotionEvent.ACTION_DOWN  || mFirstTouchTarget != null这个条件为false,那么将导致viewgroup的onInterceptTouchEvent不会再被调用,并且同一事件中的其他事件都会默认交给他来处理。

当然这里有一个特殊情况,就是FLAG_DISALLOW_INTERCEPT标记为,这个标记为是通过requestdisallowintercepttouchevent来设置,一般用于子view中,FLAG_DISALLOW_INTERCEPT一旦设置后,viewgroup将没有办法拦截除了actiondown以外的其他事件,因为当viewgroup在处理分发事件时,如果是actiondown时就会重置FLAG_DISALLOW_INTERCEPT标记为,导致子view中设置这个标记为是无效的。当面对actiondown时viewgroup总会调用自己的onintercepttouchevent方法来询问自己是否要拦截事件。在下面的代码中,当actiondown事件到来时,做重置状态的操作,而在tresttouchstate方法中会对FLAG_DISALLOW_INTERCEPT进行重置,因此子view调用

requestdisallowintercepttouchevent并不影响子viewgroup对actiondown的处理。

            // 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决定拦截事件后,其他的事件都将会交由他来处理,不再调用onintercepttouchevent方法,FLAG_DISALLOW_INTERCEPT标志位是让viewgroup不再拦截actiondown事件,总结得到以下几点(1)、onintercepttouchevent不是每次都会被调用的,如果我们想提前处理所有的点击事件,要选择dispatchtouchevent方法,只有这个方法每次都会被调用。另外一点FLAG_DISALLOW_INTERCEPT标记为给我们提供另外一种思路,当面对滑动冲突时可以考虑用此方法来解决。

3、view对点击事件的处理过程

view对点击事件的过程要稍微简单,注意这里的view不包含viewgroup,首先看他的dispatchtouchevent方法:

 /**

     * 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.

        if (event.isTargetAccessibilityFocus()) {

            // We don't have focus or no virtual descendant has it, do not handle the event.

            if (!isAccessibilityFocusedViewOrHost()) {

                return false;

            }

            // We have focus and got the event, then use normal event dispatch.

            event.setTargetAccessibilityFocus(false);

        }

        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的实现先看view处于不可用状态下点击事件的处理过程,由此可以看出不可用状态下view的点击事件还是会被消耗,尽管他看起来不可用。

  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);

        }

下边来看看view对点击事件的处理

        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和longclickable其中一个为true那么事件就会被消耗掉,当actionup事件发生时,会触发performclick方法,如果view设置了onclicklistener,那么performclick中会调用conclick方法。如下所示

  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;

        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        return result;

    }
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android UI