第三章 View事件体系(2)之事件分发

本文为Android开发艺术探索的笔记,仅供学习

1 事件的分发机制

上面几节介绍了View的滑动体系,现在我们要来将一个View 非常核心的一个知识点,事件的分发机制,不论是初学者还是中级开发者都要面对的一个问题就是滑动冲突,为了去解决该问题,我们需要更多的去了解一些基本的分发机制,从而去解决这个滑动冲突。

1.1点击事件的传递

其实事件的分发是要是对motionEvent进行分发,大家都是知道里面包括了三个MotionAction,那么系统需要对该事件进行分发,分发就需要去认识一下三个函数


分发事件的机制大致就是,对于一个跟ViewGroup来说,当有点击事件产生的时候,会调用dispathtouchevent方法,如果这个Viewgroup的onIntercept返回true就表示要对该事件进行拦截,那么就会调用Viewgroup的OntouchEvent去处理这个事件,若不拦截则把该事件传给子节点的View也不执行Viewgroup的OntouchEvent方法,该view也一样会调用disdispathtoucheventath方法,也会对该事件是否进行拦截进行处理,若不拦截就不执行OntouchEvent,就这样递归的传递,若最后一个view也不处理,那么改使事件回传给父容器。

当View设置ontouchListener的时候,就会回调一个ontouch方法,若返回true则ontouchevent就不执行,返回false这反之,这里可以看到ontouchListener的优先级大于ontouchevent, 我们再来看ontouchevent中,如果我们设置了OnClickListener,那么onclick才会被回掉,所以ontouchevent的优先级大于OnClickListener,总结一下优先级ontouchListener>ontouchevent>OnClickListener。

为了便于理解ontouchevent和OnClickListener的优先级我们来看看ontouchevent的源码
下面这一段代码是在ViewGroup的ACTION_UP事件里

switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                            ......
                            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();
                                }
                            }
                        }

performClick方法实现了onClick的回调

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

故可以看出ontouchevent的优先级OnClickListener,至于ontouchListener的优先级在后面的源码中会讲解。
我们先来总结一下分发事件机制

  1. 同一个时间序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事情,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。
  2. 正常情况下,一个事件序列只能被一个View拦截且消耗。
  3. 因为一旦一个元素拦截了某次事件,那么同一个时间序列内的所有时间都会直接交给它处理,因此同一个时间序列中的时间不能分别有两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。
  4. 某个View一旦决定拦截,那个这一个事件序列都只能由它来处理(如果时间序列能过传递给它的话),并且它的onInterceptTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不会再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。
  5. 某一个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其它事件都不会再交给他来处理,并且事件将重新交由它的夫元素去处理,即父元素的onTouchEvent会被调用。
  6. 如果View不消耗ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
  7. ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。
  8. View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
  9. View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。
  10. onClick会发生的提前是当前View是可点击的,并且它收到了down和up的事件。
  11. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后在有父元素分发给予View,通过requestDisallowInterecptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

1.2事件分发的源码解析

我们先来说说最顶端的分发,首先Activity-->Window-->View(具体实现后面几个章节会解释)
对顶级View的点击事件的分发大致解释一下
关于点击事件如何在View中进行分发,当点击事件达到顶层View(一般是一个ViewGroup)以后,会调用ViewGroup的dispatchTouchEvent方法,然后得逻辑是这样的:如果顶层ViewGroup拦截事件即onInterceptTouchEvent返回true,则事件由ViewGroup处理,这时如果ViewGroup的mOnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用。也就是说如果都提供了的话,onTouch会屏蔽掉onTouchEvent。在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用。也就是OnTouchListener,onTouchEvent,onClick三者优先级的关系。如果顶层ViewGroup不拦截事件,则事件会传递给它所在的点击事件链上的子View,这是子View的dispatchTouchEvent会被调用。如果子View需要处理该事件就进行拦截,则会调用onTouchEvent,如果不处理就将事件传给下一个View,以此循环。

接下来我们来看看viewgroup的diapatchTouchEvent的部分源码
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;
}

