ViewGroup Touch 源码解析

1、现象分析

我们分别定义四个自定义view

// 如果是继承RelativeLayout则实现其三个方法,并输出log
// dispatchTouchEvent
// onInterceptTouchEvent
// onTouchEvent
public class MyRelaLayout1 extends RelativeLayout{}

public class MyRelaLayout2 extends RelativeLayout {}

// 如果是继承TextView则实现其两个方法,并输出log
// dispatchTouchEvent
// onTouchEvent
public class MyTextView1 extends TextView {}

public class MyTextView2 extends TextView {}

写一个相互嵌套的布局

    <com.djk.test.touch.MyRelaLayout1
        android:id="@+id/p_1"
        android:layout_width="200dp"
        android:layout_height="200dp">

        <com.djk.test.touch.MyRelaLayout2
            android:id="@+id/p_2"
            android:layout_width="200dp"
            android:layout_height="200dp">

            <com.djk.test.touch.MyTextView1
                android:id="@+id/c_1"
                android:layout_width="150dp"
                android:layout_height="150dp"
                android:background="#000000" />

            <com.djk.test.touch.MyTextView2
                android:id="@+id/c_2"
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:background="#ff0000" />

        </com.djk.test.touch.MyRelaLayout2>

    </com.djk.test.touch.MyRelaLayout1>

实现效果如下图,MyTextView1为黑色背景,MyTextView2为红色背景,并覆盖在MyTextView1之上

Paste_Image.png

下面我们来进行操作来分析touch事件的传递与拦截
现象一:就现在的默认情况下,我们点击红色区域

-> MyRelaLayout1.dispatchTouchEvent.Down -> MyRelaLayout1.onInterceptTouchEvent.Down
-> MyRelaLayout2.dispatchTouchEvent.Down -> MyRelaLayout2.onInterceptTouchEvent.Down
-> MyTextView2.dispatchTouchEvent.Down -> MyTextView2.onTouchEvent.Down
-> MyTextView1.dispatchTouchEvent.Down -> MyTextView1.onTouchEvent.Down
-> MyRelaLayout2.onTouchEvent.Down 
-> MyRelaLayout1.onTouchEvent.Down

我们得到touch事件从MyRelaLayout1-> MyRelaLayout2-> MyTextView2-> MyTextView1
可以分析得出touch传递是从最外层的MyRelaLayout1传到第二层MyRelaLayout2
在RelativeLayout布局中,MyTextView2 和MyTextView1属于平等关系,那么最上层子view先拿到touch事件,然后再传递到下面的子view

现象二:我们给MyTextView2设置onTouchEvent事件return true

-> MyRelaLayout1.dispatchTouchEvent.Down -> MyRelaLayout1.onInterceptTouchEvent.Down
-> MyRelaLayout2.dispatchTouchEvent.Down -> MyRelaLayout2.onInterceptTouchEvent.Down
-> MyTextView2.dispatchTouchEvent.Down -> MyTextView2.onTouchEvent.Down
// ... Move
-> MyRelaLayout1.dispatchTouchEvent.Up -> MyRelaLayout1.onInterceptTouchEvent.Up
-> MyRelaLayout2.dispatchTouchEvent.Up -> MyRelaLayout2.onInterceptTouchEvent.Up
-> MyTextView2.dispatchTouchEvent.Up -> MyTextView2.onTouchEvent.Up

可以看出我们成功的把MyTextView1的所有事件都截断了

现象三:我们给MyRelaLayout2设置onInterceptTouchEvent事件return true

-> MyRelaLayout1.dispatchTouchEvent.Down -> MyRelaLayout1.onInterceptTouchEvent.Down
-> MyRelaLayout2.dispatchTouchEvent.Down -> MyRelaLayout2.onInterceptTouchEvent.Down -> MyRelaLayout2.onTouchEvent.Down
-> MyRelaLayout1.dispatchTouchEvent.Up -> MyRelaLayout1.onInterceptTouchEvent.Up
-> MyRelaLayout2.dispatchTouchEvent.Up -> MyRelaLayout2.onTouchEvent.Up

可以看出,成功将后面所有的事件都截断了,并调用了自己的onTouchEvent方法

