View的事件分发机制

参考资料:
1.《Android开发艺术探索》

  1. http://www.cnblogs.com/sunzn/archive/2013/05/10/3064129.html

总是记不住,写的文字,记录下,方便查阅;

点击事件的传递规则##

MotionEvent就是点击事件,当一个MotionEvent产生了以后,系统需要把她传递给一个具体的View;
这个过程就是事件的分发;
有3个非常重要的方法:
dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent;

dispatchTouchEvent
用来进行事件的分发,如果事件能够传递给当前View,此方法一定被调用;返回结果表示是否消耗此事件,返回结果受当前View的onTouchEvent和下级View的onInterceptTouchEvent影响;

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

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

三个方法之间的关系

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

        return consume;
    }

点击事件的传递规则:
对于根ViewGroup来说,点击事件产生后,ViewGroup的dispatchTouchEvent将会调用,如果此ViewGroup的onInterceptTouchEvent返回true,表示要拦截当前事件,这样事件交给ViewGroup来处理,即ViewGroup的onTouchEvent调用;如果返回false,则不拦截事件,事件就会继续传递给他的子元素,然后直接上面的步骤;

当一个点击事件发生时,她的传递过程顺序是:Activity->Window->View, 顶级View收到事件后,就会按照事件的传递机制来进行事件分发,如果View的onTouchEvent返回false,那么父容器的onTouchEvent将会被调用,onTouchEvent返回false,事件就会往上一层回传;类似于Java中的异常处理机制,当前类抛出,调用类就必须catch,如果不catch就得继续往上抛,都不处理的话,那么Java的虚拟机来处理了;

事件机制的一些结论

  1. 同一个事件序列是指从手指接触到屏幕起,直到手指离开屏幕这个过程中的所产生的一系列事件,事件序列以down开始,中间含有move,最终以up结束;
  2. 正常情况下,一个事件序列只能被一个View拦截且消耗,一旦一个元素拦截了某此事件,那么同一个事件序列内所有事件将会直接交给他;因此同一个事件序列中的事件不能分别由2个View来处理,但可以通过View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理;
  3. 某个View一旦决定拦截,那么这一个事件序列都只能由她来处理(前提是:事件序列能够给传递给她),并且她的onInterceptTouchEvent不会再被调用(onInterceptTouchEvent 询问当前view是否拦截事件);
  4. 某个View一旦开始处理事件,如果不销毁ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列中的其他事件她也收不到了;并且其事件重新交由其父来处理(调用父的onTouchEvent);
  5. 如果View不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件将消失(在down时,已经找到了targetView,后继事件都会传给该View),此时父View的onTouchEvent不会调用,当前View还可以收到其他后续事件,最终这些消失的点击交由Activity处理 (这是不完整的事件序列);
  6. ViewGroup默认不拦截任何事件;
  7. View没有onInterceptTouchEvent方法,一旦有点击事件传递给她,她的onTouchEvent就被调用;
  8. View的onTouchEvent默认不会消耗事件(返回true),除非他是不可点击的(clickable和longclickable同时为false)。如:Button,TextView;
  9. View的enable属性不影响onTouchEvent的默认返回值,只有他的clickable或者longclickable有一个为true时,ouTouchEvent就返回true;
  10. onClick会发生的前提是当前View是可点击的,并且收到了 down和up事件;
  11. 事件的传递总是由外向内的,事件总是给父,然后再由父去分发给子View,通过requestDisallowInterceptTouchEvent方法,可以让子View中干预父元素的事件分发过程,但 DOWN除外;

事件分发解析##

Activity对点击事件的分发过程####

当一个点击事件发生时,事情会先传递到Activity的dispatchTouchEvent方法中,通过此方法进行事件派发;

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

事件会转给getWindow(), getWindow返回的是PhoneWindow,实际上调用的DecorView的superDispatchTouchEvent方法,DecorView是继承自FrameLayout的,所以事件就回到了View上,从源码上可以发现,如果View都不处理事件,Activity onTouchEvent将会执行;

ViewGroup对点击事件的处理####

顶级View一般是一个ViewGroup,事件到达后,会调用dispatchTouchEvent方法,Activity事件也是到达这里,这里如事件拦截(onInterceptTouchEvent返回true),则由ViewGroup处理,如果ViewGroup mTouchListener 有设置,Listener onTouch将调用,并屏蔽onTouchEvent,否则 onTouchEvent 将执行;如果顶级ViewGroup不拦截事件,则事件往下传给事件链上的子View,此时,子View的dispatchTouchEvent将调用,继续重复上面的步骤,如此进行下去,完成事件分发;

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

注意是 事件是 down;
当mFirstTouchTarget 为找到接受事件的子View,如不拦截事件,当后续事件如:MOVE、UP来到时,事件则交给了 子 View 来处理;反之,则交给ViewGroup进行处理;

当ViewGroup不拦截事件时,则事件向下分发给它的子View:

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);      // x,y 坐标
    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) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }

        // 是否能接收事件(可见,并没有在播放动画),x、y 是否在点击区域
        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);
        // 实际调用的 是子元素的 dispatchTouchEvent
        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;
        }

        // The accessibility focus didn't handle the event, so clear
        // the flag and do a normal dispatch to all children.
        ev.setTargetAccessibilityFocus(false);
    }
    if (preorderedList != null) preorderedList.clear();
}

View对点击事件的处理####

View为一个具体的控件,没有子元素,所以事件无法向下传递,只能自己处理,首先判断,有没有 设置过 OnTouchListener,如则执行 Listener中的onTouch,并根据返回值,来判断是否要执行onTouchEvent;

View 的 dispatchTouchEvent关键代码:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        stopNestedScroll();
    }

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

        // 执行onTouchEvent
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }
    return result;
}

View onTouchEvent 部分代码
只要view的CLICKABLE和LONG_CLICKABLE有一个为true,则该View为消耗事件,onTouchEvent将返回true,click无视View 的disable
状态;UP 时,将触发performClick方法,

public boolean onTouchEvent(MotionEvent event) {
        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;  
        }
  ....
        return true;
    }

    return false;
}


总结##

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

推荐阅读更多精彩内容