完全理解Android TouchEvent事件分发机制(二)

此文接于上一篇完全理解Android TouchEvent事件分发机制(一)

可以看出来,事件一旦被某一层消费掉,其它层就不会再消费了

到这里其实对事件分发的机制就有个大概了解,知道里面的原理是怎么回事。

下面就让我们来去梳理一下这个事件分发所走的逻辑。

我们仔细思考一下,为什么有的事件有UP有的没有?

为什么Up和Down的顺序不同呢?

为什么要按照这个顺序执行呢?

这个例子主要是为了说明分发、拦截、消费的流程

以例一为例,在每个 View 都不拦截 down 事件的情况下,down 事件是这样传递的

super.dispatchTouchEvent方法,上面我们介绍过,

这个方法内部实际上是调用的onTouchEvent方法

所以最后的输出日志顺序就是从父到子依次调用分发和拦截,然后从子到父依次调用消费。

例二也是同理,区别在于

当Father拿到事件的时候,选择了拦截下来不再询问其他,

但是Father也没消费,直接又还回给了Grandpa,

Grandpa同样也没有消费这个事件。

所以最终的顺序就是,从Grandpa到Father再返回Grandpa就结束了,没有经过LogImageView。

例三的情况就不太一样了

  • 当Grandpa->Father->LogImageView 传递到LogImageView时,LogImageView不消费又返回给了Father,Father在onTouchEvent消费掉了事件。
  • 然后反馈给Father说事件已经消费。,就等于parent.dispatchTouchEvent返回true给上一级的Grandpa,
    Grandpa不会再调用grandpa.onTouchEvent方法。

从这里我们可以总结出来:

dispatchTouchEvent返回值的作用是用于标志这个事件是否“消费了”,
无论是自己或者下面的子一级用掉了都算是消费掉。

再如这个例子中,如果我们让LogImageView消费掉事件,

那么Father收到LogImageView的消息后,也会调用parent.dispatchTouchEvent返回true给Grandpa,

所以这个方法返回值的true是只要用掉就行,无论自己还是下面某一级,

而非我把事件传递下去就是true了,下面没人消费最终其实还是返回false的。

至此,我们来总结一下这三个方法的大致作用:

  • dispatchTouchEvent方法内容里处理的是分发过程。可以理解为从Grandpa->Father->LogImageView一层层分发的动作
  • dispatchTouchEvent的返回值则代表是否将事件分发出去用掉了,自己用或者给某一层子级用都算分发成功。比如Father消费了事件,或者他发出去给的LogImageView消费了事件,这两种情况下B的dispatchTouchEvent都会返回true给Grandpa

  • onInterceptTouchEvent会在第一轮从父到子的时候在分发时调用,以它去决定是否拦截掉此事件不再向下分发。如果拦截下来,就会调用自己的onTouchEvent处理;如果不拦截,则继续向下传递

  • onTouchEvent代表消费掉事件。方法内容是具体的事件处理方法,如何处理点击滑动等。

  • onTouchEvent的返回值则代表对上级的反馈,通知这个东西我用掉啦,然后他的父级就会让分发方法也返回true

下面我们来解释为什么例一、二中没有Up,而例三中有

一个Action_DOWN,一个ACTION_UP,若干个ACTION_MOVE,才构成完整的事件。

前俩例子里为什么没有Up呢,很好理解,

因为他们都没有消费事件啊,所以他们只有DOWN事件,因此只有Down,没Up。

例三做类比,Father消费掉了这个事件

  • 流程依然是先从Grandpa开始分配
    (grandpa.dispatchTouchEvent)
    (grandpa.onInterceptTouchEvent 判断是否拦截)

  • Grandpa还是没拦下来,继续分发事件(grandpa不拦截,然后调用child.dispatchTouchEvent)

  • 事件到了Father,Father进行了消费。(parent没有调用拦截方法)

  • Father调用onTouchEvent返回true消费掉事件,完成了整个行为。

【例四】
在Grandpa类的onInterceptTouchEvent中添加个判断,
如果动作是UP就return true拦截掉,DOWN则不拦截和之前一样

打印如下:

04-04 07:16:43.353 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  dispatchTouchEvent  Event 0
04-04 07:16:43.355 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onInterceptTouchEvent  Event 0
04-04 07:16:43.355 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  dispatchTouchEvent  Event 0
04-04 07:16:43.356 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onInterceptTouchEvent  Event 0
04-04 07:16:43.356 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: LogImageView  dispatchTouchEvent  Event 0
04-04 07:16:43.357 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: LogImageView  onTouchEvent  Event 0
04-04 07:16:43.357 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onTouchEvent  Event 0
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  dispatchTouchEvent  Event 1
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onInterceptTouchEvent  Event 1
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  dispatchTouchEvent  Event 3
04-04 07:16:43.392 2344-2344/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onTouchEvent  Event 3