2、源码分析(android-25)

    public boolean dispatchTouchEvent(MotionEvent ev) {
        // ...
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 清除touch targets  其核心代码就是  mFirstTouchTarget = null;
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 是否要拦截
            final boolean intercepted;
            // 如果是Down事件,或 mFirstTouchTarget != null;
            if (actionMasked == MotionEvent.ACTION_DOWN 
                    || mFirstTouchTarget != null) {
                // 子类请求不要拦截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                // 子类没有请求不要拦截事件,走我们正常的流程
                if (!disallowIntercept) {
                    // 调用自己的onInterceptTouchEvent方法来判断是否要拦截touch事件 默认情况下返回 fase
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }

            // ...
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            // 没有取消 & 没有拦截事件 正常情况下if能够执行
            if (!canceled && !intercepted) {
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    // ...
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        // ...
                        // 获取其子view的集合
                        final View[] children = mChildren;
                        // 反序for循环,获取子view(这里就是RelativeLayout中多层覆盖的情况下,首先拿到最上层的子view)
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // ...

                            newTouchTarget = getTouchTarget(child);
                            // ...
                            // 详见下面的第二个代码块,如果dispatchTouchEvent方法返回true 则能进入这个方法
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // ...
                                // addTouchTarget() 方法核心代码 mFirstTouchTarget = target; 这里将 mFirstTouchTarget 赋值
                                // 然后将mFirstTouchTarget 的值再赋值给 newTouchTarget
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                }
            }

            // 这里如果mFirstTouchTarget = null,则去调用代码块2的方法,传入的child为null,这个时候就会调用自己的onTouchEvent方法
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            // 如果child 为null,则去调用父类的dispatchTouchEvent,ViewGroup的父类也是View,
            // 所以相当于调用了View的dispatchTouchEvent,进而会调用自己的onTouchEvent方法
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {// 如果child 不为null,则去调用子类的dispatchTouchEvent,子类的touch事件从这里开始
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        return handled;
    }

3、源码变通(android源码中,所有的touch事件都会调用dispatchTouchEvent方法,其实我们可以将它拆成Down事件和其他事件来分析)

第一步,当前是Down事件:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean handled = false;
        mFirstTouchTarget = null;
        boolean intercepted = false;
        // 调用自己的onInterceptTouchEvent方法来判断是否要拦截touch事件 默认情况下返回 false
        intercepted = onInterceptTouchEvent(ev);
        // 这里如果是不拦截touch事件
        if (!intercepted) {
            // 获取其子view的集合
            final View[] children = mChildren;
            // 反序for循环,获取子view(这里就是RelativeLayout中多层覆盖的情况下,首先拿到最上层的子view)
            for (int i = childrenCount - 1; i >= 0; i--) {
                final View child = getChildView(i);
                // 调用子类的dispatchTouchEvent,如果返回true则将 mFirstTouchTarget 赋值
                handled = child.dispatchTouchEvent(event);
                if (handled) {
                    // 这里将 mFirstTouchTarget 赋值
                    mFirstTouchTarget = target;
                    // 跳出循环,此后的view就收不到touch事件了
                    break;
                }
            }
        }
        if (mFirstTouchTarget == null) {
            // 如果是mFirstTouchTarget == null,则说明没有一个子类返回true,
            // 则直接调用View的dispatchTouchEvent,进而会调用自己的onTouchEvent方法
            handled = super.dispatchTouchEvent(event);
        } 
        return handled;
    }

第二步,当前是 Move or Up 事件:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        // ...
        boolean handled = false;

        // 是否要拦截
        final boolean intercepted;
        // 这里的 mFirstTouchTarget 是由Down事件记录的
        if (mFirstTouchTarget != null) {
            // 调用自己的onInterceptTouchEvent方法来判断是否要拦截touch事件 默认情况下返回 fase
            intercepted = onInterceptTouchEvent(ev);
        } else {// 如果Down事件记录的 mFirstTouchTarget == null; 则拦截判断直接置为 true
            intercepted = true;
        }
        // 这里如果是不拦截touch事件
        if (!intercepted) {
            // 获取其子view的集合
            final View[] children = mChildren;
            // 反序for循环,获取子view(这里就是RelativeLayout中多层覆盖的情况下,首先拿到最上层的子view)
            for (int i = childrenCount - 1; i >= 0; i--) {
                final View child = getChildView(i);
                // 调用子类的dispatchTouchEvent,如果返回true则将 mFirstTouchTarget 赋值
                handled = child.dispatchTouchEvent(event);
                if (handled) {
                    // 这里将 mFirstTouchTarget 赋值
                    mFirstTouchTarget = target;
                    // 跳出循环,后面的view就收不到touch事件
                    break;
                }
            }
        }
        if (mFirstTouchTarget == null) {
            // 如果是mFirstTouchTarget == null,则说明没有一个子类返回true,
            // 则直接调用View的dispatchTouchEvent,进而会调用自己的onTouchEvent方法
            handled = super.dispatchTouchEvent(event);
        }
        return handled;
    }
Paste_Image.png

4、下面来总结一下吧
①、ACTION_DOWN事件为一个事件序列的开始,中间有若干个ACTION_MOVE,最后以ACTION_UP结束。
②、一个clickable或者longClickable的View会永远消费Touch事件,不管他是enabled还是disabled的
③、Touch事件是从最顶层的View一直分发到手指touch的最里层的View,如果最里层View消费了ACTION_DOWN事件(设置onTouchListener,并且onTouch()返回true 或者onTouchEvent()方法返回true)才会触发ACTION_MOVE,ACTION_UP的发生,如果某个ViewGroup拦截了Touch事件,则Touch事件交给ViewGroup处理
④、如果某一个View拦截了事件,那么同一个事件序列的其他所有事件都会交由这个View处理,此时不再调用View(ViewGroup)的onIntercept()方法去询问是否要拦截了。

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

推荐阅读更多精彩内容