Source: http://www.cnblogs.com/everhad/p/6298070.html
NOTE: 筆記,碎片式內容
控件
App界面的開主要就是使用View,或者稱為控件。View既繪制內容又響應輸入,輸入事件主要就是觸摸事件。
ViewTree
控件基類為View,而ViewGroup是其子類。ViewGroup可以包含其它View作為其child。任何一個ViewGroup及其所有直接或間接的child形成一個ViewTree這樣的樹結構。
RootView
顯然每一個具體的ViewTree都會有一個root,它是一個ViewGroup,接下來稱它為RootView。持有一個RootView就可以引用此ViewTree,最終訪問到所有View。
以Activity為例,使用setContentView(View view)來指定要顯示的內容,不過參數view并非是Activity最終顯示到Window的ViewTree。通過追溯源碼,最終參數view被添加到PhoneWindow.mDecor作為其childView。mDecor是FramLayout的子類對象:
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
private final class DecorView extends FrameLayout {...}
可見mDecor是Activity最終顯示的ViewTree的root。
結構特點
ViewTree的特點有:
- 只有一個RootView,它是ViewGroup。
- ViewTree中的非葉子節點都是ViewGroup。
- ViewTree中的葉子節點可以是View或ViewGroup。
- 一個ViewGroup可以有0或多個直接childView。
- 一個childView只能有一個直接ViewGroup。
直接或間接的parent和child關系是關于具體2個View而言的,而這個關系在界面中的反映就是View顯示區域的包含關系,即child總是在parent的區域內。顯示區域的包含關系和它們在ViewTree中的結構關系是對應的。
對于組成ViewTree的所有ViewGroup和View來說,View不需要知道其所在ViewGroup,但ViewGroup知道其所有childView。
路徑
這里為ViewTree引入路徑這一概念,它表示從RootView出發找到任一child時要經過的所有View的列表。
因為一個ViewGroup只能訪問其直接child,而一個child只有唯一的parent,所以從RootView到達任一child的路徑是唯一的,反之從任一child到達RootView的路線也是唯一的。
顯然對任何child的路徑總是存在的,雖然可以依靠額外的數據結構來保存各個View的關系,但樹結構本身已經在做這樣的事情了。
View系統的底層原理
View系統是framework層提供給應用開發者的一種方便開發界面的框架,類似其它編程平臺中的控件系統那樣。
Android底層使用WindowManagerService(簡稱WMS)、Surface、InputManagerService(簡稱IMS)這些服務組件和類型來管理界面顯示和輸入事件的。這里簡單地對View系統的顯示和輸入事件的獲取進行探索。
使用View和Window來顯示界面
Window是像Activity、Dialog、PopupWindow這樣的獨立顯示和交互的界面的抽象。
可以像下面這樣將一個View顯示到新窗口:
private void newFloatingWindow() {
final WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
final Button button = new Button(this);
button.setText("ClickToDismiss");
LayoutParams lp = new LayoutParams();
lp.height = LayoutParams.WRAP_CONTENT;
lp.type = LayoutParams.TYPE_PHONE;
lp.flags = LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL;
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
wm.removeViewImmediate(button);
}
});
wm.addView(button, lp);
}
上面使用WindowManager創建了一個Window并顯示傳遞的view。
通過追溯源碼:
WindowManager->WindowManagerImpl->WindowManagerGlobal
可以看到最終addView()的執行是:
ViewRootImpl.setView(View view, WindowManager.LayoutParams attrs, View panelParentView)
。
實際上,ViewRootImpl和WMS通信來完成所有實際工作:創建窗口,對View的繪制和事件分發。
NOTE:
newFloatingWindow()的調用可以在非主線程中,僅要求線程Looper設置ok。這樣onClick()回調也就在對應的子線程中。不過View對象為了性能其代碼實現是非線程安全的,所以不允許其創建和修改在不同的線程中,所以,最方便的就是在主線程中創建View,之后其它線程可以轉到主線程中去繼續操作View。否則不同View在不同線程中操作是十分混亂的。要知道,main線程是唯一且必一直存在的。
ViewRootImpl
ViewRootImpl的知識比較多,這里對它進行一個感性的介紹,便于理解文章中對它的引用。
ViewRootImpl.mView字段就是要顯示的窗口的ViewTree的RootView。
ViewRootImpl作為ViewTree的管理者,它和WMS通信完成各種“底層”操作。
作用包括:
- 執行ViewTree的繪制。主動發起或是響應requestLayout()或invalidate()而執行performTraversals()/scheduleTraversals()來對ViewTree執行遍歷操作,即測量、布局和繪制。
- 分發InputEvent給ViewTree。
在“將Root添加到Window通知WMS顯示”時,執行performTraversals()中會調用View.dispatchAttachedToWindow(AttachInfo info, int visibility
將AttachInfo指定給mView,而ViewGroup會遍歷childViews遞歸此調用。總之,每個ViewTree的View也會持有其添加到的Window的信息,其中就包含了關聯的ViewRootImpl對象。
NOTE:
一般想知道一些方法的調用時序的話,可以在可重寫的方法中打印其StackTrace信息查看方法的調用棧。除了那些跨進程IPC調用,或者Handler方式的async調用。
示例ViewTree:MyTree
這里給出一個構成界面的ViewTree的示例,它將作為后續討論的例子。為了描述方便,將此ViewTree稱作“MyTree”。
界面效果:
圖1:示例界面
對應的ViewTree結構:
圖2:ViewTree結構
ViewTree事件來源
ViewRootImpl接收來自WMS的InputEvent事件,然后調用ViewRootImpl.mView(也就是構成界面的ViewTree的RootView)的View.dispatchPointerEvent(InputEvent event)
來向ViewTree傳遞一個MotionEvent event對象。
所以這就是ViewTree事件來源。
InputEvent主要是KeyEvent和MotionEvent,本文僅討論后者。
ViewRootImpl從獲得WMS的InputEvent,到分發給mView這里有一個過程,分兩個部分。
InputEvent的接收
ViewRootImpl使用一個InputEventReceiver對象獲得WMS發送的事件,在onInputEvent(InputEvent event)回調中,它執行enqueueInputEvent(event, this, 0, true)將事件添加到一個鏈表,這樣對事件的deliver是保證順序的!分發InputEvent
過程稍微復雜,因為使用了InputStage組成的一個"input pipeline"來處理InputEvent事件。
其中一個階段就是將MotionEvent傳遞給mView。
/**
* Base class for implementing a stage in the chain of responsibility
* for processing input events.
* <p>
* Events are delivered to the stage by the {@link #deliver} method. The stage
* then has the choice of finishing the event or forwarding it to the next stage.
* </p>
*/
abstract class InputStage {...}
ViewRootImpl對事件的分發過程是在主線程中的(它的創建線程和其使用MessageQueue接收事件決定的),而且每次會分發其收到的所有消息。
所以在App的消息循環模型中,響應用戶操作后對UI的改動,全部會一次性得到執行。之后在下一次主線程下一次Message處理中響應invalidate()/requestLayout()操作進行ViewTree遍歷。
對于一個ViewTree而言,只需要關心輸入事件是從RootView那里傳入的事實即可。
觸摸操作和觸摸點
用戶第一個手指按下和最終所有手指完全離開屏幕的過程為一次觸摸操作,每次操作都可歸類為不同觸摸模式(touch pattern),被定義為不同的手勢。
每個觸屏的手指——或者稱觸摸點被稱作一個pointer
,即一次觸摸過程涉及一或多個pointer。
這里聲明以下概念:
- 任意一個pointer的按下定義為down事件;
- 任意一個pointer的移動定義為move事件;
- 任意一個pointer的抬起定義為up事件;
第一個down事件,意味著觸摸操作的開始,最后一個up事件意味著觸摸操作的結束。開始和結束時的pointer可以不是同一個。
事件序列
一次手勢操作過程中每個觸摸點都在其down->move->up過程中產生一系列事件,每個觸摸點產生的所有事件為一個獨立的事件序列。
- 事件?
事件
這一概念在代碼中是一個用來攜帶數據的類型,它描述發生了什么。類似消息這樣的概念,是數據對象而非業務對象。
View.dispatchTouchEvent
在代碼中,ViewRootImpl調用RootView的View.dispatchPointerEvent(MotionEvent event)將事件傳遞給RootView。
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
對于觸摸事件event.isTouchEvent()為true,所以執行dispatchTouchEvent(event),方法原型:
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event)
方法返回一個boolean值,表示是否此View對象是否處理了傳遞的事件。
傳遞觸摸事件到一個view對象,就是調用其View.dispatchTouchEvent(MotionEvent event)。
對方法View.dispatchTouchEvent()的調用一方面傳遞事件給view,其返回結果又表明了此view是否處理了事件。
事件傳遞
View和ViewGroup兩個類對dispatchTouchEvent()方法提供了不同的實現。
ViewGroup的實現是,因為其含有child,它會根據一定的規則選擇調用child.dispatchTouchEvent()將事件傳遞給child,或者不。當ViewGroup.dispatchTouchEvent()中執行了對child.dispatchTouchEvent()的調用時,那么事件就經由此ViewGroup到達了child。若child依然是ViewGroup,那么可能繼續傳遞事件給其child。
所以,ViewGroup的dispatchTouchEvent()方法使得多個View對象形成了dispatchTouchEvent()方法的調用棧。這樣事件參數得到傳遞,而且,返回值也會在方法調用不斷返回時向上返回。
View不包含child,所以不會有調用child.dispatchTouchEvent()的操作,它作為dispatchTouchEvent()傳遞調用的終點。
基于它們的實現,事件參數從RootView的dispatchTouchEvent()方法的調用開始,會沿著ViewTree的一個路徑不斷傳遞給下一個child——也就是調用child的dispatchTouchEvent()。
以上就是View和ViewGroup的dispatchTouchEvent()方法使得ViewTree產生事件傳遞的原理。
事件序列傳遞給View的規則
作為事件序列的第一個事件down,dispatchTouchEvent()對它殊性處理,dispatchTouchEvent()傳遞調用時,任何view若返回true,則表示它處理了down事件,那么后續事件會繼續傳遞給它。如果某個view返回false,那么調用的傳遞在它這里終止,后續事件也不會再傳遞給它。
實際上也只在傳遞down事件時,ViewGroup才會采取一定規則來決定是否傳遞事件給child。
并且它使用TouchTarget類來保存可能的傳遞目標,作為后續事件傳遞的依據,后續的事件不再應用down事件那樣的規則。這反映的是事件序列的連續性原則,一個view處理了down事件那么它一定收到后續事件,否則不再傳遞事件給它。可見down事件傳遞完成后會確定下后續事件傳遞的路徑。
NOTE:
一個View收到并處理某個觸摸點的down事件后,那么即便之后觸摸點移動到View之外,或在View的范圍之外離開屏幕,此View也會收到相應的move、up事件,不過收到的事件中觸摸點的(x,y)坐標是在View的區域外。
有關down事件的傳遞細節和TouchTarget等概念,下面源碼分析時再詳細探索。
MotionEvent
上面對事件的描述都是概念上的,代碼中,觸摸事件由MotionEvent表示,它包含了當前事件類型和所有觸摸點的數據,產生事件時觸摸點坐標等。
事件拆分
ViewTree中,事件是經過parent到達child的。由于parent和child的一對多關系和顯示區域包含關系,一個ViewGroup可以先后收到兩個手指的按下操作,而這兩個觸摸點可以落在不同的child中,并且在不同的child來看都是第一個手指的按下。
可見child和parent所“應該”處理的觸摸點是不同的,那么傳遞給它們的事件數據也應該不一樣。
ViewGroup.setMotionEventSplittingEnabled(boolean split)可以用來設置一個ViewGroup對象是否啟用事件拆分,方法原型:
/**
* Enable or disable the splitting of MotionEvents to multiple children during touch event
* dispatch. This behavior is enabled by default for applications that target an
* SDK version of {@link Build.VERSION_CODES#HONEYCOMB} or newer.
*
* <p>When this option is enabled MotionEvents may be split and dispatched to different child
* views depending on where each pointer initially went down. This allows for user interactions
* such as scrolling two panes of content independently, chording of buttons, and performing
* independent gestures on different pieces of content.
*
* @param split <code>true</code> to allow MotionEvents to be split and dispatched to multiple
* child views. <code>false</code> to only allow one child view to be the target of
* any MotionEvent received by this ViewGroup.
* @attr ref android.R.styleable#ViewGroup_splitMotionEvents
*/
public void setMotionEventSplittingEnabled(boolean split);
若不開啟拆分,那么第一個觸摸點落在哪個child中,之后所有觸摸點的事件都發送給此view。若開啟,每個觸摸點落在哪個view中,其事件序列就發送給此child。而且因為RootView收到的事件總是包含了所有觸摸到數據,所以非第一個觸摸點操作時,第一個觸摸點收到“拆分后得到的move事件”。
因為ViewGroup處理的pointer的數量肯定是大于等于所有child處理的pointer的數量的,特別的,傳遞給RootView的事件肯定包含所有觸摸點的數據。但child只處理它感興趣的觸摸點的事件——就是down事件發生在自身顯示范圍內的那些pointer。
事件拆分可以讓ViewGroup將要分發的事件根據其pointer按下時所屬的child進行拆分,然后把拆分后的事件分別發送給不同child。child收到的事件只包含它所處理的pointer的數據,而不含不相干的pointer的事件數據。
最初的MotionEvent中攜帶所有觸摸點數據是為了便于一些view同時根據多個觸摸點進行手勢判斷。而事件拆分目的是讓不同的view可以同時處理不同的事件序列——從原事件序列中分離出來的,以允許不同內容區域同時處理自己的手勢。
事件類型
action表示事件的動作類型,即上面描述的down、move、up等,不過MotionEvent類提供了更詳細的劃分。
MotionEvent.getAction()返回一個int值,它包含了兩部分信息:action和產生此事件的觸摸點的pointerIndex。
/**
* Return the kind of action being performed.
* Consider using {@link #getActionMasked} and {@link #getActionIndex} to retrieve
* the separate masked action and pointer index.
* @return The action, such as {@link #ACTION_DOWN} or
* the combination of {@link #ACTION_POINTER_DOWN} with a shifted pointer index.
*/
public final int getAction();
實際的動作類型應該通過getActionMasked()來獲得。
當一個View處理多個觸摸點的事件序列時,觸摸點產生不同事件過程是:
- 用戶第一個手指按下,產生ACTION_DOWN事件。
- 其它手指按下,觸發ACTION_POINTER_DOWN。
- 任何手指的移動,觸發ACTION_MOVE。
- 非最后一個手指離開,觸發ACTION_POINTER_UP。
- 最好一個手指離開,觸發ACTION_UP。
- 收到ACTION_CANCEL,例如View被移除、彈框、界面切換等引起的View突然不可見。此時收到cancel事件,終止一次手勢。
pointerIndex和pointerId
一個MotionEvent對象中記錄了當前View所處理的所有觸摸點(1或多個)的數據。
在MotionEvent中,pointerId是觸摸點的唯一標識,每根手指按下至離開期間其pointerId是不變的,所以可以用來在一次事件序列中用來連續訪問某個觸摸點的數據。
pointerIndex是當前觸摸點在數據集合中的索引,需要先根據pointerId得到其pointerIndex,再根據pointerIndex來調用“以它為參數的各種方法”來獲取MotionEvent中此觸摸點的各種屬性值,如x,y坐標等。
NOTE:
出于性能的考慮,多個move事件會被batch到一個MotionEvent對象,可以使用getHistorical**()
等方法來訪問最近的其它move事件的數據。
源碼分析
經過上面的“理論描述”,可以獲得View系統事件處理的一個整體認識。接下來分析View、ViewGroup中如何實現這些設計的。
源碼:View.dispatchTouchEvent
View.dispatchTouchEvent()中不涉及事件傳遞,它只能自己處理事件。
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
final int actionMasked = event.getActionMasked();
...
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;
}
操作如下:
- 調用OnTouchListener.onTouch(),傳遞事件給外部監聽者。
- 若監聽器未處理,則將事件交給自身的onTouch()去處理。
note:對OnTouchListener調用需要view的enabled=true,即為激活狀態。而onTouchEvent()的調用受enabled狀態的影響。
源碼:ViewGroup.dispatchTouchEvent
首先需要理解TouchTarget的概念。
TouchTarget
當一個觸摸點的down事件被某個child處理時,ViewGroup使用一個TouchTarget對象來保存child和pointer的對應關系。此pointer的后續事件就直接根據發給此TouchTarget中的child處理,因為down事件決定了整個事件序列的接收者。
因為TouchTarget記錄了接收后續觸摸點事件的child,而后事件將傳遞給它們,所以可以稱它為派發目標。
TouchTarget是ViewGroup的靜態內部類:
private static final class TouchTarget {
// 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;
...
}
字段pointerIdBits存儲了一個child處理的所有觸摸點的id信息,使用了bit mask技巧。比如id = n (pointer ids are always in the range 0..31 )那么pointerIdBits = 1 << n
。
因為ViewGroup中可以是多個child接收不同的pointer的事件序列,所以它將TouchTarget設計為一個鏈表節點的結構,它使用字段mFirstTouchTarget來引用一個TouchTarget鏈表來記錄一次觸屏操作中的所有派發目標。
// First touch target in the linked list of touch targets.
private TouchTarget mFirstTouchTarget;
ACTION_CANCEL
一般的,一個觸摸點的序列遵循down-move-up這樣的序列,但如果在down或者move之后,突然發生界面切換或者類似view被移除,不可見等情況,那么此時觸摸點不會收的“正常”情況下的up事件,取而代之的是來自parent的一個ACTION_CANCEL類型的事件。
此時child應該以“取消”的形式終止對一次事件序列的處理,如返回之前狀態等。
整體過程
方法的整體操作過程如下:
- ACTION_DOWN產生時重置狀態,準備迎接新觸屏操作的處理。主要就是清除上次事件派發用到的派發目標。
- 在down事件時確定pointer的派發目標。
- 根據派發目標,派發事件給child。
- 在up事件時移除對應view處理的觸摸點。
初始化操作
ACTION_DOWN意味著一次新觸摸操作的的事件序列的開始,即第一個手指按下。
這時就需要重置View的觸摸狀態,清除上一次跟蹤的觸摸點的TouchTarget列表。
// 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.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
攔截事件
ViewGroup的設計思路是優先傳遞事件給child去處理,但child的設計是不考慮其parent——不現實,
所以為了避免child返回true優先拿走parent期望去先處理的事件序列,可以重寫onInterceptTouchEvent()來根據自身狀態(也可以包含child的狀態判斷)選擇攔截事件序列。注意onInterceptTouchEvent()只能用返回值通知dispatchTouchEvent()傳遞過程需要攔截的意思,但對事件的處理是onTouchEvent()中或者OnTouchListener——和View中的處理一樣。
// Check for interception.
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;
}
onInterceptTouchEvent()的默認實現返回false——即不攔截,而子類根據需要在一些狀態下時攔截DOWN事件。
同時,ViewGroup提供了方法requestDisallowInterceptTouchEvent(boolean disallowIntercept)
供childView申請parent不要攔截某些事件。ViewGroup會傳遞此方法到上級parent,使得整個路徑上的parent收到通知,不去攔截發送給child的一個事件序列。
一般child在onInterceptTouchEvent或onTouchEvent中已經確定要處理一個事件序列時(往往是在ACTION_MOVE中判斷出了自己關注的手勢)就調用此方法確保parent不打斷正在處理的事件序列。
處理down事件:確定派發目標
在ACTION_DOWN或ACTION_POINTER_DOWN產生時,顯然一個新的觸摸點按下了,此時ViewGroup需要確定接收此down事件的child,并且將pointerId關聯給child。
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
...
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.
final ArrayList<View> preorderedList = 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 (!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;
}
}
}
上面的方法主要工作:
- 根據x,y位置,根據繪制順序“后繪制的在上”的假設對children執行倒序遍歷,找到顯示區域包含事件且可以接收事件的第一個child,因為處理的是down事件,它將作為此pointer的TouchTarget。
- 遍歷過程中,若child已經在mFirstTouchTarget所記錄的鏈表中,那么將pointerId增加給它。此時事件未派發,等待后面根據TouchTarget進行派發。
- 調用
dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
將down事件派發給child,若child處理了事件,那么它作為此pointer的TouchTarget,被添加到mFirstTouchTarget鏈表。 - 如果沒找到newTouchTarget,ViewGroup會選擇將pointer綁定到最近處理觸摸點的那個child——還是不自己處理。
NOTE:
方法dispatchTransformedTouchEvent()在檢查child是否處理事件的過程中同時已經完成了事件的派發,所以變量alreadyDispatchedToNewTouchTarget用來記錄當前event是否已經派發。
split變量表示是否對事件拆分,根據前面的理論知識,不拆分那么整個觸屏操作過程所有的觸摸點的所有事件只會發給第一個接收ACTION_DOWN的view。拆分的話,每個觸摸點的事件都是一個單獨的事件序列,發送給不同的處理它們的child。
無論事件拆分與否,若觸摸點沒有找到合適的child去處理,而已經有child在處理之前的觸摸點,那么ViewGroup還是選擇將事件交給已經處理事件的child,因為有理由相信它在處理多點觸摸事件,而后續觸摸點是整個手勢的一部分。
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);
parent在傳遞事件給child前將坐標轉換為child坐標空間下的,即對x,y進行偏移。
若child=null,則意味著ViewGroup自己處理事件,那么它以父類View.dispatchTouchEvent()的方式處理事件。
參數desiredPointerIdBits中使用位標記的方式記錄了此child處理的那些pointer,所有參數event在真正傳遞給child時會調用MotionEvent.split()來獲得僅包含這些pointerId的那些數據。也就是拆分后的子序列的事件。
派發事件
只有down事件會產生一個確定派發目標的過程。之后,pointer已經和某個child通過TouchTarget進行關聯,后續事件只需要根據mFirstTouchTarget鏈表找到接收當前事件的child,然后分發給它即可。
// 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;
if (alreadyDispatchedToNewTouchTarget && 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;
}
}
若mFirstTouchTarget=null說明沒有child處理事件,那么ViewGroup自己處理事件。
傳遞給dispatchTransformedTouchEvent()的參數child==null。
否則,就循環mFirstTouchTarget鏈表,因為event中是包含了所有pointer的數據的,在
dispatchTransformedTouchEvent()中,會根據target.pointerIdBits對事件進行拆分,只發送包含對應pointerId的那些事件數據給target.child。
處理up/cancel事件
每個pointer的ACTION_UP和ACTION_CANCEL事件意味著其事件序列的終止。
此時在傳遞事件給child之后,應該從mFirstTouchTarget鏈表中移除包含這些pointerId的那些派發目標。
// 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);
}
自己處理事件
在mFirstTouchTarget鏈表為空時,ViewGroup自己處理事件。
它通過傳遞給dispatchTransformedTouchEvent()的child參數為null來表示這一點。
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
之后在上面的調用方法中:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
}
因為ViewGroup的父類就是View,所以super.dispatchTouchEvent(transformedEvent)其實就是執行了
View.dispatchTouchEvent(),這時ViewGroup以普通View的方式自己處理事件。
流程總結
設計理論
- MotionEvent
- dispatchTouchEvent
- MotionEvent.split
- TouchTarget
- onInterceptTouchEvent
- disallowIntercept
- OnTouchListener和onTouchEvent
流程
View
通知OnTouchListener去處理;
不處理?
自己的onTouchEvent()處理。
dispatchTouchEvent()返回true?繼續處理后續事件;
false?不再收到后續事件。ViewGroup
child讓你攔截嗎,onInterceptTouchEvent()自己攔截嗎?
不攔截?——找TouchTarget;傳遞給child。
找不到child?攔截?——自己處理。
dispatchTouchEvent()返回true?繼續處理后續事件;
false?不再收到后續事件。
補充
不要重寫dispatchTouchEvent
可以看到,從View系統的設計原則上看,View和ViewGroup對dispatchTouchEvent()的不同實現形成了View事件的傳遞機制。
如果需要在ViewGroup中攔截處理事件,那么應該配合使用onInterceptTouchEvent()和requestDisallowInterceptTouchEvent()。ACTION_MOVE中的getAction()
此時action中不包含pointerIndex信息,其實只有ACTION_POINTER_UP和
ACTION_POINTER_DOWN的action才需要保護pointerIndex信息,因為此時pointerCount>1。攔截和不攔截
在正常的事件傳遞行為中補充了parent的優先處理和child的優先處理的動作。
向上傳遞child的反對攔截的請求。
在onTouchEvent中做處理,而不是在onInterceptTouchEvent中。
明確各個方法的職責。
資料
- MotionEvent和手勢識別
http://www.cnblogs.com/everhad/p/6075716.html
(本文使用Atom編寫)
![]() |
不含病毒。www.avast.com |