Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

来源:互联网 发布:淘宝买一加5靠谱吗 编辑:程序博客网 时间:2024/06/05 02:38

Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

本文转自于郭霖的博客,原博客地址请看这里


记得在前面的文章中,我带大家一起从源码的角度分析了Android中View的事件分发机制,相信阅读过的朋友对View的事件分发已经有比较深刻的理解了。还未阅读过的朋友,请先参考 Android事件分发机制完全解析,带你从源码的角度彻底理解(上) 。

那么今天我们将继续上次未完成的话题,从源码的角度分析ViewGroup的事件分发。
首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别?
顾名思义,ViewGroup就是一组View的集合,它包含很多的子View和子VewGroup,是Android中所有布局的父类或间接父类,像LinearLayout、RelativeLayout等都是继承自ViewGroup的。但ViewGroup实际上也是一个View,只不过比起View,它多了可以包含子View和定义布局参数的功能。ViewGroup继承结构示意图如下所示:

这里写图片描述

可以看到,我们平时项目里经常用到的各种布局,全都属于ViewGroup的子类。

简单介绍完了ViewGroup,我们现在通过一个Demo来演示一下Android中VewGroup的事件分发流程吧。
首先我们来自定义一个布局,命名为MyLayout,继承自LinearLayout,如下所示:

public class MyLayout extends LinearLayout {    public MyLayout(Context context, AttributeSet attrs) {        super(context, attrs);    }}

然后,打开主布局文件activity_main.xml,在其中加入我们自定义的布局:

<com.example.viewgrouptouchevent.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:id="@+id/my_layout"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical" >    <Button        android:id="@+id/button1"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="Button1" />    <Button        android:id="@+id/button2"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="Button2" /></com.example.viewgrouptouchevent.MyLayout>

可以看到,我们在MyLayout中添加了两个按钮,接着在MainActivity中为这两个按钮和MyLayout都注册了监听事件:

myLayout.setOnTouchListener(new OnTouchListener() {    @Override    public boolean onTouch(View v, MotionEvent event) {        Log.d("TAG", "myLayout on touch");        return false;    }});button1.setOnClickListener(new OnClickListener() {    @Override    public void onClick(View v) {        Log.d("TAG", "You clicked button1");    }});button2.setOnClickListener(new OnClickListener() {    @Override    public void onClick(View v) {        Log.d("TAG", "You clicked button2");    }});

我们在MyLayout的onTouch方法,和Button1、Button2的onClick方法中都打印了一句话。现在运行一下项目,效果图如下所示:

这里写图片描述

分别点击一下Button1、Button2和空白区域,打印结果如下所示:

这里写图片描述

你可以先理解成Button的onClick方法将事件消费掉了,因此事件不会再继续向下传递。
那就说明Android中的touch事件是先传递到View,再传递到ViewGroup的?现在下结论还未免过早了,让我们再来做一个实验。
查阅文档可以看到,ViewGroup中有一个onInterceptTouchEvent方法,我们来看一下这个方法的源码:

/** * Implement this method to intercept all touch screen motion events.  This * allows you to watch events as they are dispatched to your children, and * take ownership of the current gesture at any point. * * <p>Using this function takes some care, as it has a fairly complicated * interaction with {@link View#onTouchEvent(MotionEvent) * View.onTouchEvent(MotionEvent)}, and using it requires implementing * that method as well as this one in the correct way.  Events will be * received in the following order: * * <ol> * <li> You will receive the down event here. * <li> The down event will be handled either by a child of this view * group, or given to your own onTouchEvent() method to handle; this means * you should implement onTouchEvent() to return true, so you will * continue to see the rest of the gesture (instead of looking for * a parent view to handle it).  Also, by returning true from * onTouchEvent(), you will not receive any following * events in onInterceptTouchEvent() and all touch processing must * happen in onTouchEvent() like normal. * <li> For as long as you return false from this function, each following * event (up to and including the final up) will be delivered first here * and then to the target's onTouchEvent(). * <li> If you return true from here, you will not receive any * following events: the target view will receive the same event but * with the action {@link MotionEvent#ACTION_CANCEL}, and all further * events will be delivered to your onTouchEvent() method and no longer * appear here. * </ol> * * @param ev The motion event being dispatched down the hierarchy. * @return Return true to steal motion events from the children and have * them dispatched to this ViewGroup through onTouchEvent(). * The current target will receive an ACTION_CANCEL event, and no further * messages will be delivered here. */public boolean onInterceptTouchEvent(MotionEvent ev) {    return false;}

