Android开发Android技术知识Android自定义View

深入理解Android事件分发机制

2018-10-21  本文已影响57人  maoqitian

在理解事件分发机制之前,我们先要明白,事件分发机制是为View服务的,而View是Android中所有控件的基类,View可以是单个的,而多个View组成可以叫做ViewGroup。不管什么View控件,他们基类都是View,在Android多个View的叠加则Web中的DOM树形结构,所以当我们点击一个区域有多个View的情况下,到底这时候该哪个View来响应我们的点击事件呢?事件分发机制就是为了解决这个问题而产生的。

ViewGroup官方文档继承关系.png
- 如下源码,如果ViewGroup将事件传递到子View,则会调用addTouchTarget(child, idBitsToAssign)方法,并退出遍历子View的循环

```
public boolean dispatchTouchEvent(MotionEvent ev) {
.......
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
.....
 // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } 
   .....     
}
/**
 * Adds a touch target for specified child to the beginning of the list.
 * Assumes the target child is not already present.
 */
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}                            
```
- 如上源码,调用addTouchTarget方法,会给mFirstTouchTarget赋值,也就是说mFirstTouchTarget!=null,前面我们已经讨论过,mFirstTouchTarget==null则拦截所有的事件给该ViewGroup处理,可见mFirstTouchTarget是否赋值对于ViewGroup的事件拦截起了关键的作用。
- 接着往下看,如果子View遍历结束后事件还是没有进行处理,这样的情况有两种可能,一个就是上面提到的例子场景二,ViewGroup的子View没有消费事件,也就是子View的onTouchEvent返回了false,另一个情况则是则是ViewGroup子View,也就不存在事件传递子View的情况。我们看如下代码,是在上面分析的代码之后出现,第三个参数子View为null,也就是调用super.dispatchTouchEvent(event)方法,ViewGroup是继承View,也就是说不管是否拦截,ViewGropu最终还是将点击事件交由到View来处理了,只是child.dispatchTouchEvent还是super.dispatchTouchEvent的问题。 ViewGroup的源码事件分发就到这里,接下来我们分析一下View的事件分发流程。
```
public boolean dispatchTouchEvent(MotionEvent ev) {
.....
 // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } 
   .....     
}
```
```
 /**
 * 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) {
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;
        }
    }
.....  
return result;
}
```
 - 我们看到上面的源码中,View对于点击的事件的处理首先是判断是注册OnTouchListener,并且如果OnTouchListener的onTouch放回true,则整个dispatchTouchEvent返回true,已经拦截了事件,则不会执行下面的onTouchEvent方法的调用,也就是说事件拦截了,但是不调用onTouchEvent方法,这里其实很好理解,如果开发者注册了OnTouchListener并在onTouch放回true,说明开发者是想自己来处理触摸事件,而onTouchEvent是属于Android的事件传递机制方法,是系统帮我们处理的,所以当我们自己处理了点击事件,就不需要系统来再次处理了。所以OnTouchListener的调用有先级高于onTouchEvent。
- 如果Ciew没有注册OnTouchListener方法,接下来事件传递到onTouchEvent方法,我们接着看onTouchEvent源码
```
 /**
 * Implement this method to handle touch screen motion events.
 * <p>
 * If this method is used to detect click actions, it is recommended that
 * the actions be performed by implementing and calling
 * {@link #performClick()}. This will ensure consistent system behavior,
 * including:
 * <ul>
 * <li>obeying click sound preferences
 * <li>dispatching OnClickListener calls
 * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
 * accessibility features are enabled
 * </ul>
 *
 * @param event The motion event.
 * @return True if the event was handled, false otherwise.
 */
public boolean onTouchEvent(MotionEvent event) {
    ....
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    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;
    }
  ....
  }
```
- 通过上面源码和注释,我们可以知道,View即使是处于不可用状态,他还是会消费(consumes)点击事件,只是他不会响应点击事件,也就是返回各种点击的状态(点击,长按)。
- 接着看看剩下源码对点击事件的处理

```
public boolean onTouchEvent(MotionEvent event) {
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
               ......
                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) {
                        // 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)) {
                                performClickInternal();
                            }
                        }
                    }
                .....
                }
            break;
        }
        ....
        return true;
    }
    return false;
}
```
- 触摸事件结束,也就是ACTION_UP,所以这里我们看对于ACTION_UP的处理就可以了。我们看到对于ACTION_UP,,如果没有!clickable,也就是没有View的CLICKABLE、LONG_CLICKABLE和CONTEXT_CLICKABLE都不存在,则清除所有的状态回调等,如果其中一个存在,则直接消费这个时间,我们看到方法后面有个retrun true存在,也证实事件被消费了,也就是onTouchEvent方法返回了true。而如果ACTION_UP没有消费事件,最终onTouchEvent方法是返回false。
- 到这里,我们还看到ACTION_UP事件会触发performClickInternal();方法,我们看看他做了什么

```
 private boolean performClickInternal() {
    // Must notify autofill manager before performing the click actions to avoid scenarios where
    // the app has a click listener that changes the state of views the autofill service might
    // be interested on.
    notifyAutofillManagerOnClick();
    return performClick();
}

public boolean performClick() {
    // We still need to call this method to handle the cases where performClick() was called
    // externally, instead of through performClickInternal()
    notifyAutofillManagerOnClick();
    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);
    notifyEnterOrExitForAutoFillIfNeeded(true);
    return result;
}
```
- 可以从源码看到他最终调用的是performClick()方法,如果View设置了OnClickListener,则会调用onClick方法。我们知道View默认的LONG_CLICKABLE是false,而CLICKABLE需要根据具体View才能知道,比如Button是可点击的,则CLICKABLE为true,而ImageView默认是不可点击的,所以CLICKABLE为false,但是开发中我们也发现,不管View是否可以点击,只要我们设置setOnClickListener()或者setOnLongClickListener()方法,则该View就是可以被点击或者长按的,也就是LONG_CLICKABLE或者CLICKABLE为true。我们从源码可以看出。到此,从源码角度分析事件分发机制的流程我们已经走完。

```
 /**
 * Register a callback to be invoked when this view is clicked. If this view is not
 * clickable, it becomes clickable.
 *
 * @param l The callback that will run
 *
 * @see #setClickable(boolean)
 */
public void setOnClickListener(@Nullable OnClickListener l) {
    if (!isClickable()) {
        setClickable(true);
    }
    getListenerInfo().mOnClickListener = l;
}

public void setOnLongClickListener(@Nullable OnLongClickListener l) {
    if (!isLongClickable()) {
        setLongClickable(true);
    }
    getListenerInfo().mOnLongClickListener = l;
}
```

最后,总结事件分发机制的核心知识点

终于,把事件分发机制给回顾了一遍,其实五月份的时候我就复习过一次事件分发机制,但是当时没有记录,所以这次在回头看以前记得有些知识点感觉还是模糊,所以记录下来才能在以后忘记的时候去回顾再总结。如果文章中有写得不对的地方,请给我留言指出,大家一起学习进步。如果觉得我的文章给予你帮助,也请给我一个喜欢和关注。

上一篇下一篇

猜你喜欢

热点阅读