前言
View作爲Android應用與用戶交互入口,除了展示視圖外,還承擔了處理用戶操作的任務,比如用戶的點擊、長按、滑動事件等。處理點擊事件的機制就是View的事件分發機制。
View的事件分發機制
當用戶點擊屏幕時,就會產生點擊事件,這個事件信息被封裝在一個類中,這個類就是MotionEvent。事件產生後Android系統會將事件傳遞到View的層級中,然後MotionEvent就會在View的層級中傳遞分發。
在View的分發機制中會設計到三個重要的方法,這三個方法承擔了View事件機制的處理任務。它們分別是:
- dispatchTouchEvent(MotionEvent ev)—對事件進行分發。
- onInterceptTouchEvent(MotionEvent ev)—用來攔截事件,在dispatchTouchEvent中調用,這個方法存在於ViewGroup中。
- onTouchEvent(MotionEvent ev)—用來處理事件
View點擊事件的發生
當點擊事件發生後,事件首先會傳遞到當前的Activity中,這個過程調用了Activity的dispatchTouchEvent方法。public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction(); //空方法,用於重寫回調
}
//調用當前Window的superDispatchTouchEvent方法
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//調用Activity的onTouchEvent方法
return onTouchEvent(ev);
}
//PhoneWindow中的方法
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
public boolean superDispatchTouchEvent(MotionEvent event) {
//調用ViewGroup中的dispatchTouchEvent方法
return super.dispatchTouchEvent(event);
}
可以看到當事件產生後,首先在當前Activity中會進行事件攔截,如果當前Window不攔截就會調用Activity的onTouchEvent方法。
同時,我們可以看到在PhoneWindow中會調用DecorView的superDispatchTouchEvent方法。這個方法又調用了dispatchTouchEvent方法。這是就開始了View層級的事件分發。
事件分發開始
從上面的代碼中可以看到。在View層級中,事件處理從ViewGroup的dispatchTouchEvent方法開始。我們開始從這裏分析。
public boolean dispatchTouchEvent(MotionEvent ev) {
//......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
//一個完整的事件從DOWN事件開始,UP事件結束
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 重置觸摸狀態,因爲程序可能由於切換、ANR或者某些其他狀態改變。框架已經刪除了up和cancel事件
cancelAndClearTouchTargets(ev);
//重置狀態
resetTouchState();
}
// Check for interception.
//檢查是否有攔截事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//這個標誌也跟requestDisallowInterceptTouchEvent有關,通過此函數設置標誌可以另子View決定父容器是否攔截子View事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//調用onInterceptTouchEvent攔截事件
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;
}
//......
// 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) {
//不攔截事件,繼續分發事件
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;
//......
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);
//獲取點擊範圍內的字View
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
//獲取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);
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;
}
//......
}
if (preorderedList != null) preorderedList.clear();
}
//......
}
}
// 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 {
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;
//父容器不攔截情況下,分發事件到對應的子View
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//......
predecessor = target;
target = next;
}
}
//......
return handled;
}
從代碼中可以看到,事件分發的情況分爲兩種,一種是如果父容器不攔截事件,就把事件分發到對應的子View;另一種是父容器攔截事件,事件交由自己處理。在第一種情況下,ViewGroup會遍歷子View,判斷子View是否在點擊區域內,如果是就將事件交由子View分發。第二種情況下,ViewGroup攔截事件。這個兩種情況最終都會調用dispatchTransformedTouchEvent方法。接下來分析這個方法的作用。
//最終分發事件的方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//......省略部分代碼
// Perform any necessary transformations and dispatch.
if (child == null) {
//父容器攔截事件,調用View中的dispatchTouchEvent
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());
}
//父容器不攔截事件,將事件分發到子View中
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
從代碼中可以看到,這個實現了剛纔ViewGroup中分發事件的兩種情況,父容器攔截以及不攔截。攔截的情況下child就爲null,這個時候調用View的dispatchTouchEvent方法。不攔截的情況下調用child的dispatchTouchEvent方法。我們再來分析下View中的dispatchTouchEvent方法。
public boolean dispatchTouchEvent(MotionEvent event) {
//.......此處省略部分代碼
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;
}
從代碼中可以看到,在View的dispatchTouchEvent方法中,如果OnTouchListener不爲null,就優先調用OnTouchListener的onTouch方法,並且會返回true,表示該事件被消耗。否則會調用onTouchEvent方法。在這裏我們只分析onTouchEvent方法。
public boolean onTouchEvent(MotionEvent event) {
//......
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//如果View可以點擊,處理點擊事件
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
//處理UP事件
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if (!clickable) {
//取消長按事件
removeLongPressCallback();
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
//獲取焦點
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//處理點擊事件
performClick();
}
}
}
}
mIgnoreNextUpEvent = false;
break;
//處理DOWN事件
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
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:
//......
break;
case MotionEvent.ACTION_MOVE:
//......
break;
}
return true;
}
return false;
}
從代碼中可以看到,onTouchEvent處理了分發過來的事件。事件類型有ACTION_UP、ACTION_DOWN、ACTION_MOVE、ACTION_CANCEL。onTouchEvent處理事件的前提是View是可點擊的。其中當View註冊了OnCLickListener和onLongClickLinster即爲可點擊的。可以看到處理點擊事件是在ACTION_UP中處理的,通過調用perfromClick方法實現,當調用點擊事件時,說明長按事件未到達長按的時間。而長按事件是在ACTION_DOWN中實現的,通過checkForLongClick方法發送延遲消息,當達到長按時間時就調用長按事件。
事件分發的原理
經過上面的分析,現在總結一下View事件分發的原理。事件的開始是從Activity到PhoneWindow中,最後經由View層級。在View的層級中從頂級View(DecorView)分發。
- 當點擊事件產生後,有頂層的ViewGroup分發事件。
- 通過調用dispatchTouchEvent方法,當父容器攔截事件時就調用View的dispatchTouchEvent方法,進而調用onTouchEvent方法或者OnTouchListener的onTouch方法。
- 否則,調用子View的dispatchTouchEvent方法。如果子View是ViewGroup類型,則繼續按照步驟1分發事件。否則調用View的dispatchTouchEvent方法。
總結
View的事件分發機制,處理了用戶通過觸摸屏幕產生的事件。一般來說通過View的事件分發,我們經常需要處理的有DOWN、MOVE、UP事件。通過實現這些類型的事件,就可以實現不同的交互操作,進而豐富View與用戶的交互體驗。