前面Down行为和例三一样,后面就不同了
UP流程变了,然后多了个CANCEL的动作
这里我们可以理解为

  • GrandPa调用dispatchTouchEvent分发UP事件

  • GrandPa调用onInterceptTouchEvent返回true,拦截UP,DOWN事件则是正常的传递

  • FatherView调用dispatchTouchEvent分发CANCEL动作

  • FatherView使用CANCEL动作调用onTouchEvent方法,结束

大概也就了解的差不多了,还剩一个TouchTarget目标的概念,
为什么例三中Up和Down流程不同?

回头去看完整源码:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // 1.每次起始动作就重置之前的TouchTarget等参数
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            // 2.如果是起始动作才拦截,或者已经有人消费掉了事件,再去判断拦截
            // 起始动作是第一次向下分发的时候,每个view都可以决定是否拦截,然后进一步判断是否消费,很好理解
            // 如果有人消费掉了事件,那么也拦截~ 就像例四中的情况,也可以再次判断是否拦截的
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                // 3.这里可以设置一个disallowIntercept标志,如果是true,就是谁收到事件后都不准拦截!!!
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action);
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
            // 4.如果未拦截,只有Down动作才去子一级去找目标对象~
            // 因为找目标这个操作只有Down中才会处理
            if (actionMasked == MotionEvent.ACTION_DOWN ) {
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        newTouchTarget = getTouchTarget(child);
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }
            }
        }

        if (mFirstTouchTarget == null) {
            // 5.把自己当做目标,去判断自己的onTouchEvent是否消费
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // 6.如果有人消费掉了事件,找出他~
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                // 7.消费对象信息其实是一个链式对象,记载着一个一个传递的人的信息,遍历调用它child的分发方法
                final TouchTarget next = target.next;
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                target = next;
            }
        }
    }

    return handled;
}

dispatchTransformedTouchEvent方法,内部简化代码为:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    return handled;
}

就是判断如果没child了(是ViewGroup但是没子控件,或者自己就是View),
如果没child,就调用View的dispatchTouchEvent方法,
实质就是调用onTouchEvent判断是否消费掉事件
如果有child,就调用child的dispatchTouchEvent将事件一层层向下分发

例三四中的复杂情况
其中关键主要在于多了一个TouchTarget的处理.

向下传递的核心主要是在于dispatchTransformedTouchEvent方法

第一轮动作的Down时,只要不拦截,就会在注释4代码处遍历所有child调用该方法层层传递下去

而后续其他动作时,就会进入注释6代码条件,然后遍历TouchTarget中的信息用该方法层层分发

注意不要误解:

第一次Down的时候会for循环所有child

第二轮Up的时候也会while(target.next)的迭代循环挨个判断,但是next是遍历同级,不是子级

dispatchTrancformTouchEvent(target.child)这里的.child才是向子一级一层一层分发传递的地方.

这个TouchTarget对象,主要保存的是传递路线信息,它是一个链式结构

不过这个路线不是Grandpa->Father->LogImageView的一个单子,而是每个ViewGroup都会保存一个向下的路线信息.

Cancel部分

dispatchTrancformTouchEvent中会判断,如果cancel=true动作,
则会把动作改成ACTION_CANCEL一层一层的传下去.

注意:

onTouch和onTouchEvent有什么区别,又该如何使用?

从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。

另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

到这里,我们的事件传递就全部讲解完成了,下面让我们看看他的实际用途吧。

用途:

滑动事件冲突的解决方法:

外部拦截法

外部拦截法是指在有点击事件时都要经过父容器,那么在父容器时如果需要拦截就拦截自己处理,不需要则传递给下一层进行处理,

下面看个例子

首先定义一个水平滑动的HorizontalScrollView,看主要代码

主要的拦截是需要重写onInterceptTouchEvent

此示例是借鉴于lylodlig的一篇文章,在此感谢。

@Override  
   public boolean onInterceptTouchEvent(MotionEvent ev) {  
       boolean intercepted = false;  
       int x = (int) ev.getX();  
       int y = (int) ev.getY();  
       switch (ev.getAction()) {  
           case MotionEvent.ACTION_DOWN:  
               //down事件不拦截,否则无法传给子元素  
               intercepted = false;  
               if (!mScroller.isFinished()) {  
                   mScroller.abortAnimation();  
                   intercepted = true;  
               }  
               break;  
           case MotionEvent.ACTION_MOVE:  
               int deltaX = x - mLastXIntercept;  
               int deltaY = y - mLastYIntercept;  
               //水平滑动则拦截  
               if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {  
                   intercepted = true;  
               } else {  
                   intercepted = false;  
               }  
               break;  
           case MotionEvent.ACTION_UP:  
               //不拦截,否则子元素无法收到  
               intercepted = false;  
               break;  
       }  
       //因为当ViewGroup中的子View可能消耗了down事件,在onTouchEvent无法获取,  
       // 无法对mLastX赋初值,所以在这里赋值一次  
       mLastX = x;  
       mLastY = y;  
       mLastYIntercept = y;  
       mLastXIntercept = x;  
       return intercepted;  
   }  