如果不看源码你还真可能被这注释吓到了,这么长的英文注释看得头都大了。可是源码竟然如此简单!只有一行代码,返回了一个false!
好吧,既然是布尔型的返回,那么只有两种可能,我们在MyLayout中重写这个方法,然后返回一个true试试,代码如下所示:

public class MyLayout extends LinearLayout {    public MyLayout(Context context, AttributeSet attrs) {        super(context, attrs);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        return true;    }}

现在再次运行项目,然后分别Button1、Button2和空白区域,打印结果如下所示:

这里写图片描述

你会发现,不管你点击哪里,永远都只会触发MyLayout的touch事件了,按钮的点击事件完全被屏蔽掉了!这是为什么呢?如果Android中的touch事件是先传递到View,再传递到ViewGroup的,那么MyLayout又怎么可能屏蔽掉Button的点击事件呢?

看来只有通过阅读源码,搞清楚Android中ViewGroup的事件分发机制,才能解决我们心中的疑惑了,不过这里我想先跟你透露一句,Android中touch事件的传递,绝对是先传递到ViewGroup,再传递到View的。记得在Android事件分发机制完全解析,带你从源码的角度彻底理解(上) 中我有说明过,只要你触摸了任何控件,就一定会调用该控件的dispatchTouchEvent方法。这个说法没错,只不过还不完整而已。实际情况是,当你点击了某个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。如果我们点击了MyLayout中的按钮,会先去调用MyLayout的dispatchTouchEvent方法,可是你会发现MyLayout中并没有这个方法。那就再到它的父类LinearLayout中找一找,发现也没有这个方法。那只好继续再找LinearLayout的父类ViewGroup,你终于在ViewGroup中看到了这个方法,按钮的dispatchTouchEvent方法就是在这里调用的。修改后的示意图如下所示:

这里写图片描述

那还等什么?快去看一看ViewGroup中的dispatchTouchEvent方法的源码吧!代码如下所示:

public boolean dispatchTouchEvent(MotionEvent ev) {    if (mInputEventConsistencyVerifier != null) {        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);    }    // If the event targets the accessibility focused view and this is it, start    // normal event dispatch. Maybe a descendant is what will handle the click.    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {        ev.setTargetAccessibilityFocus(false);    }    boolean handled = false;    if (onFilterTouchEventForSecurity(ev)) {        final int action = ev.getAction();        final int actionMasked = action & MotionEvent.ACTION_MASK;        // 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();        }        // Check for interception.        final boolean intercepted;        if (actionMasked == MotionEvent.ACTION_DOWN                || mFirstTouchTarget != null) {            // 判断子View或子ViewGroup是否请求不要拦截事件(默认是false,可以在子view调用requestDisallowInterceptTouchEvent对这个值修改)            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;            if (!disallowIntercept) {                // ViewGroup可以通过重写onInterceptTouchEvent进行拦截事件                intercepted = onInterceptTouchEvent(ev);                ev.setAction(action); // restore action in case it was changed            } else {                // 子View或子ViewGroup请求不要拦截事件                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;        }        // If intercepted, start normal event dispatch. Also if there is already        // a view that is handling the gesture, do normal event dispatch.        if (intercepted || mFirstTouchTarget != null) {            ev.setTargetAccessibilityFocus(false);        }        // Check for cancelation.        final boolean canceled = resetCancelNextUpFlag(this)                || actionMasked == MotionEvent.ACTION_CANCEL;        // Update list of touch targets for pointer down, if needed.        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;        TouchTarget newTouchTarget = null;        boolean alreadyDispatchedToNewTouchTarget = false;        // 如果不拦截事件,则找到被点击的相应控件然后调用该控件的dispatchTouchEvent方法        if (!canceled && !intercepted) {            // If the event is targeting accessiiblity focus we give it to the            // view that has accessibility focus and if it does not handle it            // we clear the flag and dispatch the event to all children as usual.            // We are looking up the accessibility focused host to avoid keeping            // state since these events are very rare.            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()                    ? findChildWithAccessibilityFocus() : null;            // ACTION_DOWN才遍历查找            if (actionMasked == MotionEvent.ACTION_DOWN                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {                final int actionIndex = ev.getActionIndex(); // always 0 for down                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)                        : TouchTarget.ALL_POINTER_IDS;                // Clean up earlier touch targets for this pointer id in case they                // have become out of sync.                removePointersFromTouchTargets(idBitsToAssign);                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 = buildOrderedChildList();                    final boolean customOrder = preorderedList == null                            && isChildrenDrawingOrderEnabled();                    final View[] children = mChildren;                    //遍历所有children查找被点击的控件                    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);                        // 调用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();                            // 如果返回true,添加child到target                            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();                }                if (newTouchTarget == null && mFirstTouchTarget != null) {                    // Did not find a child to receive the event.                    // Assign the pointer to the least recently added target.                    newTouchTarget = mFirstTouchTarget;                    while (newTouchTarget.next != null) {                        newTouchTarget = newTouchTarget.next;                    }                    newTouchTarget.pointerIdBits |= idBitsToAssign;                }            }        }        // 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);        } 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;            // 给所有的target分发事件            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;}

