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事件分发过程的流程图吧,相信可以帮助大家更好地去理解:
整个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
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下) 。
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- mysql与oracle的一些不同点
- px 与 dp, sp换算公式
- ViewPager实现轮播图效果
- Qt qobject_cast 函数用法
- mac快捷键
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Dialog弹出框(系统自带)
- epoll的反应堆实现模式
- linux学习笔记之用户与组以及权限的管理
- 正则表达式积累笔记
- android压力测试命令monkey详解
- java设计模式进阶_bridge
- Linux实用脚本命令
- Java学习(2)-Map的遍历