View的事件分发机制(源码分析)

来源:互联网 发布:python聚类结果可视化 编辑:程序博客网 时间:2024/06/09 22:56

View的事件分发机制

点击事件的传递规则

事件分发就是当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递过程就是分发过程,由三个很重要的方法来共同完成:dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev)

用来进行事件的分发,如果事件能够传递到当前View,则一定为触发此方法,返回的结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消费当前事件

public boolean onInterceptTouchEvent(MotionEvent event)

在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件

public boolean onTouchEvent(MotionEvent event)

在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消费当前事件,如果不消费,则在同一个事件序列中,当前View无法再次接收到事件

三个方法的关系如下:

public boolean dispatchTouchEvent(MotionEvent ev){    boolean consume = false;    if(onInterceptTouchEvent(ev)){        consume = onTouchEvent(ev);    }else{        consume = child.dispatchTouchEvent(ev);    }    return consume;}

这里写图片描述

点击事件的传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它自身,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着这个事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件最终被处理

当一个View需要处理事件时,如果它设置了OnTouchListener,那个onTouch方法会被回调。如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用。所以,给View设置OnTouchlistener,其优先级比onTouchEvent要高,在onTouchEvent方法中,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。可以看出,OnClickListener的优先级最低,处于事件传递的尾端

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,顶级View收到事件后,就会按照事件分发机制去分发事件。

总结:

  • 同一个事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,down->move->…->move->up
  • 正常情况下, 一个事件序列只能被一个View拦截且消耗。一旦一个元素拦截了此事件,那么同一个事件序列内的所有事件都会直接交给它处理
  • 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会再被调用(拦截后,下次不会再询问它是否要拦截了)
  • 某个View一旦开始处理事件,如果它不消费ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理
  • 如果View只消费了ACTION_DOWN事件,那么这个点击事件会消失,当前View可以持续收到后续事件,最终这些消失的点击事件会传递给Activity处理
  • ViewGroup默认不拦截任何事件,即onInterceptTouchEvent返回false
  • View的onTouchEvent默认都会消费事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable属性默认都会false,clickable属性要分情况,比如Button的clickable属性默认为true,TextView的就默认为false
  • View的enable属性不影响onTouchEvent的默认返回值,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true
  • onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件
  • 事件传递过程是由外向内的,由父级分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外

事件分发的源码分析

1.Activity对点击事件的分发过程

事件先传给当前Activity,由Activity的dispatchTouchEvent来进行事件派发,具体工作是由Activity内部的Window来完成的,Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(setContentView设置的View的父容器),可以通过Activity.getWindow.getDecorView()获得

源码: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 implementation 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);}

首先事件交给window进行分发,如果所有的View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用

源码:Window#superDispatchTouchEvent

/** * Used by custom windows, such as Dialog, to pass the touch screen event * further down the view hierarchy. Application developers should * not need to implement or call this. * */public abstract boolean superDispatchTouchEvent(MotionEvent event);

Window类是一个抽象类,其实现类是PhoneWindow

源码:PhoneWindow#superDispatchTouchEvent

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

通过源码可以看出,PhoneWindow将事件直接传递给了DecorView

// This is the top-level view of the window, containing the window decor.private DecorView mDecor;@Overridepublic final View getDecorView() {    if (mDecor == null) {        installDecor();    }    return mDecor;}

这个mDecor就是我们通过setContentView设置的View的父级,也就是说现在事件传递到了顶级(根)View

2.顶级View对点击事件的分发过程

首先看一下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;    }

从上面代码中我们可以看出,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget!=null。当ViewGroup不拦截事件并将事件交给子元素处理时mFirstTouchTarget!=null。也就是说一旦ViewGroup拦截事件,当ACTION_MOVE和ACTION_UP事件到来时,(actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null)为false,导致ViewGroup的onInterceptTouchEvent不会再被调用(intercepted = true),并且同一序列中的其他事件都会默认交给它处理

这里有一个特殊情况,当disallowIntercept(final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)为true时,会执行intercepted = false。FLAG_DISALLOW_INTERCEPT是一个标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。一旦子View请求父级不要拦截后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。

