Android事件分发机制

2017-03-29  本文已影响70人  锐_nmpoi

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 事件。
上一篇下一篇

猜你喜欢

热点阅读