☞閱讀原文
- 情境(Situation)
- 衝突(Complication)
- 疑問(Question)
- 答案(Answer)
- 剖析
- 論點
- 約法三章
- 點
- 論據
- 人機交互
- View樹
- 類圖
- 註釋
- DecorView
- WindowCallbackWrapper
- Activity
- PhoneWindow
- ViewGroup
- View
- 事件流
- 論點
- 論證
- 剖析
- 一張圖
- 標準
- 常見錯誤
- 最佳實踐
- 漁
- 方法論
- 利器
- 利
- 進階
- 參考
- 長歌
情境(Situation)
1. 專注於移動互聯網數年,作爲高P的我【鼓掌】竟然對事件分發機制見招拆招,似懂非懂。不專業,沒法忍。
2. View樹的遞歸嵌套邏輯讓廣大一線同行雲裏霧裏,手足無措。
衝突(Complication)
1. 網上好多相關主題的博客,描述信息點非常多(但是ACTION_CANCEL描述很少),看完後不明覺厲。
2. 事件分發主要用於解決自定義炫酷控件以及滑動嵌套引發的衝突問題(程序傻傻分不清是橫滑還是豎滑),發現同行各種寫法都有,雷無處不在【人在家中坐,鍋從天上來】。
我的機會來了
疑問(Question)
1. 有沒有體系化剖析套路?
2. 指出常見錯誤,給出最佳實踐?
3. 清晰明瞭的給出一張圖,便於查閱?
4. “魚”和“漁”可以兼得?
答案(Answer)
剖析
論點
約法三章
1. 限於個人水平,本文只包含單點觸控事件(ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL)。
2. Window類相關的我不會,膚淺的認爲和事件分發關係不大(求大牛點撥),直接跳過。
3. 一家之言,姑妄言之,姑妄聽之。
點
1. 事件流一致性保證(Consistency Guarantees):按下開始,中間可能伴隨着移動,鬆開或者取消結束。ACTION_DOWN -> ACTION_MOVE(*) -> ACTION_UP/ACTION_CANCEL。
2. View類的dispatchTouchEvent方法完成事件的消費處理,ViewGroup的dispatchTouchEvent方法完成事件的分發處理。正常情況下不建議重寫該方法改變系統事件分發機制。
3. ViewGroup類的onInterceptTouchEvent方法完成事件的攔截處理。事件分發路徑上的ViewGroup,在ACTION_DOWN或者不是自己直接消費事件時一定會調用onInterceptTouchEvent方法。
4. View類的onTouchEvent方法完成具體處理事件消費,即觸發點擊監聽(OnClickListener)和長時間點擊監聽(OnLongClickListener)以及按鍵狀態、焦點相關處理。
1. 如果設置了OnTouchListener,會先調用OnTouchListener,如果該監聽onTouch返回true,則不會調用onTouchEvent,直接返回已消費;
2. 如果設置了TouchDelegate ,onTouchEvent中會先調用TouchDelegate,如果該類onTouchEvent返回true,則直接返回已消費;
3. 如果View 可點擊,執行onTouchEvent中事件處理,並返回true;
1. ACTION_DOWN:置按鍵標誌位爲按下狀態,並觸發延時(500ms)執行長按點擊事件。
2. ACTION_MOVE:如果按鍵座標超出該控件區域,則置按鍵標誌位爲非按下狀態,並且移除ACTION_DOWN觸發的延時執行長按點擊事件。
3. ACTION_UP:如果按鍵標誌位爲按下狀態,並且ACTION_DOWN觸發的長按點擊事件還未執行,則移除長按點擊事件,執行點擊事件。
4. ACTION_CANCEL:置按鍵標誌位爲非按下狀態,移除ACTION_DOWN觸發的延時執行長按點擊事件。
4. 否則不可點擊,返回false;
論據
基於 Android 8.0 (API Level 28) 源碼解析
人機交互
賞析
用戶的按鍵行爲->手機傳感器->ViewRootImpl->DecorView->WindowCallbackWrapper->Activity->PhoneWindow->DecorView->ViewGroup*->View->程序員的代碼邏輯->硬件(顯示器、揚聲器等)響應輸出->用戶感知
View樹
賞析
1. View是由樹形結構組織,節點爲ViewGroup或者View。ViewGroup可以包含多個子節點,View沒有子節點。
2. Android中View樹的根節點爲DecorView(父View爲FrameLayout,屬於ViewGroup)。
3. Android中用戶可自定義的View子樹根節點id爲“android:id/content”。
{:.info}
類圖
賞析
1. ViewRootImpl是Android層邏輯起始點,用於接收來自系統底層的事件消息。相當於View管理類,本身不是View。(BTW:View繪製流程的三部曲(measure、layout、draw)也由該類觸發的。)
2. DecorView是Android View樹的根節點,持有window對象。本身能夠直接進行真正事件分發能力(繼承了父類ViewGroup和View的事件分發處理功能),但是事件分發會直接調用window,間接傳遞到Activity的事件分發,後續會由Activity回調DecorView的真正事件分發能力。對應圖中的環形依賴。
3. Activity是Android中的頁面,真正的事件分發由該類的dispatchTouchEvent觸發。(Easter Eggs:如果你想讓用戶操作不了你的界面,蒙一層透明的View是不是有點low,直接重寫該方法就可以控制。)
4. ViewGroup負責事件分發和攔截處理。按下事件和後續事件(移動、釋放或者取消)處理不相同。
1. 按下事件,先判斷是否攔截。
1. 如果不攔截的話,分發事件尋找目標消費子View(逆序遍歷子View,遞歸調用子View的事件分發,判斷是否有子View消費。mFirstTouchTarget存儲目標消費子View對象)。
1. 如果有子View消費,則目標子View消費事件。
2. 否則自己嘗試消費事件。
2. 否則直接自己嘗試消費事件。
2. 後續事件
1. 如果按下事件找到了目標消費子View,則判斷是否攔截,否則不攔截。
2. 如果有目標消費子View,則根據是否攔截。
1. 如果沒有攔截,正常傳送後續事件;
2. 如果有攔截,則當前事件轉換爲取消事件發送給目標消費子View,並且重置目標消費子View爲空,接下來的後續事件直接自己嘗試消費事件(不管是否消費,後續事件都會接收到&嘗試處理事件分發);
3. 否則自己嘗試消費事件。(不會調用是否攔截,其實攔截或者不攔截,都是自己消費事件。)
5. View負責事件消費事件處理。
1. 調用mOnTouchListener的onTouch。
1. 如果消費,直接返回true;
2. 否則,繼續調用onTouchEvent方法;
1. 如果爲啓用的(enable),返回可點擊(clickable)。
2. 否則,調用mTouchDelegate的onTouchEvent。
1. 如果消費,直接返回true;
2. 否則,
1. 如果可點擊(clickable)
1. 進行事件流(ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL)處理(包含焦點、按鍵狀態、按鍵和長時間按鍵);
2. 返回true。
2. 否則返回false;
註釋
DecorView
/**
* Decor的意思是:裝飾,佈置。
* View樹的根節點。
* 事件分發的啓點,ViewRootImpl最先調用dispatchPointerEvent(實現在父類View裏面)。
* 事件調用在DecorView裏面形成了一個環。(先通過Window交由Activity分發,Activity再調用DecorView中的真正事件分發方法)
*/
public class DecorView extends FrameLayout {
private PhoneWindow mWindow;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// DecorView直接覆蓋ViewGroup的事件分發實現,其實這只是饒了個圈,
// 正真的事件分發會由Activity回調到superDispatchTouchEvent(ViewGroup的事件分發處理)。
// 調用Window的WindowCallbackWrapper對象繼續分發。
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
public boolean superDispatchTouchEvent(MotionEvent event) {
// 調用父類ViewGroup進行事件分發處理。
return super.dispatchTouchEvent(event);
}
}
WindowCallbackWrapper
/**
* Wrapper的意思是包裝材料。
* 實實在在的一個殼,包裹着Activity。
*/
public class WindowCallbackWrapper implements Window.Callback {
final Window.Callback mWrapped;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
// 交給Callback(具體對象爲Activity)接力事件分發。
return mWrapped.dispatchTouchEvent(event);
}
}
Activity
/**
* Activity和View不一樣,Activity就是一個殼,沒有事件分發機制,View樹如果沒有消費,Activity撿個漏。
*/
public class Activity implements Window.Callback {
private Window mWindow;
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 交給Window(具體對象爲PhoneWindow)接力事件分發。
if (getWindow().superDispatchTouchEvent(ev)) {
// View樹消費掉事件
return true;
}
// 如果View樹沒有消費事件,Activity消費事件的機會來了。
// 啓示:如果View樹消費事件,在按下事件的後續事件中,如果父ViewGroup進行攔截,
// 雖然後續返回的消費狀態對整個事件流沒有影響,但是會對Activity有影響(View數不消費,Activity有機會消費)。
return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
// 事件消費處理,系統默認基本不幹啥
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
}
PhoneWindow
/**
* PhoneWindow也是一個殼,將事件轉回給DecorView分發處理。
*/
public class PhoneWindow extends Window {
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
// 交給DecorView接力事件分發(自此,環形結束,開始ViewGroup和View中事件分發和消費閃亮登場)。
return mDecor.superDispatchTouchEvent(event);
}
}
ViewGroup
/**
* ViewGroup,View容器的意思。
* dispatchTouchEvent完成時間分發邏輯。
* onInterceptTouchEvent:爲事件攔截接口,父控件可以主動截留事件自己消費,否則只能等子Viwe樹都不消費才能撿漏。【有控制權就是爸爸】
*/
public abstract class ViewGroup extends View implements ViewParent {
@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.
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;
// 攔截條件1,要麼是按下事件,要麼自己不直接消費事件。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 攔截條件2,允許攔截開關打開。
//(默認狀態是打開的,其他View可以調用requestDisallowInterceptTouchEvent進行控制,
// 多爲子View掉父View,滑動衝突外部攔截法就是靠調用這個接口控制父View攔截)。【爸爸的權利也不是絕對的】
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;
// 遞歸查找目標消費子View條件1:事件沒有被取消,也沒有被攔截
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;
// 遞歸查找目標消費子View條件2:事件必須是按下事件。【多點觸控的不討論,關鍵是我也不會】
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);
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 = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 逆序遍歷,後面的View後繪製,蓋在上面
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
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資格1:事件的座標在View區域內。
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);
// 消費事件View資格2:自己或者子View樹消費事件。進入遞歸事件分發。
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();
// 標記當前View爲目標消費子View,消費路徑上都是父View標記直接子View(下發分發不用再找了)。不存在跨級。
// 我也沒有搞明白爲啥整一個鏈式結構存目標消費子View。我沒有遇到多餘1個目標消費子View的情況。【看邏輯,如果有子View消費,則跳出循環,不會繼續分發】
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.
// 沒有目標子View消費,自己消費。(要麼自己攔截了,要麼子View樹沒有消費)
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;
// 如果是按下事件,則已消費,直接置消費狀態爲true
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 非按下事件,要麼持續正常處理消費,要麼被攔截(事件轉成取消事件,還是繼續分發給目標View)
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
// 如果是取消事件(要麼被攔截,要麼傳過來的就是取消事件),則清空目標消費子View。
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;
}
// 攔截處理
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
// 事件分發處理封裝部分邏輯的子方法,實現取消事件轉換
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;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
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);
}
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
}
View
public class View {
public final boolean dispatchPointerEvent(MotionEvent event) {
// View樹接收事件的起點,由ViewRootImpl調用DecorView的該方法開始,
// 接下來會調用到DecorView的dispatchTouchEvent方法。
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
// 事件消費處理
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// 優先mOnTouchListener消費處理,如果消費,直接返回已消費
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 自己處理消費,封裝在onTouchEvent內
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
// 針對完整事件流(ACTION\_DOWN -> ACTION\_MOVE(*) -> ACTION\_UP/ACTION\_CANCEL)完成按鍵監聽、長時間按鍵監聽、焦點以及按鍵狀態處理。
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) {
// 按鍵未啓用,直接返回點擊狀態。
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;
}
// 有效觸摸代理消費事件,可用於擴大點擊熱點控制。如果消費,直接返回已消費。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
// 可點擊情況下進行按鍵處理。
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
// 檢查按鍵標誌位狀態,只有爲按下狀態才接着處理。
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
// ACTION_DOWN觸發的長按點擊事件還未執行,則移除長按點擊事件,
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)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
// 置按鍵標誌位爲按下狀態,並觸發延時(500ms)執行長按點擊事件。
// 以下爲滾動和非滾動下的處理。
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
// 置按鍵標誌位爲非按下狀態,移除ACTION_DOWN觸發的延時執行長按點擊事件。
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
// Be lenient about moving outside of buttons
// 檢查按鍵座標是否超出該View區域。
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
// 置按鍵標誌位爲非按下狀態,並且移除ACTION\_DOWN觸發的延時執行長按點擊事件。
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
}
事件流
DemoParentInterceptTouchEventActivity頁面git倉庫
使用MECE(Mutually Exclusive Collectively Exhaustive,相互獨立,完全窮盡)法則
條件 | 結果 |
---|---|
1.父控件ACTION_DOWN攔截 2.父控件消費事件 |
1. 接收按下事件 -DOWN-> Parent.dispatchTouchEvent -DOWN-> Parent.onInterceptTouchEvent -true-> Parent.dispatchTouchEvent -DOWN-> Parent.(super)dispatchTouchEvent{Parent處理消費} -DOWN-> Parent.onTouchEvent -true-> Parent.dispatchTouchEvent-true-> 返回消費狀態true 2. 接收移動事件 -MOVE-> Parent.dispatchTouchEvent -MOVE-> Parent.(super)dispatchTouchEvent{Parent處理消費} -MOVE-> Parent.onTouchEvent -消費狀態-> Parent.dispatchTouchEvent -true-> 返回消費狀態true 3. 接收釋放事件 -UP-> Parent.dispatchTouchEvent -UP-> Parent.(super)dispatchTouchEvent{Parent處理消費} -UP-> Parent.onTouchEvent -true-> Parent.dispatchTouchEvent -true-> 返回消費狀態true |
1.父控件ACTION_DOWN攔截 2.父控件不消費事件 |
4. 接收按下事件 -DOWN-> Parent.dispatchTouchEvent -DOWN-> Parent.onInterceptTouchEvent -true-> Parent.dispatchTouchEvent -DOWN-> Parent.(super)dispatchTouchEvent{Parent處理消費} -DOWN-> Parent.onTouchEvent -false-> Parent.dispatchTouchEvent-false-> 返回消費狀態false 5. 接收不到移動事件 6. 同5 |
1.父控件ACTION_MOVE攔截 2.子控件消費事件 |
7. 接收按下事件 -DOWN-> Parent.dispatchTouchEvent -DOWN-> Parent.onInterceptTouchEvent -false-> Parent.dispatchTouchEvent -DOWN-> Child.dispatchTouchEvent{Parent分發,遍歷調用Child分發消息,Child內部遞歸分發消息} -DOWN-> TargetChild(目標子控件,區別Child,子控件消費事件,要麼是自己消費了,要麼是自己的後代或者後代的後代消費了).onTouchEvent{存在調用多個Child該方法,前提是前面的Child均返回false} -true-> Child.dispatchTouchEvent -true-> Parent.dispatchTouchEvent{記錄目標消費Child爲該View}-true-> 返回消費狀態true 8. 接收移動事件 -MOVE-> Parent.dispatchTouchEvent -MOVE-> Parent.onInterceptTouchEvent -true-> Parent.dispatchTouchEvent -CANCEL-> Child(目標消費Child).dispatchTouchEvent{Child處理消費} -CANCEL->Child.onTouchEvent -消費狀態-> Child.dispatchTouchEvent-消費狀態-> 返回消費狀態 9. 接收釋放事件 -UP-> Parent.dispatchTouchEvent -UP-> Parent.(super)dispatchTouchEvent{Parent處理消費} -UP-> Parent.onTouchEvent -消費狀態-> Parent.dispatchTouchEvent -消費狀態-> 返回消費狀態 |
1.父控件ACTION_MOVE攔截 2.子控件不消費事件 3.父控件消費事件 |
10. 接收按下事件 -DOWN-> Parent.dispatchTouchEvent -DOWN-> Parent.onInterceptTouchEvent -false-> Parent.dispatchTouchEvent -DOWN-> Child.dispatchTouchEvent{Parent分發,遍歷調用Child分發消息,Child內部遞歸分發消息} -DOWN-> TargetChild(目標子控件,區別Child,子控件處理消費事件).onTouchEvent{滿足事件座標在控件內的子View或者子View的後代均會調用到} -false-> Child.dispatchTouchEvent -false-> Parent.dispatchTouchEvent{沒有目標消費Child} -DOWN-> Parent.(super)dispatchTouchEvent{Parent處理消費} -DOWN-> Parent.onTouchEvent -true-> 返回消費狀態true 11. 同2 12. 同3 |
1.父控件ACTION_MOVE攔截 2.子控件不消費事件 3.父控件不消費事件 |
13. 接收按下事件 -DOWN-> Parent.dispatchTouchEvent -DOWN-> Parent.onInterceptTouchEvent -false-> Parent.dispatchTouchEvent -DOWN-> Child.dispatchTouchEvent{Parent分發,遍歷調用Child分發消息,Child內部遞歸分發消息} -DOWN-> TargetChild(目標子控件,區別Child,子控件處理消費事件).onTouchEvent{滿足事件座標在控件內的子View或者子View的後代均會調用到} -false-> Child.dispatchTouchEvent -false-> Parent.dispatchTouchEvent{沒有目標消費Child} -DOWN-> Parent.(super)dispatchTouchEvent{Parent處理消費} -DOWN-> Parent.onTouchEvent -false-> 返回消費狀態false 14. 同5 15. 同5 |
1.父控件ACTION_UP攔截 2.子控件消費事件 |
16. 同7 17. 接收移動事件 -MOVE-> Parent.dispatchTouchEvent -MOVE-> Parent.onInterceptTouchEvent -false-> Parent.dispatchTouchEvent -MOVE-> Child(目標消費Child).dispatchTouchEvent -MOVE-> Child.onTouchEvent -true-> Child.dispatchTouchEvent -true-> Parent.dispatchTouchEvent -true-> 返回消費狀態true 18. 接收釋放事件 -UP-> Parent.dispatchTouchEvent -UP-> Parent.onInterceptTouchEvent -true-> Parent.dispatchTouchEvent -CANCEL-> Child(目標消費Child).dispatchTouchEvent{Child處理消費} -CANCEL-> Child.onTouchEvent -消費狀態-> Child.dispatchTouchEvent -true-> Parent.dispatchTouchEvent -true-> 返回消費狀態true |
1.父控件ACTION_UP攔截 2.子控件不消費事件 3.父控件消費事件 |
19. 同10 20. 同2 21. 同3 |
1.父控件ACTION_UP攔截 2.子控件不消費事件 3.父控件不消費事件 |
22. 同13 23. 同5 24. 同5 |
1. 父控件不攔截 2. 子控件消費事件 |
25. 同7 26. 同17 27. 接收釋放事件 -UP-> Parent.dispatchTouchEvent -UP-> Parent.onInterceptTouchEvent -false-> Parent.dispatchTouchEvent -UP-> Child(目標消費Child).dispatchTouchEvent -UP-> Child.onTouchEvent -true-> Child.dispatchTouchEvent -true-> Parent.dispatchTouchEvent -true-> 返回消費狀態true |
1. 父控件不攔截 2. 子控件不消費事件 3. 父控件消費事件 |
28. 同10 29. 同2 30. 同3 |
1. 父控件不攔截 2. 子控件不消費事件 3. 父控件不消費事件 |
31. 同13 32. 同5 33. 同5 |
啓示
1. ACTION_DOWN執行事件分發查找(遍歷子View,遞歸分發查找,如果子View未消費,則回退到自己消費,依次向上回溯,找到目標消費View爲止)找到目標消費子View。後續事件不再需要查找,直接發送給目標消費子View,如果沒有,則自己消費。
2. 事件已消費路徑上(終點爲目標消費View),如果有父控件攔截事件,則第一次攔截後,會將當前事件轉爲ACTION_CANCEL傳遞給目標消費子View,後續事件則直接自己處理消費,不論是否消費,均能收到後續事件流
論證
1. 從事件流可證明事件一致性保證(Consistency Guarantees):
1. ViewGroup在ACTION_DOWN的事件分發返回false(不消費事件),則不再會收到後續事件(ACTION_MOVE、ACTION_UP/ACTION_CANCEL)。
2. ViewGroup在ACTION_DOWN的事件分發返回true(消費事件),則會收到後續事件(ACTION_MOVE、ACTION_UP/ACTION_CANCEL),如果ViewGroup攔截後續事件,則第一次攔截會將事件轉爲ACTION_CANCEL傳遞給目標消費子View(終止子View接收後續事件),接下來的後續事件自己消費。
3. ViewGroup在非ACTION_DOWN的事件分發返回消費狀態對整體事件流沒有影響。
2. 從註釋可證明:
View.dispatchTouchEvent方法完成事件的消費處理;
ViewGroup.dispatchTouchEvent方法完成事件的分發處理;
ViewGroup.onInterceptTouchEvent方法完成事件的攔截處理;
事件分發路徑上的ViewGroup,在ACTION_DOWN或者不是自己直接消費事件時一定會調用onInterceptTouchEvent方法。
以及View類的onTouchEvent方法完成具體處理事件消費。
一張圖
賞析
1. ACTION_DOWN會觸發查找目標消費View,優先子View嘗試消費,如果子View仍然沒有消費,則依次回溯到父控件嘗試消費(直至DecorView,然後Activity嘗試消費),如果找到了,則回溯返回true。
2. ACTION_DOWN後續事件執行的前提是事件分發路徑的終點就是目標消費View,目標消費View的父控件均會調用到事件攔截(讓父控件有機會攔截下來,改變事件流),如果目標消費View的父控件攔截,攔截時的事件會轉換爲ACTION_CANCEL繼續按原路徑分發,後續的事件則不再分發給目標消費View,而是攔截的父控件自己消費。
3. 非ACTION_DOWN返回的消費狀態對事件流沒有影響,如果未消費,會回調給Activity處理。
標準
常見錯誤
1. 不知道onInterceptTouchEvent和onTouchEvent什麼時候會調用,但是知道dispatchTouchEvent每次都會調用,就把邏輯直接寫在dispatchTouchEvent的重寫方法裏面。
問題: 不滿足事件流一致性,存在目標消費View沒有接收到ACTION_UP/ACTION_CANCEL就結束了,導致焦點、按鍵狀態或者按鍵事件不符合預期。
2. 發現onInterceptTouchEvent經常調用到,邏輯寫在onInterceptTouchEvent裏面。
問題: onInterceptTouchEvent在View自己消費情況下或者攔截之後的事件流不再會調用到,會把坑隱藏得更深【不好復現的Bug纔是最難解決的Bug】。
3. 鳥槍法,dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent均會調用到邏輯。
問題: 路子太野。。。
4. 覺得自己很牛X,邏輯分散在dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent裏面。
問題: 可讀性差,邏輯混亂。
5. 事件消息只處理了ACTION_DOWN、ACTION_MOVE、ACTION_UP,沒有對ACTION_CANCEL或者其他多點觸控事件容錯處理。
問題: 總會出現不常見的問題。
最佳實踐
1. 明確事件流調用順序以及攔截後的事件流。
2. dispatchTouchEvent:正常情況下不建議重寫dispatchTouchEvent方法改變系統事件分發機制,可以看到,Google就沒有幾個類重新該方法。最多記下座標點,但千萬調用super. dispatchTouchEvent保證系統事件分發正常調用。
3. onInterceptTouchEvent:只處理攔截邏輯,在合適事件將事件流導到onTouchEvent。
4. onTouchEvent:真正處理邏輯。
5. 除常見事件處理外,一定要上剩餘事件容錯處理。
漁
方法論
1. MECE法則和金字塔原理
2. SCQA 架構如何理解?
利器
1. AS源碼英文翻譯,參考AS翻譯插件Translation
2. Android源碼調試
1. Android模擬器GenyMotion
2. GenyMotion創建和App的build.gradle中targetSdkVersion相同API Level模擬器即可Debug對應上源碼。進階參考如何調試Android Framework?
3. Android Studio你不知道的調試技巧
3. 關鍵日誌輸出,使用靜態代理,進階參考Android插件化原理解析——Hook機制之動態代理
4. 繪圖工具
1. ProcessOn
2. Edraw
5. 個人主頁
1. 將純文本轉化爲靜態網站和博客
2. TeXt主題模板
3. 怎樣引導新手使用 Markdown?
利
1. 隨心所欲控制事件流【大權在手,天下我有】
2. 事件分發不再是個事,怕個球
3. 各種酷炫動畫和自定義控件燥起來
4. 再也不用擔心面試中尬聊事件分發
5. 借鑑上述不成熟的“漁”去愛幹嘛幹嘛
進階
1. 滾動控件和按鍵衝突處理,界面佈局滾動
2. 滑動衝突
1. NestedScrolling機制
2. Android NestedScrolling機制完全解析 帶你玩轉嵌套滑動
3. 外部攔截法&內部攔截法
3. 手勢(GestureDecetor)
參考
1. 圖解 Android 事件分發機制
2. Android 響應用戶屏幕手勢操作
3. Android MotionEvent詳解
4. android觸控,先了解MotionEvent(一)
5. Android多點觸控之——MotionEvent(觸控事件)
6. 圖解Android事件傳遞之View篇
7. 圖解Android事件傳遞之ViewGroup篇
長歌
念奴嬌·天丁震怒
完顏亮(金代)
天丁震怒,掀翻銀海,散亂珠箔(bó)。
六出奇花飛滾滾,平填了山中丘壑。(六出:雪花六角,因用爲雪花的別名。)
皓虎顛狂,素麟猖獗(chāng jué),掣(chè, 拉)斷珍珠索。(皓虎:白色的老虎。素麟:白色的麒麟。)
玉龍酣戰,鱗甲滿天飄落。
誰念萬里關山,征夫僵立,縞(gǎo)帶沾旗腳。(僵立:因寒冷而凍得僵硬直立。縞帶:白色的衣帶。)
色映戈矛,光搖劍戟(jǐ ),殺氣橫戎幕。(戎幕:行軍作戰時的營帳。)
貔(pí)虎豪雄,偏裨(pí)英勇,共與談兵略。(裨:副,偏,小。)
須拼一醉,看取碧空寥廓(liáo kuò)。