为什么是除了ACTION_DOWN以外的事件呢?因为ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作,而在resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT进行重置

// 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不拦截事件的时候,事件会向下分发交由它的子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的所有子元素,然后判断子元素是否能够接收到点击事件:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内;如果某个子元素满足这两个条件,那么事件就会传递给它来处理。dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法,这样事件就交由子元素处理了,从而完成了一轮事件分发

/** * Transforms a motion event into the coordinate space of a particular child view, * filters out irrelevant pointer ids, and overrides its action if necessary. * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead. */private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,        View child, int desiredPointerIdBits) {    final boolean handled;    // Canceling motions is a special case.  We don't need to perform any transformations    // or filtering.  The important part is the action, not the contents.    final int oldAction = event.getAction();    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {        event.setAction(MotionEvent.ACTION_CANCEL);        if (child == null) {            handled = super.dispatchTouchEvent(event);        } else {            handled = child.dispatchTouchEvent(event);        }        event.setAction(oldAction);        return handled;    }    ……}

如果子元素的dispatchTouchEvent返回true,mFirstTouchTarget就会被赋值同时跳出for循环

newTouchTarget = addTouchTarget(child, idBitsToAssign);alreadyDispatchedToNewTouchTarget = true;break;

如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素(如果还有下一个子元素)

mFirstTouchTarget的赋值在addTouchTarget内部完成,mFirstTouchTarget是一种单链表结构,mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列中所有的点击事件

/** * Adds a touch target for specified child to the beginning of the list. * Assumes the target child is not already present. */private TouchTarget addTouchTarget(View child, int pointerIdBits) {    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);    target.next = mFirstTouchTarget;    mFirstTouchTarget = target;    return target;}

如果遍历完所有的子元素后事件都没有被消费:ViewGroup没有子元素或子元素在onTouchEvent中返回了false,这时ViewGroup会自己处理点击事件

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

3.View对点击事件的处理过程

/** * 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)) {        //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,那么onTouchEvent就不会被调用(OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件)。

onTouchEvent的实现如下,可以看出当View处于不可用状态依然会消耗点击事件

/** * 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 float x = event.getX();    final float y = event.getY();    final int viewFlags = mViewFlags;    if ((viewFlags & ENABLED_MASK) == DISABLED) {        if (event.getAction() == 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));    }    ……}

接着,如果View设置有代理,还会执行TouchDelegate的onTouchEvent方法

if (mTouchDelegate != null) {        if (mTouchDelegate.onTouchEvent(event)) {            return true;        }}

再看一下onTouchEvent中对点击事件的具体处理

if (((viewFlags & CLICKABLE) == CLICKABLE ||            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {        switch (event.getAction()) {            case MotionEvent.ACTION_UP:                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {                    ……                    if (!mHasPerformedLongPress) {                        // 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();                            }                        }                    }                    ……                }                break;                ……        }        ……        return true;    }

只要View的CLICKABLE和LONG_CLICKABLE有一个为true,就会消费这个事件,即onTouchEvent返回为true,当ACTION_UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法

/** * Call this view's OnClickListener, if it is defined.  Performs all normal * actions associated with clicking: reporting accessibility event, playing * a sound, etc. * * @return True there was an assigned OnClickListener that was called, false *         otherwise is returned. */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;}

setOnClickListener和setOnLongClickListener会自动将View的CLICKABLE和LONG_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(OnClickListener l) {    if (!isClickable()) {        setClickable(true);    }    getListenerInfo().mOnClickListener = l;}/** * Register a callback to be invoked when this view is clicked and held. If this view is not * long clickable, it becomes long clickable. * * @param l The callback that will run * * @see #setLongClickable(boolean) */public void setOnLongClickListener(OnLongClickListener l) {    if (!isLongClickable()) {        setLongClickable(true);    }    getListenerInfo().mOnLongClickListener = l;}

参考:《Android开发艺术探索》

0 0
原创粉丝点击