編輯:關於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 ArrayList preorderedList = 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系統的啟動過程
當我們拿到一台Android的智能手機,從打開開關,到我們可以使用其中的app時,這個啟動過程到底是怎麼樣的? 系統上電 當給Android系統上電,CPU復位之後,
RxJava(RxAndroid)線程切換機制
自從項目中使用RxJava以來,可以很方便的切換線程。至於是怎麼實現的,一直沒有深入的研究過!本篇文章就是分析RxJava的線程模型。 RxJava基本使用 先上一個
Android熱修復:Andfix和Hotfix,兩種方案的比較與實現
android的熱修復技術我看的最早的應該是QQ空間團隊的解決方案,後來真正需要了,才仔細調查,現在的方案中,阿裡有兩種Dexposed和Andfix框架,由於前一種不支
Android輕松畫出觸摸軌跡
本文實例介紹了Android如何畫出觸摸軌跡的方法,分享給大家供大家參考,具體內容如下效果圖:實現代碼:package com.android.gameview5;imp