事件體系簡介
既然是View的事件分發,那么首先我們應該知道什么是Android體系中的事件,它在code世界中又是如何定義的,下面我們簡單介紹一些MotionEvent的知識。
MotionEvent
Android體系中將所有的輸入事件都放到了MotionEvent中,實際上MotionEvent已經發展到十分復雜,由于本文的重點不在這里,所以就單點觸控事件進行介紹。
單點觸控
單點觸控中有以下幾個事件:
- ACTION_DOWN 手指初次接觸到屏幕上時觸發,簡單地說就是手指按下觸摸到屏幕的時候
- ACTION_UP 手指離開屏幕時觸發,及手指抬起離開屏幕時
- ACTION_MOVE 手指在屏幕上滑動時觸發,當手指在屏幕上連續一段距離后被識別為滑動,滑動在一次事件序列中可能會被觸發多次,并不唯一
- ACTION_CANCEL 當事件傳遞下來后又被上層收回時觸發
- ACTION_OUTSIDE 手指觸控的區域不在控件上方時觸發
一次單點觸控流程可以大概描述為手指按下(ACTION_DOWN)——>手指在屏幕上滑動(ACTION_MOVE)——>最后手指離開屏幕(ACTION_UP),這是用戶一般使用的簡化過程。了解過這些基礎事件后,我們再看幾個MotionEvent常用API:
- getAction() 獲取上述事件類型
- getX() 獲取觸摸點相對于View的X軸坐標
- getY() 獲取觸摸點相對于View的Y軸坐標
- getRewX() 獲取觸摸點相對屏幕的X軸坐標
- getRewY() 獲取觸摸點相對屏幕的Y軸坐標
上面介紹的知識中首先要強調的是ACTION_MOVE在一次事件流程中可能觸發的次數不唯一,其次就要介紹一下比較冷門的兩個事件ACTION_CANCEL,ACTION_OUTSIDE。
- ACTION_CANCEL
ACTION_CANCEL顧名思義取消事件,既然叫做取消那么就肯定存在先獲取然后被收回這樣一個過程,舉個例子:你收到了一個會議消息,然后你又被告知這個會議被取消了;對應于View事件分發就是指事件序列分發到了某個環節,但他的上層又收回了這個事件序列,這時這個環節就會收到上層給他的ACTION_CANCEL事件.
- ACTION_OUTSIDE
對于這個事件可能大家會覺得不可思議,明明觸摸事件的發生點都不在該控件的所在區域,看起來不相關的兩者是如何聯系到一起的呢。實際上這種情況的確存在而且也具有它的意義。比如我們常用的Dialog,是不是會有這樣的情況,我們點擊它本身的關閉或取消按鈕,而是點了一下它周圍的其他區域,Dialog依然能夠關閉;這種情況很多彈窗上出現的很多,就是這個ACTION_OUTSIDE事件的作用。
對于Dialog是如何收到這個ACTION_OUTSIDE事件的,這主要是通過WindowManager的布局參數的flags設置為FLAG_WATCH_OUTSIDE_TOUCH,這樣Daialog就具有了觀察并處理超出本身范圍的事件的能力
正文
Android事件分發從當前Activity開始,在到該Activity持有的Window,又由Window將事件傳遞給DecorView,最后又由DecorView再將事件分發給子View;如果子View并不消費該事件,那么該事件序列又會被交給該View的父容器,再到DecorView,再到Window,最后又回到了Activity。這意味著如果一直沒有View消費該事件序列,那么事件會回到Activity并被處理掉。
當不考慮事件序列中途被攔截并消費,只考慮事件分發到View的情況,比如一個Button或者一個TextView。若該View并沒有消費該事件的話,那么最多就會出現上述流程
Activity的事件分發
Activity的事件分發在其dispatchTouchEvent方法里進行了處理,我們看源碼:
public boolean dispatchTouchEvent(MotionEvent ev){
if(ev.getAction() == MotionEvent.ACTION.DOWN){
onUserInteraction();
}
if(getWindow().superDispatchTouchEvent(ev)){
return true;
}
return onTouchEvent(ev);
}
這里我們可以看到Activity直接將事件交給了Window處理,如果Window的superDispatchTouchEvent方法返回false,那么Activity就會調用他的onTouchEvent方法,這個里如何具體實現我們后面再看。那么事件已經到了Window了。
Window的事件分發
我們知道Window是抽象類,唯一實現類是PhoneWindow,又因為該superDispatchTouchEvent是抽象方法,那么我們只有去PhoneWindow里去看該方法的實現,看源碼:
public boolean superDispatchTouchEvent(MotionEvent ev){
return mDecor.dispatchTouchEvent(ev);
}
相比于Activity將事件傳到Window來說,Window的做法則要更加簡單粗暴,這里直接傳遞到了DecorView,而這個DecorView就是我們一般說的頂級View,我們在layout包里寫的xml文件就是它的子View。一般來說這個DecorView都是一個ViewGroup,所以事件序列來到了ViewGroup,我們繼續看。
ViewGroup的事件分發
ViewGroup大家應該已經比熟悉了,一般來說ViewGroup的很多邏輯
處理都跟其自身和其子View息息相關,就像View的measure、draw和layout方法類似,這里也很相似,首先我們來看看幾個比較重要的方法:
public boolean dispatchTouchEvent(MotionEvent ev)
用于事件的分發。當事件傳遞到了該View后,該方法一定會被調用,上面關于Activity等的事件分發時,大家應該都看到了這個方法。該方法的返回值由其自身的onTouchEvent()方法和子View的dispatchTouchEvent方法共同決定。
public boolean onInterceptTouchEvent(MotionEvent ev)
此方法會在上述方法內部被調用(View為ViewGroup時),具體的View不會調用該方法。此方法用于判斷是否攔截某個事件,返回結果表示是否攔截當前事件。當同一個事件序列中的某個事件被攔截后,此方法不會被再次調用。
public boolean onTouchEvent(MotionEvent ev)
用于處理點擊事件,返回結果用于判斷是否消費事件,如果不消費,那么此view不會再收到該事件序列的其他事件。
下面我們結合ViewGroup的源碼來分析它的事件分發機制,首先看dispatchTouchEvent方法,源碼內容很長,我們一段一段的分析:
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down. 注釋1
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.注釋2
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.注釋3
intercepted = true;
}
這里定義了一個名為handled的布爾值,根據命名我們可以猜測為是否處理的,顯然事件還沒有被處理,這也跟我們說的viewGroup的dispatchEvent方法默認不攔截事件相應證,然后再看注釋1處,如果當前事件是ACTION_DOWN(下文簡稱down事件)的話,會調用兩個方法,又看命名,第一個方法應該是清除并取消觸摸的目標,第二個方法應該是重置觸摸狀態,那么到底與我們的猜測是否一致呢,通過查看這兩個方法的源碼和注釋,與我們的猜測完全一致:
/**
* Cancels and clears all touch targets.
* 取消并清除所有觸摸目標
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
//mFirstTouchTarget 一個成員變量,初始為null,如果事件被子view處理完后
//這個值將被賦值并指向子view,
if (mFirstTouchTarget != null) {
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 &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
再次回,到dispatchTouchEvent方法上來,看注釋2處又做了什么,這里有定義了一個布爾值,表示是否攔截事件,然后的判斷內容是如果事件down或者mFirstTouchTarget不為空的話則執行onInterceptTouchEvent方法,是不是down事件很好判斷,前面說到mFirstTouchTarget在子view處理了事件后會被賦值,而想要交給viewgroup處理時,顯然它等于null。所以onInterceptTouchEvent方法只會執行一次在這里就可以看出,并且當down事件被攔截后,之后的整個事件序列都會交給他處理(注釋3)。但是在調用onIntercepTouchEvent方法之前還有一個叫disallowIntercept的布爾值,用于判斷是否禁止攔截,主要受FLAG_DISALLOW_INTERCEPT這個標志位的影響,因為這個標志位子view可以通過一定方法來改變,使viewgroup不攔截該事件并將它傳遞到子view上,這聽起來似乎可以用來解決滑動沖突,但是這得要求這個事件不是down事件,上文已經分析過了,因為此時會進行一系列重置工作,包括標記位。如果down事件并沒有攔截呢,那么這個時候就要將事件傳遞給子View了(這可能會有點長):
//獲得所有子view的數量
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.
//創建了一個用于向子view分發touch事件的list
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//遍歷子view
for (int i = childrenCount - 1; i >= 0; i--) {
//獲取子view的腳標
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
//通過腳標拿到子view對象
final View child = getAndVerifyPreorderedView(
preorderedList, children, 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;
}
//如果view在播放動畫或者點擊事件的坐標不在該view區域內 則遍歷下一個
//否則交由它處理
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//從單鏈表中拿到這個值為child的TouchTarget對象
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);
//調用dispatchTransformedTouchEvent方法,用于判斷子view是否攔截事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
//記錄時間
mLastTouchDownTime = ev.getDownTime();
//記錄子view的腳標
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();
//為新觸摸目標賦值 將child添加進鏈表并返回值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//已經分發給新的觸摸目標設置為true
alreadyDispatchedToNewTouchTarget = true;
//跳出循環,因為處理事件的view已經有了
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
//清空存放view的list
if (preorderedList != null) preorderedList.clear();
}
上面這一大片代碼主要完成了遍歷子View,然后判斷子view是否能夠接受到事件,如果能則將事件分發給子view,如果子view攔截該事件,就記錄下它然后設置為newToucthTarget。判斷子view是否攔截是dispatchTransformedTouchEvent這個方法來完成的,我們看看是怎么實現的:
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) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
···
}
這里只截取了最主要的一段代碼,可以看到如果子view不為空,就調用子view的dispatchTouchEvent方法,否則調用父類View的dispatchTouchEvent方法,最后返回結果。如果子view攔截了事件返回true,那么viewgroup就會記錄下它,并為newTouchTarget賦值,然后跳出循環不再遍歷,這一點參見上面代碼中的注釋。到這里還會出現另一種情況,那就是子View的dispatchTouchEvent都返回了false,整個遍歷過程都沒有找到view來處理事件怎么辦呢,顯然這時候只能viewgroup自己來處理事件了,看看它是如何處理的:
// 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);
}
這里又調用了dispatchTransformedTouchEvent方法,需要注意的是傳入的child參數為null,根據上面的分析,這里會將事件交給View來處理,這就是Viewgroup的處理邏輯,下面再看view的事件處理過程是怎樣的。
View的事件分發
view相比ViewGroup沒有onInterceptTouchEvent方法,一點有事件傳遞到它,那么它的onTouchEvent方法就一定會被調用。view的onTouchEvent方法默認會消費事件,除非它是不可點擊的,clickable和longClickable同時為false,所有view的longClickable默認都為false,,clickable就各不相同了,比如Button的clickable默認為true,TextView默認為false;但是當我們為TextView設置點擊事件監聽的時候,它依然能夠消費事件,這主要是因為setOnClickListener的時候,會將clickable設置為true。下面來看看view的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//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不為null,并且是可以點擊的則執行它的onTouch方法,如果onTouch返回true,那么事件處理就結束了,就不會執行下面的onTouchEvent方法,顯然onTouchListener優先級高于onTouchEvent。然后我們再看onTouchEvent:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {//注釋1
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
//注釋2
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
}
看到注釋1處,當view處于不可用狀態下,依然能處理點擊事件,就像源碼注釋寫的那樣,這些view并不會響應,但依然消費了觸摸事件。注釋2處是view的代理來處理點擊事件,處理機制與OnTouchListener相似,就不分析了。現在來看看在onTouchEvent中對具體的事件是如何處理的:
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
//如果根本就是不可點擊 那么設置的相關callback都會被移除
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
...
}
...
break;
}
...
return true;
}
從上面的代碼可以看出只要CLICKABLE和LONG_CLICKABLE有一個為true,即為可點擊狀態,那么它就會消費這個事件。點擊事件的觸發在action_up這個事件,這個事件里可以看出主要調用了performClickInternal方法,其實這個方法也主要是調用的performClick方法,如果view設置了onClickListener,那么就會調用它的onClick方法:
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
一開始我們就說過,在設置clickListener的時候,clickable會被設置為true,所以當我們為View設置OnClickListener或OnClickListener的時候,只要事件傳遞到它這里來,那么就會執行我們設置的邏輯。到這里View的事件分發也就講完了,對事件分發的清楚認識對于滑動沖突的處理和自定義view都有極大的幫助。