Android源码分析--View的事件分发机制
2017-07-18 15:45
711 查看
Android源码分析–View的事件分发机制
网上关于View的事件分发机制的文章有很多,所以这次更博呢,更重要的目的是让自己重新温习一遍View的分发机制,通过文字的描述,希望自己能讲透讲明白这个过程,从头到尾从源码层面去剖析内部原理。看了本文你能学到什么?
View事件的分发流程;
View事件分发过程中,源码级的分析
View事件的分发流程
首先我们来看一张图片,大体上了解下这整一个过程从上图我们可以看到,最先拿到View事件的是Activity,Activity可以先下分发,也可以自己消费,向下分发这个过程,下面我们会通过源码来分析,我们主要先看下大体的一个流程。可以发现,Activity、ViewGroup、View三者都能消费事件,事件消费既在onTouchEvent中返回true。如果View和ViewGroup都不消费这个事件,最终事件会交还给Activity,由它来消费。当然,ViewGroup中还可以拦截事件,不向下继续传递。这一part先到这里,看不太不明白没关系其实,下面我们将从源码层面去一步步跟踪分析这一个过程。
View事件分发过程中,源码级的分析
首先,我们先得了解事件分发过程中,非常重要的三个方法:public boolean dispatchTouchEvent(MotionEvent ev); //用来分发事件
public boolean onInterceptTouchEvent(MotionEvent ev); //用来拦截事件
public boolean onTouchEvent(MotionEvent ev); //用来处理事件
这三个方法,在事件分发过程中,起了非常重要的作用,需要注意的是,onInterceptTouchEvent是ViewGroup才有的方法。我们先要有一个大体的了解,上面我们说了,首先获得事件的是Activity,获得事件之后,会调用自己的dispatchTouchEvent,我们来看下源码:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
可以看到,方法里面有一句getWindow().superDispatchTouchEvent(ev),这句话是调用了Window的superDispatchTouchEvent(ev)方法,事件就交给了Window,但是,问题来了,假如对源码有一定了解的话,Window是一个抽象类,该方法是空方法,那么,我们就得着Window的实现类,我们知道,Android中,PhoneWindow是Window的唯一实现类,所以,我们在PhoneWindow中查看superDispatchTouchEvent(ev)的具体实现:
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
哈哈,坑爹了吧,这里直接return的是mDecor的superDispatchTouchEvent(ev)返回值,那么,事件就传递到了mDecor上面了。什么是mDecor呢,这是PhoneWindow中的一个成员变量,是DecorView的一个对象,我们回忆一下,我们在Activity的onCreate方法里面有这一句:setContentView(view),这个View我们可以通过getWindow().getDecorView().findViewById(R.id.content)得到View的父控件,这的返回值其实是FrameLayout,我们把返回值强转为ViewGroup之后调用getChildAt(0),得到的就是setContentView中我们设置的View。
刚刚说到现在事件由PhoneWindow传递到了DecorView上面,而DecorView是继承自FrameLayout的,所以,到这一步,实际上事件就已经传递给View了。我们接下来看下DecorView中的superDispatchTouchEvent(event)方法:
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
可以看到,还是没有具体的处理case,继续往前看,刚刚说到DecorView是继承自FrameLayout的,FrameLayout是没有实现dispatchTouchEvent方法的,而FrameLayout有是继承自ViewGroup的,所以 我们可以直接看ViewGroup中的dispatchTouchEvent方法,由于这个方法实现过长,我们分开看
// Handle an initial down. //如果是down事件的话,设置的请求不拦截的FLAG_DISALLOW_INTERCEPT标记将被清空, //这个标记位是在requestDisallowInterCeptTouchEvent方法中 //设置的。换句话表达,就是ViewGroup将无法拦截设置了 //FLAG_DISALLOW_INTERCEPT标记的move和up事件。 //下面的代码,如果是down事件,将重置标记位,所以如果down事件 //设置了FLAG_DISALLOW_INTERCEPT标记,其实也是没用的, //依然会判断是否拦截。这点需要非常清楚,后续的事件冲突处理需要灵活运用。 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(); } 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; }
//设置标记位 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }
上面的代码是关于拦截事件的判断,我们可以看到,条件是down事件或者mFirstTouchTarget不为空时,才可能拦截,也就是说,当事件是move或者up时,mFirstTouchTarget不为空才能进入这个if分支。那么mFirstTouchTarget是什么呢?看到这里我也不明白,咱暂且先放下。继续往下看
// Check for cancelation. final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // 是否需要拆分MotionEvent动作,默认为拆分 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; //MotionEvent没有被取消并且没有被拦截时进入分支 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; //当前事件的down,或者接下来手指的down,这里需要明白 //一个事情,MotionEvent.ACTION_DOWN是指出发当前MotionEvent事件的触点的down事件, //MotionEvent.ACTION_POINTER_DOWN是指有另外的触点触发的down事件, //这里其实是一个多点触控的逻辑,以下,大致理解为找出触发当前一系列MotionEvent的触点, //然后获取事件相当于父控件的X,Y坐标。 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. //清除触点信息,只留下动作信息,一个MotionEvent是由Action和Pointer组成的 //一个16位值,前8位表示触点信息,后八位表示动作信息。 removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { //子View根据动作索引去获取当前触点的相当于父控件(其实就是当前这个ViewGroup)X,Y坐标 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 = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; //遍历所有子View 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. //子View是否能获取焦点 if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } //子View是否能接受事件,坐标是否在当前控件范围内 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); //dispatchTransformedTouchEvent实际调用的是子View(child)的dispatchTouchEvent方法。 //实际上就是事件传递给子View的一个过程。如果子View的dispatchTouchEvent返回true, //mFristTouchEvent会被赋值,并且终止循环,如果返回false,那么循环继续,直至没有子View 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(); //mFristTouchEvent就是在下面这个方法被赋值的。 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(); } ... } }
上面可以看到,当TouchEvent被传递给child时,mFristTouchEvent会被赋值。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }
可以看到 其实TouchTarget是一种单链表的结构,mFirstTouchTarget是否被赋值,将影响到ViewGroup的拦截case,那么,我们需要回过头再去想想我们放下没有解决的问题,mFirstTouchTarget在事件分发给子View之后会被赋值,就意味着,onInterCeptTouchEvent这个方法的调用时机实际上是如果ViewGroup拦截了当前的down事件的话,那么mFristTouchEvent必然为null,onInterCeptTouchEvent这个方法不会被调用,而是由它直接处理。
所以我们得出以下结论:
如果ViewGroup拦截了TouchEvent事件,那么,move和up事件也必然交由它处理,不再分发。
接下来我们继续往下看,如果mFristTouchEvent为空时如何处理的:
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }
上面调用了dispatchTransformedTouchEvent方法,注意,这里是第二次调用这个方法了,第一次是在ViewGroup分发事件给子View时,child传递是的非空的,第二次是上面,我们可以看到 ,这里的第三个参数传递的是null,我们跟踪下,只看它关键部分的代码:
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); //child为空,事件交到View的dispatchTouchEvent处理 if (child == null) { handled = super.dispatchTouchEvent(event); } else {//子View处理 handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; }
接下来看下View的dispatchTouchEvent的实现:
public boolean dispatchTouchEvent(MotionEvent event) { ... boolean result = false; ... 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; } if (!result && onTouchEvent(event)) { result = true; } } ... return result; }
因为View没有子View,这里的实现也比较简单,我把主要的代码贴出来了,从上面的代码可以看出,只要一个View设置了OnTouchListener,那么,将执行的是onTouch方法,通过onTouch返回true,事件到此为止消耗完毕,如何OnTouchListener为null,那么将执行View的onTouchEvent方法,执行完毕如果返回true,事件消费完毕。可以这么理解,OnTouchListener的优先级是高于onTouchEvent的。
接下来我们看下View中的onTouchEvent方法的实现:
if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } //如果View设置有代理,将执行TouchDelegate中的onTouchEvent方法 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } }
上面的代码,我们可以得出一个结论
一个View的状态即使是disable的状态下,照样是能消费点击事件的。
//只要View的CLICKABLE、LONG_CLICKABLE或者CONTEXT_CLICKABLE有一个为true,那么就会消费这个事件 if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { 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; case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_MOVE: drawableHotspotChanged(x, y); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); setPressed(false); } } break; } return true; } return false; }
up事件发生时,会触发调用performClick()方法,如果View设置了clickListener,performClick中会调用它的onClick方法。
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); return result; }
以上,就是一个TouchEvent完整的分发机制,然后,看懂上面的代码,回过头再看这图片,是不是非常简单了呢?祝各位进步!
相关文章推荐
- Android事件分发机制源码分析上----View事件分发分析
- Android事件分发机制源码分析下----ViewGroup事件分发分析
- Android—— View事件分发机制的源码分析
- Android应用开发原理之从ViewGroup源码分析ViewGroup的事件分发机制
- Android触摸事件分发处理机制详解与源码分析一(View篇)
- Android View和ViewGroup事件分发机制源码分析
- Android源码分析(二):View的事件分发机制探析
- Android 事件分发机制(最新源码6.0分析)--ViewGrop
- 【Android】源码分析 - View事件分发机制
- Android事件分发机制源码分析之View篇
- Android中ViewGroup、View事件分发机制源码分析总结(雷惊风)
- Android触摸屏事件派发机制详解与源码分析一(View篇)
- Android触摸屏事件派发机制详解与源码分析一(View篇)
- Android触摸屏事件派发机制详解与源码分析一(View篇)
- Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)
- Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)dispatchtouchevent,ontouch,ontouchevent,onclick
- Android View 事件分发机制 源码解析 (上)
- Android触摸屏事件派发机制详解与源码分析一(View篇)
- Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)
- Android触摸屏事件派发机制详解与源码分析一(View篇)