編輯:關於Android編程
主要是想一自己閱讀代碼後的一些小收獲分享給大家。讓大家更加深入的了解Android的事件分發這塊的內容。
本文將在第一章通過自己的語言,簡單介紹dispatchTouchEvent,並將其中的一些關鍵點直接提煉出來,方便那些不想閱讀源代碼的同學把握住其中的關鍵點。
在第二章將放上源碼,其中包含了我閱讀過程中的26處注釋。
ViewGroup的dispatchTouchEvent是對View的dispatchTouchEvent函數的重新,兩者具有很大的區別。如下表:
View的dispatchTouchEvent函數 ViewGroup的dispatchTouchEvent函數 1)是將Event派發給自己的onTouchEvent函數處理。1)將Event派發給子View
2)只有在子View沒有相應該事件或ViewGroup攔截了該事件的時候,才通過View的dispatchTouchEvent將事件派發給自身。
有上面兩個表,應該大致了解了ViewGroup的dispatchTouchEvent大致講上面了吧。那現在具體講講做了哪些事: Step 1: 如果收到的ACTION_DOWN 那麼清除所有和時間相關的狀態 Step 2: 判斷是否攔截事件
1) 如果在子view中調用了getParent().requestDisallowInterceptTouchEvent(true)那麼將不會攔截該事件。
Step 3: 如果事件是ACTION_DOWN ,並且沒有被攔截,將會執行向子View的派發處理。
1)派發順序
5.0之前,基本按照子View被添加的順序
5.0之後,還需要考慮Z軸、Drawing順序、添加的順序
重點關注Z軸順序,值最大的將會最優先,這是得到所有的子view,那麼如何判斷是否點擊到子View區域內呢?
2)判斷子View是否可見或者存在動畫,否則不處理
3)事件是否點擊到子View的區域內
protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) { final float[] point = getTempPoint(); point[0] = x; point[1] = y; //[lxb] 如果view做了相應的變換,那麼將point也做相應的變換 transformPointToViewLocal(point, child); //[lxb] 直接將pointInView函數拿出 // return LocalX >= 0 && localX < (mRight - mLeft) // && localY >= 0 && localY < (mBottom - mTop); // 經過變換之後的point,則已經將左上角作為原點,所以只要判斷是否在這個矩形中就可以了 // 所以這就是為什麼如TranslateAnimation的原始點擊點還是在最開始的地方 final boolean isInView = child.pointInView(point[0], point[1]); if (isInView && outLocalPoint != null) { outLocalPoint.set(point[0], point[1]); } return isInView; }
這也就是為什麼補間動畫盡管顯示的View已經移動了另一個地方,但是點擊區域還是在最原始的地方。
4)派發該事件給子View,看子View是否要處理
注意,parent將event該子view的時候做了相應的偏移處理,所以子view中的event.getX() 和 parent中的不一樣。
具體如下:
//[lxb] 將event做相應的偏移之後,然後傳遞給相應的子view final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event);
5)將相應了該事件的子view加入到mFirstTouchTarget的單鏈表中(接下來的事件都會直接派發給其中的view)
Step 4: 如果沒有一個子View相應Action_DOWN 怎麼辦?
那麼直接將事件交給父類View的dispatchTouchEvent,再根據條件派發給自己。
Step 5: 其他事件如 ACTION_MOVE, ACTION_UP, ACTION_CANCEL則直接照著mFirstTouchTarget鏈表派發就行。
最後:
講講攔截,如果ViewGrop 攔截該事件,那麼將立馬給之前相應事件的子View派發一個ACTION_CANCEL事件。
另外,還需要注意的是,如果ViewGrop 攔截該事件,並不會立馬將該事件給自己的onTouchEvent,只有等下次事件過來才可能,所以在onInterceptTouchEvent中需要對ACTION_UP和ACTION_CANCEL也做相應處理。
public boolean dispatchTouchEvent(MotionEvent ev) { //[lxb #1] 調試使用,請忽略 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. //[lxb #2] [輔助功能] 事件將會第一個派發給開啟了accessibility focused的view if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false); } boolean handled = false; //[lxb #3] 表示窗口是否為模糊窗口(FILTER_TOUCHES_WHEN_OBSCURED),如果是窗口,則表示不希望處理改事件。(如dialog後的窗口) if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); //[lxb #4] 過濾字段的最後8bit,也就是指只關心是ACTION_DOWN、ACTION_UP等事件,而不關心是哪個手指引起的。 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. //[lxb #5] 初始化相關狀態 //[lxb #5] (01) 清空mFirstTouchTarget鏈表,並設置mFirstTouchTarget為null。mFirstTouchTarget是"接受觸摸事件的View"所組成的單鏈表 //[lxb #5] (02) 清空mGroupFlags的FLAG_DISALLOW_INTERCEPT標記,如果設置了FLAG_DISALLOW_INTERCEPT,ViewGroup對觸摸事件進行攔截。 //[lxb #5] (03) 清空mPrivateFlags的PFLAG_CANCEL_NEXT_UP_EVEN標記,作用是將下一個時間變我Cancel cancelAndClearTouchTargets(ev); resetTouchState(); } // Check for interception. final boolean intercepted; //[lxb #6] 如果為DOWN事件,或者mFirstTouchTarget為null(那麼事件直接給到自己),就沒必要執行攔截。 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { //[lxb #7] 查看是否設置了,禁止攔截的標記 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { //[lxb #8] ViewGroup的onInterceptTouchEvent不執行攔截,除非子類重寫了該方法(如listview) intercepted = onInterceptTouchEvent(ev); //[lxb #9] 僅僅是避免action被篡改過。 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. //[lxb #10] 查看時候被標記了PFLAG_CANCEL_NEXT_UP_EVENT 或者 當前是一個Cancel事件 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // Update list of touch targets for pointer down, if needed. //[lxb #11] 比如我們多個手指放到了屏幕上,是否要將第二個手指的事件下面下去 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; 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; 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. //[lxb #12] 清除Targets中相應的pointer ids removePointersFromTouchTargets(idBitsToAssign); //[lxb #13] 遍歷所有的child,將事件派發下去 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. //[lxb #13] 以 1)Z軸(5.0系統引入) 2)draw的順序 進行排序 final ArrayListpreorderedList = buildOrderedChildList(); //[lxb #14] 可以理解為,是否按照draw的順序(因為,buildOrderedChildList在都沒有設置Z的情況下返回null) final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { //[lxb #15] 這裡兩端代碼,簡單的理解根據不同的排列選項(1、view添加到 2、view的draw順序 3、viewZ 軸順序) 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. //[lxb #16] 如果存在開啟了AccessibilityFocus 的view if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; //[lxb #17] 如果正是當前的childView開啟了AccessibilityFocus,直接將i指向最後一個元素 //[lxb #17] 和 break的區別是,還將執行後面的代碼,但是不會再進行循環了 i = childrenCount - 1; } //[lxb #18] canViewReceivePointerEvents 判斷child是否為visiable 或者 是否有動畫 //[lxb #18] isTransformedTouchPointInView 判斷x, y是否在view的區域內(如果是執行了補間動畫 則x,y會通過獲取的matrix變換值 //[lxb #18] 換算當相應的區域,這也是為什麼補間動畫的觸發區域不隨著動畫而改變) if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } //[lxb #19] getTouchTarget 查找child是否已經記錄在mFirstTouchTarget這個單鏈表中 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); //[lxb #20] 簡單的理解,dispatchTransformedTouchEvent就是將相應的事件傳遞下去 //[lxb #20] 不過需要注意一點的就是,event被傳遞給child的時候將會做相應偏移,如下 //[lxb #20] final float offsetX = mScrollX - child.mLeft; //[lxb #20] final float offsetY = mScrollY - child.mTop; //[lxb #20] event.offsetLocation(offsetX, offsetY); //[lxb #20] 為什麼要做偏移呢? 因為event的getX得到的值是,childView到parentView邊境的距離,是一個相對值 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 //[lxb #21] 找到childIndex所代表的child的最原始的index【?】看代碼,children和mChildren指向同一鏈表 for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); //[lxb #22] 將相應該事件的child包裝成一個Target,添加到mFirstTouchTarget的鏈表中 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(); } //[lxb #22] 如果沒有child相應該事件,則將此事件交給最近加入的target 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; } } } //[lxb #23] mFirstTouchTarget == null 表示,沒有能相應該事件的child,那麼就調用父類(也就是View)的dispatchTouchEvent // 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; while (target != null) { final TouchTarget next = target.next; //[lxb #24] 表示在Down事件處理中,已經將這個事件交給newTouchTarget處理過了,就不重復處理了 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { //[lxb #25] 再次判定是否需要cancel(被標記為cancel 或者 事件被攔截) final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } //[lxb #26] 問:難道看到這裡你們會不會產生一個疑問,如果parent在ACTION_MOVE過程中攔截了該事件,哪裡hi處理呢? //[lxb #26] 答:如果攔截了該事件,還是需要自身 dispatchTransformedTouchEvent 函數將事件交個自己的onTouchEvent //[lxb #26] 此外dispatchTransformedTouchEvent完成上述操作需要一個條件,也就是child形參數為null //[lxb #26] 問:那麼怎麼為null呢? //[lxb #26] 答:往上面看10幾行,不就是了嗎? //[lxb #26] 那麼如何達到,其實條件就是 mFirstTouchTarget == null, 請看下面的分析 //[lxb #26] 如果intercepted == true的情況下, cancelChild == true, predecessor == null //[lxb #26] 從而使得mFirstTouchTarget 一直 -> next,當target遍歷到最後的時候,next == null,從而使得mFirstTouchTarget == null。 //[lxb #26] 問: 稍等,這裡僅僅做了將mFirstTouchTarget 設置了為null,那麼如何派發給自己的onTouchEvent呢? //[lxb #26] 這個只能等下一個事件過來了 //[lxb #26] 結論【事件攔截,攔截了該事件,並沒有將本次這個事件傳遞給自身的onTouchEvent,而需要等到下次】 //[lxb #26] 問:如何驗證 //[lxb #26] 答:重新FrameLayout的 onInterceptTouchEvent 和 onTouchEvent 將相應的event.getEventTime打印出來, //[lxb #26] 將會發現攔截的事件和傳遞到onTouchEvent的時間不是一個時間。 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. //[lxb #27] cancel ACTION_UP ACTION_HOVER_MOVE(表示鼠標滑動)等,清理狀態 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); } } //[lxb #28] 調試使用,可以忽略 if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
當我們拿到一台Android的智能手機,從打開開關,到我們可以使用其中的app時,這個啟動過程到底是怎麼樣的? 系統上電 當給Android系統上電,CPU復位之後,
自從項目中使用RxJava以來,可以很方便的切換線程。至於是怎麼實現的,一直沒有深入的研究過!本篇文章就是分析RxJava的線程模型。 RxJava基本使用 先上一個
android的熱修復技術我看的最早的應該是QQ空間團隊的解決方案,後來真正需要了,才仔細調查,現在的方案中,阿裡有兩種Dexposed和Andfix框架,由於前一種不支
本文實例介紹了Android如何畫出觸摸軌跡的方法,分享給大家供大家參考,具體內容如下效果圖:實現代碼:package com.android.gameview5;imp