这部分的代码基于API23,和原博客贴的代码不同。有需要的请参考原博客
这个方法代码比较长,我们只挑重点看。在前面我们已经知道在ViewGroup的dispatchTouchEvent方法中找到被点击的相应控件然后调用该控件的dispatchTouchEvent方法,既然要找,那肯定是用循环啦!果然~在第94行出现了for循环,而且循环条件也正是children,看来是这里面去调用子控件的dispatchTouchEvent方法了,再仔细往下看到第128行dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign),把找到的child传进去并调用其dispatchTouchEvent。
现在知道了怎么去调用子View的dispatchTouchEvent方法,那ViewGroup又是怎么拦截的呢?这里我们需要从for循环往前看,在第61行有个if(!canceled && !intercepted)判断是否可以进到for循环,由字面意思可以知道拦截事件是有intercepted的值控制的,继续往前看intercepted赋值。在第31行disallowIntercept表示子View或子ViewGroup是否请求不要拦截事件,如果子View或子ViewGroup请求不要拦截事件即disallowIntercept=true则直接跳到第38行intercepted=false,否则将执行第34行intercepted=onInterceptTouchEvent(ev)。disallowIntercept的值默认是false的,可以通过在子view调用requestDisallowInterceptTouchEvent对这个值修改。也就是说默认情况下ViewGroup是否拦截子View的事件,是由onInterceptTouchEvent的返回值决定的,也就是和我们一开始的结果是一样的。
那第一个demo的打印结果:如果按钮的点击事件得到执行,就会把MyLayout的touch事件拦截掉。这个又是怎么实现的呢?
我们已经知道,如果一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true,因此第128行的条件判断成立,于是在第145行调用addTouchTarget()给newTouchTarget赋值以及在第147行设置alreadyDispatchedToNewTouchTarget = true

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

把mFirtsTouchTarget,赋值给newTouchTarget.next,然后把mFirstTouchTarget指向newTouchTarget。

最后在第184行if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget)进行条件判断,显然是条件是成立的,handled = true,事件已经被消费了。

那如果子view是ImageView,其dispatchTouchEvent是返回false的。那ViewGroup又是怎么去处理事件,怎样回调设置的onTouch方法呢?
这里分为两种情况:
1. ViewGroup是根布局,此时 mFirstTouchTarget为空,执行第174行,child参数为null
2. ViewGroup是其他ViewGroup的子布局,此时 mFirstTouchTarget不为空,执行189行,child为自己
最后都是执行View中的dispatchTouchEvent方法,也就是将ViewGroup座位普通view一样去处理了。

