編輯:關於Android編程
自從開始負責公共控件模塊開始,我一直都想好好分析一下Android事件傳遞流程,相信網上有一大堆相關文章,但是我個人覺得作為一個專業的控件開發人員,如果只是知道一下大概,而不知其所以然,則不算一個合格的公共控件人員,感謝我曾經一位同事,在我剛開始接觸控件的時候帶著我,很耐心的教會我控件的內在,下面我個人從源碼角度來分析Android事件傳遞流程,基於Android5.0的代碼,如果有錯誤的地方,還望指出。
一,根視圖內部消息派發過程
對於上層代碼來說,最先處理事件的是viewRootImpl,下面先看一下viewRootImpl與TouchEvent相關的內容:
protected int onProcess(QueuedInputEvent q) { if (q.mEvent instanceof KeyEvent) { mKeyEventStatus = INPUT_DISPATCH_STATE_VIEW_POST_IME_STAGE; return processKeyEvent(q); } else { mMotionEventStatus = INPUT_DISPATCH_STATE_VIEW_POST_IME_STAGE; // If delivering a new non-key event, make sure the window is // now allowed to start updating. handleDispatchDoneAnimating(); final int source = q.mEvent.getSource(); //判斷為屏幕點擊事件或者鼠標點擊事件 if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { return processPointerEvent(q); } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { return processTrackballEvent(q); } else { return processGenericMotionEvent(q); } } } private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; mAttachInfo.mUnbufferedDispatchRequested = false; boolean handled = mView.dispatchPointerEvent(event); if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) { mUnbufferedInputDispatch = true; if (mConsumeBatchedInputScheduled) { scheduleConsumeBatchedInputImmediately(); } } return handled ? FINISH_HANDLED : FORWARD; }
從上面代碼我們可以看出事件出傳遞到mView的dispatchPointerEvent方法裡面,通過追蹤mView,不難發現mView其實是PhoneWindow.DecorView。
而dispatchPointerEvent()是View裡面的方法,如果當前事件是Touch事件則會調用當前View的dispatchTouchEvent(),再看一下PhoneWindow.DecorView dispatchTouchEvent():
@Override public boolean dispatchTouchEvent(MotionEvent ev) { final Callback cb = getCallback(); //DecorView繼承自FrameLayout,但FrameLayout沒有重寫dispatchTouchEvent,cb為空則直接執行ViewGroup.dispatchTouchEvent() return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); } //CallBack 是Window的一個內部Interface,作用是可以讓用戶在事件的分發,菜單的構建過程中進行攔截。其中dispatchTouchEvent正是用於攔截觸控事件,Activity實現了這個方法 public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
從上面代碼可以看出,getWindow().superDispatchTouchEvent(ev)為false的時候,自身的onTouchEvent()函數才會被回調。這就是為什麼每次事件的都是從Activity的dispatchTouchEvent 開始,最終又會傳回Activity的onTouchEvent。另外這裡的getWindow()實際上返回的是PhoneWindow,下面再看看PhoneWindow相關的代碼:
@Override public boolean superDispatchTouchEvent(MotionEvent event) { boolean handled = mDecor.superDispatchTouchEvent(event); return handled; } public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
從上面代碼可看出,事件最後傳到mDecor.superDispatchTouchEvent(),而mDecor.superDispatchTouchEvent()單單執行了超類的dispatchTouchEvent()也就是ViewGroup的dispatchTouchEvent(),之後事件被分發到布局中的View中去。上述流程圖如下:
二,ViewGroup內部消息派發過程
1.ViewGroup中的dispatchTouchEvent
ViewGroup中的dispatchTouchEvent()在View中的事件傳遞中承擔了比較重要的角色,也是精華部分。它主要承擔了是三個工作:1.攔截子View事件(調用自身的onInterceptTouchEvent()) 2.找出可以接受事件的子View,並把事件傳遞下去 2.把交由自身的TouchEvent()處理事件。下面貼出ViewGroup中的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() && 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. /* ACTION_DOWN是一些列事件的開端,因此需要做一些初始化工作。這裡主要實現了兩點: 1.將mFirstTouchTarget設置為null 2.清除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(); } // Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { /* 1.disallowIntercept 由FLAG_DISALLOW_INTERCEPT標志位所決定,應用可以通過調用View.requestDisallowInterceptTouchEvent(boolean disallowIntercept)設置。默認為false 2.disallowIntercept為false時,onInterceptTouchEvent()會被調用,其返回值將決定事件是否會分發到其子view 3.TouchTarget是ViewGroup的內部靜態類,鏈式結構,有相關的回收機制,用於記錄當前被點擊的View,以及點擊的Pointer, ViewGroup的mFirstTouchTarget總指向TouchTarget的最前一個。 4.mFirstTouchTarget != null說明已經找到可以接受事件的View,通過onInterceptTouchEvent()同樣可以對其進行攔截 */ 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; } // 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; 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. removePointersFromTouchTargets(idBitsToAssign); full_meizu6795_lwt_l1-userdebug final int childrenCount = mChildrenCount; /* 1.從這裡開始遍歷子View,遍歷能采取兩種方式: ①當isChildrenDrawingOrderEnabled()返回true,根據getChildDrawingOrder(int childCount, int i)函數的順序遍歷 從這裡可以看出,應用可以通過重寫isChildrenDrawingOrderEnabled()以及getChildDrawingOrder(int childCount, int i)來指定子View的遍歷順序,isChildrenDrawingOrderEnabled()默認返回false ②默認情況下,采用倒序遍歷子View 2.遍歷過程中,會調用dispatchTransformedTouchEvent(),把事件傳到當前子View的dispatchTouchEvent()中 3.遍歷過程中,假如對某個子view執行dispatchTransformedTouchEvent()返回true,遍歷被中斷。同時把當前的View以及當前的TouchEvent的pointerIndex記錄在mFirstTouchTarget 中 */ 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 ArrayListpreorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; 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); 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(); 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. /* mFirstTouchTarget == null表明沒有找到可以消費事件的子view, 然後通過this.dispatchTransformedTouchEvent()——>View.dispatchTransformedTouchEvent()——>this.onTouchEvent() 流程把事件分發到自身的onTouchEvent() */ 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. /* 當mFirstTouchTarget !=null時可能是以下幾種情況: (1).滿足條件(alreadyDispatchedToNewTouchTarget && target == newTouchTarget)也就是當前事件是Action_Down,並且通過上面的遍歷找到可以消費事件的子View,則直接返回true; (2).不滿足1條件,但是事件通過需要被攔截,或者當前可消費事件的子View暫時不可見時,向該子View派發Action_Cancel事件,如上面的case5 (3).向當前可消費事件的子View分發事件。通常是這樣的情況:找到可消費事件的子View,當前又非Action_Down事件並且當前的ViewGroup沒有進行攔截。 */ TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; 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; } //cancelChild為true,說明child收到攔截,因此清理與該child相關的TouchTarget 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; }
其處理流程圖如下:
2.ViewGroup的dispatchTransformedTouchEvent()
下面先對dispatchTransfZ喎?/kf/ware/vc/" target="_blank" class="keylink">vcm1lZFRvdWNoRXZlbnSjqKOp1LTC67340NC31s72o7o8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;">
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.
//上面分析提及到,在事件被攔截時,子View可能會接收到CANCEL事件,在這裡可以看到event被直接傳到子類的dispatchTouchEvent()沒有作坐標轉換,因此在ACTION_CANCEL事件裡 面我們不應該去event的相關坐標進行計算。
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
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 & desiredPointerIdBits;
// Motion事件沒有對應點,則丟棄這個Motion
if (newPointerIdBits == 0) {
return false;
}
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//這裡將當前的坐標重新轉換到child的坐標系中
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);
}
.....
}
從上述源碼可知,dispatchTransformedTouchEvent()主要有兩個作用:
1.當child為空時,把事件傳遞到超類也就是View的dispatchTouchEvent()中。
2.當child不為空時,事件傳遞到child的dispatchTouchEvent()中,但是傳遞之前首先把事件的對應的坐標重新轉化為child坐標系中的坐標,而轉換的的方法是:
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
為什麼要這樣轉換?這裡涉及兩種坐標的概念:
1.視圖坐標:視圖坐標沒有邊界,它取決於View本身的大小,而不受屏幕大小的限制。
2.布局坐標:大小受限制,是指父視圖給子視圖分配的布局(layout)大小,超過這個大小的區域將不能顯示到父視圖的區域中。
上面兩種坐標的關系如下圖:
從上圖可知道布局坐標轉化為視圖坐標只需要加上mScrollY/mScrollX即可。
通常我們通過event.getX()/event.getX()獲取到的坐標是布局坐標,而child.mLeft,child.mRight則是相對於視圖坐標而言的,因此在父視圖獲取獲取到event的坐標後必須先轉化為視圖坐標,再根據child在父視圖中的位置,進行坐標平移,即 event.offsetLocation(+mScrollX - offsetX, + mScrollY - offsetY).
3,View的dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent event) {
....
if (onFilterTouchEventForSecurity(event)) {
//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.dispatchTouchEvent()的主要代碼,從上面代碼可以看出,當mOnTouchListener不為空時會先執行mOnTouchListener.onTouch(this, event),而當mOnTouchListener沒有消耗事件時onTouchEvent()才會被執行,這說明在同一個View,mOnTouchListener優先級會比onTouchEvent要高。
4,View的onTouchEvent(MotionEvent event)
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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.
//盡管當前View是disable狀態,但是只要是CLICKABLE或者LONG_CLICKABLE仍然可以消費事件
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
//如果設置代理,則事件交由代理處理,如果代理消耗了事件則直接返回true
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
...
if (!mHasPerformedLongPress) {
// 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();
}
//假如對view設置了OnClickListener,在這裡會被回調。
if (!post(mPerformClick)) {
performClick();
}
}
}
...
break;
case MotionEvent.ACTION_DOWN:
...
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
//檢查長按事件,假如設置了OnLongClickListener,在此有可能被回調
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
//檢查長按事件,假如設置了OnLongClickListener,在此有可能被回調
checkForLongClick(0);
}
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
從上面代碼可以總結出一下幾點:
1.盡管當前View是disable狀態但仍然具有消費事件能力
2.假如設置了代理,事件將先交由代理處理,假如代理消費了事件,則直接返回true
3.在沒有設置代理的情況下,假如標記位CLICKABLE LONG_CLICKABLE不為0,則事件必然在此被消費
4.OnLongClickListener和OnClickListener的回調函數都在View的onTouchEvent()裡面執行,但執行時機有區別, OnClickListener.onClick()在ACTION_UP時被回調。OnLongClickListener.onLongClick()則在ACTION_DOWN的時候被執行(其實也不一定在這個時機被執行,因為長按需要作一定的時延檢測)。另外我們在定義View時經常需要監 聽點擊事件,這裡不推薦通過setOnClickListener()的方式實現,因為這樣相當於占用了應用的監聽權利,假如此時應用在不知道代碼邏輯的情況下,繼續setOnClickListener(),那麼默認的監聽器被覆蓋。做成意想不到的後果。其實替代方案可以是重寫performClick()。長按事件同理。
大家好,今天給大家分享一下Android裡的Context的一些用法,以前經常有人在群裡問我比如我在一個工具類裡的某個方法,或者View裡需要調用Context.但是工具
版本控制是項目開發過程中必不可少的部分,不管是個人還是團隊,靈活的使用版本控制將會使項目開發變得更加輕松。Android Studio集成了版本控制系統,目前支持CVS、
讀前須知:PPK寫這篇文章的時候,IPhone還沒有生產出4S之後的產品。所以,這篇文章中提到的IPhone,都是指IPhone4S及之前的手機。TOP This pag
先給最終效果圖:當我們在最下邊的gallery中切換圖片時,上面的大圖片會自動切換,切換時有動畫效果哦,很簡單的一個程序,有待完善更多的功能!activity代碼:pac