美文网首页
Android事件分发机制

Android事件分发机制

作者: 锐_nmpoi | 来源:发表于2017-03-29 22:15 被阅读70次

    Activity对点击事件的分发

    在点击事件产生了,首先最先接触到点击事件的就是Activity,Activity通过 dispatchTouchEvent 来进行事件的分发。源码如下:

    <b>Activity # dispatchTouchEvent</b>

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

    由上面的源码可以得知,从getWindow().superDispatchTouchEvent(ev) 中得知, Activity事件的分发是由内部的Window来完成的。如果他返回的是true,那个事件循环就此结束了。如果返回false ,那么意味着所有View 的onTouchEvent都返回了false,都不处理事件,那么最后 return onTouchEvent(ev); 就会执行,既Activity中onTouchEvent就会执行。

    从上面可以知道,getWindow().superDispatchTouchEvent(ev)的执行就是用来把事件分发给子View的。

    <b>Window # superDispatchTouchEvent</b>

     public abstract boolean superDispatchTouchEvent(MotionEvent event);
    

    但是这个方法是abstract方法,那么就有继承于Window的类实现了这个方法,从Activity的启动中,我们可以得知 PhoneWindow 就是这个类。

    <b>PhoneWindow # superDispatchTouchEvent</b>

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

    那么PhoneWindow直接把事件传递给了 DecorView

    从PhoneWindow 的构造方法中可以看到

    mDecor = (DecorView) preservedWindow.getDecorView();
    

    Window # getDecorView

     /**
     * Retrieve the top-level window decor view (containing the standard
     * window frame/decorations and the client's content inside of that), which
     * can be added as a window to the window manager.
     *
     * <p><em>Note that calling this function for the first time "locks in"
     * various window characteristics as described in
     * {@link #setContentView(View, android.view.ViewGroup.LayoutParams)}.</em></p>
     *
     * @return Returns the top-level window decor view.
     */
    public abstract View getDecorView();
    

    从Window中getDecorView的注解可以看到DecorView拿到的就是Activity所设置的View,而这个View就是我们在Activity中通过setContentView设置的View。那么目标事件就会传递给了子View。

    从这里就知道Activity把事件传递给了顶级View。顶级View也称根View,通常这个View是ViewGroup。

    顶级VIew对事件的分发过程

    从上述中,事件已经传递给了DecorView,而DecorView是继承于FrameLayout,FrameLayout又继承于ViewGroup。处理事件的分发原理就可以从 ViewGroup 中找到。

    ViewGroup # dispatchTouchEvent

    我们逐步来看这个方法。

     // 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;
        }
    
    ....
    newTouchTarget = addTouchTarget(child, idBitsToAssign);
    ....
    
    
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
            final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
            target.next = mFirstTouchTarget;
            mFirstTouchTarget = target;
            return target;
        }
    

    从上面可以知道,ViewGroup中 onInterceptTouchEvent 是否被调用,看的是事件的传递是否为MotionEvent.ACTION_DOWN 或者 mFirstTouchTarget != null 。 MotionEvent.ACTION_DOWN 这个很好判断。至于mFirstTouchTarget != null 要从后面的代码分析 。 从后面的代码中可以看到,当事件由ViewGroup的子元素处理成功时,在addTouchTarget方法中,mFirstTouchTarget就指向这个子元素。所以一旦当前ViewGroup拦截了这个事件,不会再调用onInterceptTouchEvent 方法 ,那么这一系列的事件将不再进行分发,全部交给ViewGroup处理。

    当然除了上面 if 的两个条件,影响是否 调用 onInterceptTouchEvent 方法的就是 FLAG_DISALLOW_INTERCEPT 标志位。 这个标记位是通过requestDisallowInterceptTouchEvent 方法来设置的,一般用在子View中。 FLAG_DISALLOW_INTERCEPT 标志位 一旦设置后,那么除了ACTION_DOWN事件外,其他事件都被拦截掉了。Why?因为在ViewGroup进行事件分发的时候,如果是ACTION_DOWN事件,那么就会重置 FLAG_DISALLOW_INTERCEPT ,将导致子View中设置这个标志位无效。因此,我们可以利用这个来解决滑动冲突。

    当ViewGroup不再拦截的时候,就会继续执行下面的代码:

    ViewGroup # dispatchTouchEvent

    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);
    
        .....
    
        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 # dispatchTransformedTouchEvent

    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    

    再从上面代码可以清楚的看到,ViewGroup就会遍历所有的子View。拿到子View的时候,源码中做了几个判断(上述省略了),判断这个子View是否符合条件。如果不满足则直接选取下一个子View。 拿到符合条件的子View,那么将调用 dispatchTransformedTouchEvent 这个方法。 在dispatchTransformedTouchEvent方法中就调用了child.dispatchTouchEvent(event)。接下来就把事件分发到了子View身上了。

    如果遍历了所有的子View事件都没有被合适的处理,可能有以下两种情况:

    1. ViewGroup没有子View
    2. 子View处理了点击事件,但是在 onTouchEvent 中返回了false。

    在这两种情况下,那么ViewGroup就会处理自己的点击事件。源码如下:

    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    }
    

    从上面的dispatchTransformedTouchEvent中的第三个参数原本为Child,而现在改为null ,那么 将调用 super.dispatchTouchEvent(event) 这个方法,交给自己的View处理。

    VIew对事件的处理过程

    public boolean dispatchTouchEvent(MotionEvent event) {
        .....
    
        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;
            }
        }
    
        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        
        .....
    
        return result;
    }
    

    从View对事件的分发来说,首先会判断有没有设置 OnTouchListener,如果OnTouchListener中的onTouch返回true,那么onTouchEvent就不会被调用,可见OnTouchListener 的优先级高于onTouchEvent。

    下面来看看 onTouchEvent 的源码:

    View # 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);
    }
    

    上面源码注释写的很清楚了,当View处于不可用的状态下,点击事件照样会消耗点击事件。

    继续接下来:

    View # onTouchEvent

    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) {
    
                ....
        
                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;
    }
    

    从上面if的条件可以看出来,只用View的CLICKABLE和LONG_CLICKABLE有一个为true,那么就会消耗这个事件,既onTouchEvent返回true。

    从case MotionEvent.ACTION_UP : 和 注解 Only perform take click actions if we were in the pressed state

    从上面也可以看到,当View接受到了Down 和 Up 事件的时候 ,会触发performClick方法。这个方法内部其实就是调用了onClick方法。既实现了View 的点击事件。

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

    可以得出以下的结论:

    1. View的enable 属性不影响onTouchEvent的默认返回值,哪怕是一个View是disable状态,只要它的clickable或者longClickable有一个为true,那么他的onTouchEvent就返回true。
    2. onClick会发生前提是View是可以点击的,并且他收到了down 和 up 事件。

    相关文章

      网友评论

          本文标题:Android事件分发机制

          本文链接:https://www.haomeiwen.com/subject/qjfdottx.html