本篇博文是Android點擊事件分發機制系列博文的第三篇,主要是從解讀ViewGroup類的源碼入手,根據源碼理清ViewGroup點擊事件分發原理,明白ViewGroup和View點擊事件分發的關系,并掌握ViewGroup點擊事件分法機制。特別聲明的是,本源碼解讀是基于最新的Android6.0版本。
ViewGroup事件分發中的三個重要方法的源碼解析
關于ViewGroup事件分發,我們重點需要解讀dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三個方法。ViewGroup比View多了一個onInterceptTouchEvent攔截事件方法,該方法源碼默認返回false,即ViewGroup默認不攔截任何事件。
(一)dispatchTouchEvent源碼解析
/** * 重寫了父類View的dispatchTouchEvent方法 */ @Override 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() amp;amp; isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false); } boolean handled = false;// 是否處理 if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action amp; MotionEvent.ACTION_MASK; // 手指按下去進行一些初始化的處理 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;// 是否被攔截 // 如果事件類型ACTION_DOWN或者mFirstTouchTarget不為空 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags amp; FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) {// 當disallowIntercept為false時 intercepted = onInterceptTouchEvent(ev);// 這里是重點,它會調用onInterceptTouchEvent方法,當該方法為true時攔截,為false時不攔截 // 所謂的攔截,是指按下去自身以及以后的后續事件move up,攔截下來給自己onTouch使用 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; } // 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 amp; FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled amp;amp; !intercepted) {// 這里的canceled和intercepted都為false時,條件成立,也就是說不攔截 // 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; if (actionMasked == MotionEvent.ACTION_DOWN || (split amp;amp; actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {// 按下去或多指觸摸或者移動劃過 final int actionIndex = ev.getActionIndex(); // always 0 for down final int idBitsToAssign = split ? 1 lt;lt; 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 amp;amp; childrenCount != 0) {// 如果子控件的個數不為0 且 newTouchTarget為空 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. // 考慮到兩個View交叉重合的情況,下面的先放進集合,但是按常理說我們手指先按到上面的,這里做了一個倒序 final ArrayListlt;Viewgt; preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null amp;amp; isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i gt;= 0; i--) {// 遍歷ViewGroup中的子控件 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); // 判斷是否有子控件,如果沒有就不會執行里面的操作,如果有子控件則執行內部的操作 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 lt; childrenCount; j ) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX();// 拿到X、Y位置 mLastTouchDownY = ev.getY(); // newTouchTarget用到了單向鏈表 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 amp;amp; mFirstTouchTarget != null) {// 如果newTouchTarge為空 且 mFirstTouchTarget不為空 // 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.分發給touch targets if (mFirstTouchTarget == null) { // 沒有觸摸的對象就把它當做一個普通的View // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);// 第三個參數本應為child,這里是null意味著需要調用父類View的dispatchTouchEvent方法,然后調用onTouch方法 } 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 amp;amp; 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 amp;amp; actionMasked == MotionEvent.ACTION_POINTER_UP) { final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 lt;lt; ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } } if (!handled amp;amp; mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
ViewGroup攔截情況源碼分析
首先我們來看一下第34行~48行的代碼,ViewGroup在如下兩種情況下會判斷是否要攔截當前事件:
事件類型為ACTION_DOWN或者 mFirstTouchTarget != null
即,當事件由ViewGroup的子元素成功處理時,mFirstTouchTarget 會被賦值并指向子元素,換句話說,當ViewGroup不攔截事件并將事件交由子元素處理時mFirstTouchTarget != null。反過來,一旦事件由當前的ViewGroup攔截時,mFirstTouchTarget != null條件就不成立。那么當ACTION_MOVE和UP事件到來時,由于actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null這個條件為false,導致ViewGroup的onInterceptTouchEvent不會再被調用,并且同一序列中的其他事件都會默認交給它處理。
另外,這里有一種特殊情況,我們看36行代碼,有個FLAG_DISALLOW_INTERCEPT標記為,這個標記是通過requestDisallowInterceptTouchEvent()方法來設置的,一般用在子View中。如果我們通過reqeustDisallowInterceptTouchEvent()方法設置了FLAG_DISALLOW_INTERCEPT標記位后,ViewGroup將無法攔截除了ACTION_DOWN以外的其他方法(即調用該方法并不影響ACTION_DOWN事件處理)。因為ViewGroup會在ACTION_DWON事件到來時做重置狀態操作,這里從代碼第22~29行可以看出。
requestDisallowInterceptTouchEvent源碼解析
/** * {@inheritDoc} */ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags amp; FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags amp;= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }
取消、清理、重置之前的觸摸狀態
/** * Cancels and clears all touch targets. */ private void cancelAndClearTouchTargets(MotionEvent event) { if (mFirstTouchTarget != null) {// 如果保存的第一個觸摸View對象不為空 boolean syntheticEvent = false; if (event == null) { final long now = SystemClock.uptimeMillis(); event = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); event.setSource(InputDevice.SOURCE_TOUCHSCREEN); syntheticEvent = true; } for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) { resetCancelNextUpFlag(target.child); dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits); } clearTouchTargets(); if (syntheticEvent) { event.recycle(); } } } /** * Resets all touch state in preparation for a new cycle. */ private void resetTouchState() { clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags amp;= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE; }
因此我們可以得出如下結論:
1.當ViewGroup決定攔截事件后,那么點擊事件將會默認交給它處理并且不再調用它的onInterceptTouchEvent方法,FLAG_DISALLOW_INTERCEPT這個標記的作用是ViewGroup不再攔截事件,前提是ViewGroup不攔截ACTION_DOWN事件處理。
2.如果事件能夠傳遞到當前的ViewGroup,如果我們要提前處理所有點擊事件,應該選擇dispatchTouchEvent方法,因為只有這個方法能確保每次都會被調用;而onInterceptTouchEvent()卻無法保證每次事件都會被調用。
3.FLAG_DISALLOW_INTERCEPT標記位可以用于解決滑動沖突問題。
ViewGroup不攔截情況源碼分析
ViewGroup不攔截事件的時候,事件會向下分發交由它的子View進行處理。先來看下代碼64行(!canceled amp;amp; !intercepted)這里的canceled和intercepted都為false時,條件成立,也就是說不攔截。接下來74行的條件判斷:
actionMasked == MotionEvent.ACTION_DOWN
|| (split amp;amp; actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE
在該if條件內,看到第86行,如果newTouchTarget == null amp;amp; childrenCount != 0,即子控件的個數不為0 且 newTouchTarget為空,在95行中遍歷整個ViewGroup中的子控件,這里的集合做了個倒序排列,如果兩個View交叉覆蓋在一起,下面的子控件先放進集合,因為后被添加的子控件會浮在上面,通常我們會希望點擊的時候最上層的那個組件先去響應事件。接著105行代碼開始判斷子控件是否能夠接收到點擊事件,主要依賴于兩個條件:第一子控件是否在播動畫;第二點擊事件是否落在子控件的區域內。如果某個子控件滿足這兩個條件,那么事件就會傳遞給它來處理。
buildOrderedChildList方法解析
/** * 實現倒序排序 * Populates (and returns) mPreSortedChildren with a pre-ordered list of the View's children, * sorted first by Z, then by child drawing order (if applicable). This list must be cleared * after use to avoid leaking child Views. * Uses a stable, insertion sort which is commonly O(n) for ViewGroups with very few elevated * children. */ ArrayListlt;Viewgt; buildOrderedChildList() { final int count = mChildrenCount; if (count lt;= 1 || !hasChildWithZ()) return null; if (mPreSortedChildren == null) { mPreSortedChildren = new ArrayListlt;Viewgt;(count); } else { mPreSortedChildren.ensureCapacity(count); } final boolean useCustomOrder = isChildrenDrawingOrderEnabled(); for (int i = 0; i lt; mChildrenCount; i ) { // add next child (in child order) to end of list int childIndex = useCustomOrder ? getChildDrawingOrder(mChildrenCount, i) : i; View nextChild = mChildren[childIndex]; float currentZ = nextChild.getZ(); // insert ahead of any Views with greater Z int insertIndex = i; while (insertIndex gt; 0 amp;amp; mPreSortedChildren.get(insertIndex - 1).getZ() gt; currentZ) { insertIndex--; } mPreSortedChildren.add(insertIndex, nextChild); } return mPreSortedChildren; }
接著看代碼,129行通過dispatchTransformedTouchEvent()這一重要方法(后面有詳細分析),判斷是否有子控件,如果有子控件則執行內部的操作,并找到最終觸摸的對象,通過addTouchTarget方法賦值給newTouchTarget。在dispatchTransformedTouchEvent()方法中,如果子控件的dispatchTouchEvent()方法返回true,那么mFirstTouchTarget就會被賦值,同時跳出for循環,詳見148行代碼。同樣如果dispatchTouchEvent()方法返回false,ViewGroup就會把事件分發給下一個子控件(如果還有下一個子控件)。
mFirstTouchEvent的真正賦值其實是在addTouchTarget方法中完成的,mFirstTouchEvent其實是一個單鏈表結構,如果mFirstTouchEvent為null,那么ViewGroup就會默認攔截下來同一序列中所有的點擊事件。
addTouchTarget方法解析
/** * Adds a touch target for specified child to the beginning of the list. * Assumes the target child is not already present. */ private TouchTarget addTouchTarget(View child, int pointerIdBits) { TouchTarget target = TouchTarget.obtain(child, pointerIdBits); // 往鏈表中插入一個元素 target.next = mFirstTouchTarget;// 將mFirstTouchTarget賦值給target.next mFirstTouchTarget = target;// 將target賦值給mFirstTouchTarget return target; }
接著我們看到171行代碼中,如果mFirstTouchEvent為null,也就是說要么ViewGroup中沒有子控件,要么是子控件處理了點擊事件,但是在dispatchTouchEvent中返回了false(子控件在onTouchEvent中返回了false),那么ViewGroup就會自己處理點擊事件,需要說明的是175行代碼中,第三個參數本應為child,這里是null意味著需要調用父類View的dispatchTouchEvent方法,然后調用onTouch方法。
TouchTarget 內部類源碼解析
/* Describes a touched view and the ids of the pointers that it has captured. * 鏈表實現的內部類,解決多指觸控問題用來指定當前觸摸的對象,多個手指觸控(0~31個手指) * This code assumes that pointer ids are always in the range 0..31 such that * it can use a bitfield to track which pointer ids are present. * As it happens, the lower layers of the input dispatch pipeline also use the * same trick so the assumption should be safe here... */ private static final class TouchTarget { private static final int MAX_RECYCLED = 32; private static final Object sRecycleLock = new Object[0]; private static TouchTarget sRecycleBin; private static int sRecycledCount; public static final int ALL_POINTER_IDS = -1; // all ones // The touched child view. public View child; // The combined bit mask of pointer ids for all pointers captured by the target. public int pointerIdBits; // The next target in the target list. public TouchTarget next; private TouchTarget() { } public static TouchTarget obtain(View child, int pointerIdBits) { final TouchTarget target; synchronized (sRecycleLock) { if (sRecycleBin == null) { target = new TouchTarget(); } else { target = sRecycleBin; sRecycleBin = target.next; sRecycledCount--; target.next = null; } } target.child = child; target.pointerIdBits = pointerIdBits;// 手指的ID return target; } public void recycle() { synchronized (sRecycleLock) { if (sRecycledCount lt; MAX_RECYCLED) { next = sRecycleBin; sRecycleBin = this; sRecycledCount = 1; } else { next = null; } child = null; } } }
至關重要的dispatchTransformedTouchEvent方法解析
/** * Transforms a motion event into the coordinate space of a particular child view, * filters out irrelevant pointer ids, and overrides its action if necessary. * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead. */ private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // Canceling motions is a special case. We don't need to perform any transformations // or filtering. The important part is the action, not the contents. final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) {// 如果child為空,則調用自己的分發方法 handled = super.dispatchTouchEvent(event); } else {// 否則調用child的分發方法 handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } // Calculate the number of pointers to deliver. final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits amp; desiredPointerIdBits; // If for some reason we ended up in an inconsistent state where it looks like we // might produce a motion event with no pointers in it, then drop the event. if (newPointerIdBits == 0) { return false; } // If the number of pointers is the same and we don't need to perform any fancy // irreversible transformations, then we can reuse the motion event for this // dispatch as long as we are careful to revert any changes we make. // Otherwise we need to make a copy. final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); } // Perform any necessary transformations and dispatch. if (child == null) {// 如果ViewGroup中沒有子控件,調用父類View的dispatchTouchEvent handled = super.dispatchTouchEvent(transformedEvent); } else {// 如果有子控件 final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } // 調用子控件的dispatchTouchEvent,有兩種情況,如果是子控件是View,又分成兩種情況(Button返回true,TextView返回false);如果是ViewGroup則進入遞歸了,又回到了這段代碼,最終要么沒有任何消耗事件的View,要么找到消費事件的View handled = child.dispatchTouchEvent(transformedEvent); } // Done. transformedEvent.recycle(); return handled; }
該方法在dispatchTouchEvent()中被調用,用于將事件分發給子View處理。我們重點看一下60~71行代碼。在dispatchTransformedTouchEvent()方法一共有三個參數,其中第三個參數View child有時為null,有時不為null。61行代碼中,child==null意味著事件沒有被消費,ViewGroup中沒有子控件需要調用父類View的dispatchTouchEvent方法,即super.dispatchTouchEvent(event)。
接著我們關注下handled這個變量,可以發現dispatchTransformedTouchEvent()方法return handled,而handled的值其實是取決于dispatchTransformedTouchEvent()方法遞歸調用dispatchTouchEvent()方法的結果,也就是說在子控件中dispatchTouchEvent()方法的onTouchEvent()是否消費了Touch事件的返回值決定了dispatchTransformedTouchEvent()的返回值,從而決定mFirstTouchTarget是否為null,更進一步決定了ViewGroup是否處理Touch事件。
(二)onInterceptTouchEvent源碼解析
public boolean onInterceptTouchEvent(MotionEvent ev) { return false;// 默認不攔截 }
你沒有看錯,這方法就是簡簡單單的一個布爾值返回,當返回true時,對事件進行攔截,返回false則不攔截。
(三)ViewGroup點擊事件分發小結
Android點擊事件分發是到達頂級View后(一般是ViewGroup),會調用ViewGroup的dispatchTouchEvent方法,其中它的onInterceptTouchEvent方法如果返回true,則會對事件傳遞進行攔截,事件由ViewGroup處理;如果onInterceptTouchEvent方法返回false,則代表不對事件進行攔截,默認返回false。則此時子View中的dispatchTouchEvent方法將被調用,到此,事件已經由頂級View傳遞給了下一層的View,接下來的過程是一個遞歸循環的過程,和頂級View事件分發過程是一致的,直到完成整個事件分發。
Tags: 安卓開發
文章來源:http://blog.csdn.net/mynameishuangshuai/article/de