在上一篇中我们提到,如果一个View的dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。看一下这个是怎么实现的?
我们可以看到在for循环外面第72行还有个条件判断,限定MotionEvent的action类型。像MotionEvent.ACTION_UP不会进到里面去,而是直接执行第182行,递归遍历处理保存的target的dispatchTransformedTouchEvent。因为前一个action返回false,child没有添加到targe,也就不会再执行其dispatchTouchEvent了。

再看一下整个ViewGroup事件分发过程的流程图吧,相信可以帮助大家更好地去理解:

Created with Raphaël 2.1.0开始点击按钮ViewGroup的dispatchTouchEvent()方法是否拦截 = !requestDisallowInterceptTouchEvent() && onInterceptTouchEvent()是否拦截?拦截掉子View的事件ViewGroup的父类View的dispatchTouchEvent方法View的dispatchTouchEvent方法回调View注册的onTouch方法返回true?事件被消费了结束View的onTouchEvent方法是否可点击?处理事件ACTION_UP?回调View注册的onClick事件子View的dispatchTouchEvent方法yesnoyesnoyesnoyesno

整个Android点击事件分发完全解析到这里介绍了,最后总结一下:

1. 事件分发流程:根布局(ViewGroup)->子ViewGroup->子View
2. 子ViewGroup/子View可以调用getParent().requestDisallowInterceptTouchEvent(true)请求不要拦截事件,优先级大
3. ViewGroup可以重写onInterceptTouchEvent()方法返回true拦截事件,优先级小
4. 子View消费了事件就直接结束了,ViewGroup就无法再处理事件
5. 只有onTouch返回false才继续执行onTouchEvent,onClick在onTouchEvent里回调
6. 当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action(这个可以理解为既然你连前一个action都不处理,那后面的action也必要发给你了)
7. Button控件默认是可点击的,其onTouchEvent返回true****7. ImageView控件默认是不可点击的,其onTouchEvent返回false,设置setOnClickListener监听或android:clickable=”true”也会设置控件可点击使其返回true
8. 只有ACTION_DOWN才去查找被点击的控件并保存target,其他ACTION递归遍历保存的target,处理ACTION_DOWN事件的view会继续处理其他ACTION

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 孩子不写作业怎么办啊 对孩子在校被欺怎么办 二年级的孩子不爱学习怎么办 孩子不爱学习怎么办二年级 二年级孩子不爱写作业怎么办 孩子喜欢的朋友家长不喜欢怎么办? 孩子不喜欢上幼儿园家长怎么办 孩子不喜欢家长学佛怎么办呢? 学生家长讨厌老师老师该怎么办 孩子不爱去幼儿园总是哭怎么办 孩子学习一点都不主动怎么办 孩子怕老师不想上学怎么办 幼儿园孩子怕老师不想上学怎么办 孩子在幼儿园怕老师怎么办 幼儿园老师对孩子有歧视怎么办 儿子在幼儿园受到老师歧视怎么办 我把老师骂了怎么办 孩子讨厌老师骂老师怎么办 幼儿园老师讨厌一个孩子怎么办 幼儿园老师对孩子不好怎么办 高中分班讨厌班主任怎么办 把孩子老师惹了怎么办 高三班主任第一节课应该怎么办 高三孩子与班主任不和怎么办 孩子很害怕一件事怎么办 吼完孩子觉得很害怕怎么办 小朋友在幼儿园被欺负怎么办 老师总找孩子茬怎么办 小孩说幼儿园老师不喜欢她怎么办 小孩在学校老师不喜欢怎么办 好哭的孩子老师怎么办 小孩哭的犟住怎么办 2-3小孩脾气很犟怎么办 生了孩子没奶怎么办 二年级孩子写作业慢怎么办 六年级下册数学解决问题差的怎么办 孩子五年级了学习成绩不好怎么办 孩子做错事不承认怎么办 二年级小孩拼音差怎么办 小学三年级数学60多分怎么办 孩子三年级了数学不开窍怎么办