在down事件不需要拦截,返回false,否则的话子view无法收到事件,将全部会由父容器处理,这不是希望的;up事件也要返回false,否则最后子view收不到

看看move事件,当水平滑动距离大于竖直距离时,代表水平滑动,返回true,由父类来进行处理,否则交由子view处理。这里move事件就是主要的拦截条件判断,如果你遇到的不是水平和竖直的条件这么简单,就可以在这里进行改变,比如,ScrollView嵌套了ListView,条件就变成,当ListView滑动到底部或顶部时,返回true,交由父类滑动处理,否则自身ListView滑动。

在onTouchEvent中主要是做的滑动切换的处理

@Override  
    public boolean onTouchEvent(MotionEvent event) {  
        mVelocityTracker.addMovement(event);  
        int x = (int) event.getX();  
        int y = (int) event.getY();  
        switch (event.getAction()) {  
            case MotionEvent.ACTION_DOWN:  
                if (!mScroller.isFinished()) {  
                    mScroller.abortAnimation();  
                }  
                break;  
            case MotionEvent.ACTION_MOVE:  
                int deltaX = x - mLastX;  
                int deltaY = y - mLastY;  
                if (getScrollX() < 0) {  
                    scrollTo(0, 0);  
                }  
                scrollBy(-deltaX, 0);  
                break;  
            case MotionEvent.ACTION_UP:  
                int scrollX = getScrollX();  
                mVelocityTracker.computeCurrentVelocity(1000);  
                float xVelocityTracker = mVelocityTracker.getXVelocity();  
                if (Math.abs(xVelocityTracker) > 50) {//速度大于50则滑动到下一个  
                    mChildIndex = xVelocityTracker > 0 ? mChildIndex - 1 : mChildIndex + 1;  
                } else {  
                    mChildIndex = (scrollX + mChildWith / 2) / mChildWith;  
                }  
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));  
                int dx = mChildIndex * mChildWith - scrollX;  
                smoothScrollBy(dx, 0);  
                mVelocityTracker.clear();  
                break;  
        }  
        mLastY = y;  
        mLastX = x;  
        return true;  
    }  

在这个嵌套一个普通的ListView,这样就可以解决水平和竖直滑动冲突的问题了。

<com.shanlovana.rcimageview.HorizontalScrollViewEx  
        android:layout_width="match_parent"  
        android:layout_height="200dp">  
  
        <ListView  
            android:id="@+id/listView"  
            android:layout_width="match_parent"  
            android:layout_height="match_parent" />  
  
        <Button  
            android:layout_width="match_parent"  
            android:layout_height="match_parent"  
            android:background="@android:color/holo_blue_bright"  
            android:text="2" />  
  
        <Button  
            android:layout_width="match_parent"  
            android:layout_height="match_parent"  
            android:background="@android:color/holo_green_dark"  
            android:text="3" />  
    </com.shanlovana.rcimageview.HorizontalScrollView>  

内部拦截法

内部拦截法是父容器不拦截任何事件,所有事件都传递给子view,如果需要就直接消耗掉,不需要再传给父容器处理
下面重写一个ListView,只需要重写一个dispatchTouchEvent方法就OK

    @Override  
    public boolean dispatchTouchEvent(MotionEvent ev) {  
        int x = (int) ev.getX();  
        int y = (int) ev.getY();  
  
        switch (ev.getAction()) {  
            case MotionEvent.ACTION_DOWN:  
                //子View的所有父ViewGroup都会跳过onInterceptTouchEvent的回调  
                getParent().requestDisallowInterceptTouchEvent(true);  
                break;  
            case MotionEvent.ACTION_MOVE:  
                int deltaX = x - mLastX;  
                int deltaY = y - mLastY;  
                if (Math.abs(deltaX) > Math.abs(deltaY) + 5) {//水平滑动,使得父类可以执行onInterceptTouchEvent  
                    getParent().requestDisallowInterceptTouchEvent(false);  
                }  
                break;  
        }  
        mLastX = x;  
        mLastY = y;  
        return super.dispatchTouchEvent(ev);  
    }  

在down事件调用getParent().requestDisallowInterceptTouchEvent(true),这句代码的意思是使这个view的父容器都会跳过onInterceptTouchEvent,在move中判断如果是水平滑动就由父容器去处理,父容器只需要把之前的onInterceptTouchEvent改为下面那样,其他不变.

@Override  
    public boolean onInterceptTouchEvent(MotionEvent ev) {  
        int x = (int) ev.getX();  
        int y = (int) ev.getY();  
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {  
            mLastX = x;  
            mLastY = y;  
            if (!mScroller.isFinished()) {  
                mScroller.abortAnimation();  
                return true;  
            }  
            return false;  
        } else {  
            //如果是非down事件,说明子View并没有拦截父类的onInterceptTouchEvent  
            //说明该事件交由父类处理,所以不需要再传递给子类,返回true  
            return true;  
        }  
    }  

解决这些问题的基本的思想就是这些,希望可以帮助到您。

Github源码下载地址

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

推荐阅读更多精彩内容