我们可以看到判断事件是否被拦截一共有两个判断就是**包裹的部分

第一个判断是有两个参数,第一个是ACTION_DOWN和mFirstTouchTarget,前者就是一个点击事件,后者就是当ViewGroup的子View去处理了该事件,那么该View对象就会赋值给mFirstTouchTarget。

第二个判断就是就是子View是否有FLAG_DISALLOW_INTERCEPT标记位,该标记位一般是子View通过requstDisallowInterceptTouchEvent去设置的,目的就是为了让ViewGroup无法拦截除了ACTION_DOWN以外的其他事件,为什么这样说呢?因为ACTION_DOWN事件会重置FLAG_DISALLOW_INTERCEPT,也就是说FLAG_DISALLOW_INTERCEPT标记位对ACTION_DOWN无效。下面代码说明为什么无效。

// 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();//会先清除,再让其赋予该标记位,且不拦截除了ACTION_DOWN的事件
}
private void resetTouchState() {
    clearTouchTargets();
    resetCancelNextUpFlag(this);
    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    mNestedScrollAxes = SCROLL_AXIS_NONE;
}

可以看出FLAG_DISALLOW_INTERCEPT的标记位了就让ViewGroup拦截住了
上述中我们可以总结两点
1.onInterceptTouchEvent方法并不是每次都会执行,如果想要提前处理好所有的点击事件,要选择dispatchTouchEvent,因为只有该方法每次都会被调用。
2.FLAG_DISALLOW_INTERCEPT标记位给我们提供了一个思路,在处理一些滑动冲突的时候,我们是不是可以利用这个标记位?


接下来看看ViewGroup不拦截的时候的分发给子View的处理
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);//获取点击事件的坐标
    final float y = ev.getY(actionIndex);
    // Find a child that can receive the event. 找到一个可以接收事件的孩子。
    // Scan children from front to back.扫描孩子从前面到后面
    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    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);

        // 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) {//判断这焦点是否是对应的view
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }
        if (!canViewReceivePointerEvents(child)//该方法就是判断点击事件的坐标是否落在View里面
                || !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;
        }

*包裹的部分的两个判断
当有子View的时候,决定时间是否分发到子View有两个要素,
第一个就是判断子View是否有焦点,因为有焦点的话是不能获取到点击事件,所以我们要保证子View能获取到焦点
第二就是判断点击事件的坐标是否落在子View上

其中dispatchTransformedTouchEvent方法就是调用了子View的dispatchTouchEvent,从而完成了吧点击事件分发到子View

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

我们再来看看这段代码

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

这段代码完成了mFirstTouchEvent的赋值,并且跳出该循环,前面也已经说了mFirstTouchEvent所指向的是处理事件的View,如果为空ViewGroup就是拦截 我们来看看源码

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

mFirstTouchEvent的赋值主要是通过addTouchTarget这个方法,那么我来看看addTouchTarget的源码

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

讲完了ViewGroup,我们来看看View对点击事件的处理,我们先来看看View的diapatchTouchEvent

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不是ViewGroup所以就不需要再分发事件了,我们可以看到View先要去判断是否设置了onTouchListener,若返回true这不去调用onTouchEvent,这里就不说优先级了之前说过。

我们再来看看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);
}

(enable表示View是否可用) 通过enable,我们可以看到View处于不可用状态,但是不影响事件是消耗和onTouchEvent的返回。

if (mTouchDelegate != null) {//TouchEdlegate类的作用是增大View的点击范围
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

下面看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) {
                // 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:
            ...
            break;

        case MotionEvent.ACTION_CANCEL:
         ...
            break;

        case MotionEvent.ACTION_MOVE:
             ...
            break;
    }

不管是CLICKABLE LONG_CLICKABLE CONTEXT_CLICKABLE 只要有一个为true 就可以消耗事件,在ACTION_UP事件里的performClick方法,在View设置了onclickListener的话会回掉该方法里的onClick方法

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

所以想要回掉onclick,必须要设置CLICKABLE LONG_CLICKABLE CONTEXT_CLICKABLE 中的其中一个为true就可以了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容