View 事件簡介
View 事件悼潭,既 MotionEvent江解,是用戶觸摸屏幕的一系列事件贤牛。同一事件序列是從手指接觸屏幕的那一刻起构蹬,到手指離開屏幕的那一刻結(jié)束,在這個(gè)過程中所產(chǎn)生的一系列事件悔据,這個(gè)事件序列以 ACTION_DOWN 開始庄敛,中間含有一系列的 ACTION_MOVE,最終以 ACTION_UP 結(jié)束科汗。
View 事件分發(fā)簡述
當(dāng)用戶點(diǎn)擊屏幕的時(shí)候藻烤,TouchEvent 最先傳遞給Activity.dispatchTouchEvent(MotionEvent)
,然后再調(diào)用DecorView.superDispatchTouchEvent(MotionEvent)
头滔,接著直接調(diào)用super.dispatchTouchEvent(MotionEvent)
怖亭,即ViewGroup.dispatchTouchEvent(MotionEvent)
。
ViewGroup 和 View 分發(fā)事件拙毫,有三個(gè)很重要的方法:
-
public boolean dispatchTouchEvent(MotionEvent ev)
:用來進(jìn)行事件的分發(fā)依许,如果事件能夠傳遞給當(dāng)前 View,那么該方法一定會(huì)調(diào)用缀蹄,返回值受當(dāng)前 View 的 onTouchEvent 和下級(jí) View 的 dispatchTouchEvent 的影響峭跳,表示是否消耗當(dāng)前事件; -
public boolean onInterceptTouchEvent(MotionEvent ev)
:表示在上述方法內(nèi)部調(diào)用缺前,判斷當(dāng)前 View 是否攔截某個(gè)事件蛀醉。如果當(dāng)前 View 攔截了某個(gè)事件,那么同一個(gè)事件序列當(dāng)中衅码,此方法不會(huì)被再次調(diào)用拯刁;反之,如果是下級(jí) View 攔截了事件逝段,并且下級(jí) View 沒有調(diào)用public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
垛玻,那么當(dāng)前 View 的onInterceptTouchEvent(MotionEvent ev)
會(huì)被繼續(xù)調(diào)用,即當(dāng)前 View 依舊擁有攔截后續(xù)事件的能力奶躯; -
public boolean onTouchEvent(MotionEvent event)
:在 dispatchTouchEvent中調(diào)用帚桩,用來處理點(diǎn)擊事件,返回結(jié)果表示是否消耗當(dāng)前事件嘹黔,如果不消耗账嚎,則在同一個(gè)事件序列中,當(dāng)前 View 無法再次接收到事件儡蔓。
上述三個(gè)方法的關(guān)系大致可以用下面的偽代碼來表示:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume=false;
if(onInterceptTouchEvent(ev)){
consume=onTouchEvent(ev);
}else{
cousume=child.dispatchTouchEvent(ev);
}
returen consume;
}
dispatchTouchEvent搭建了事件分發(fā)的框架郭蕉,一般不需要重寫,自定義 View 時(shí)通常重寫的是onInterceptTouchEvent和onTouchEvent喂江。事件的分發(fā)有點(diǎn)類似有序樹的查找算法召锈,ViewGroup 就是結(jié)點(diǎn),View 就是葉子开呐。遍歷到 View 的時(shí)候烟勋,就要返回了规求。
源碼分析
ViewGroup
dispatchTouchEvent
// 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();
}
當(dāng)接收到 ACTION_DOWN 事件的時(shí)候,會(huì)重置一下狀態(tài)(比如說重置FLAG_DISALLOW_INTERCEPT狀態(tài)位卵惦,允許攔截事件)阻肿,清除之前的 TouchTarget。
TouchTarget :Describes a touched view and the ids of the pointers that it has captured.
// 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;
}
此段代碼是判斷當(dāng)前 ViewGroup 是否要攔截事件沮尿,首先介紹幾個(gè)變量的含義:
- mFirstTouchTarget:touch target list 的第一項(xiàng)丛塌,表示上一個(gè)處理事件的 child;
- disallowIntercept:是否允許當(dāng)前 ViewGroup 攔截事件畜疾,默認(rèn)是允許赴邻,其 child 可以設(shè)置為不允許;
由上述代碼可知啡捶,如果 MotionEvent 不是 ACTION_DOWN 且 child 沒有處理上一個(gè)事件姥敛,則 ViewGroup 會(huì)攔截下事件;否則會(huì)調(diào)用 onInterceptTouchEvent 來判斷是否需要攔截事件(除非 child 不允許 view parent 攔截事件)瞎暑。
if (!canceled && !intercepted) {
......
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
.....
//當(dāng) ViewGroup 不攔截事件彤敛,且事件為 DOWN 類型,那么就要遍歷其 child了赌,尋找處理事件的 child墨榄。
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
......
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//能夠接受事件的child:事件坐標(biāo)在 View 內(nèi);View 可見或者沒有在執(zhí)行動(dòng)畫
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;
}
//
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
......
//用 child 獲得新的 TouchTarget勿她,并將其添加到 Touch Target list 的第一個(gè)袄秩;
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
當(dāng) ViewGroup 不攔截事件時(shí),且事件類型為 DOWN 時(shí)逢并,ViewGroup 會(huì)遍歷其 child之剧,尋找能接收事件的 child,然后調(diào)用dispatchTransformedTouchEvent
將事件傳遞給 child 進(jìn)行處理砍聊。具體邏輯處理可以看代碼中的注釋猪狈。
// 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 (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
......
}
}
}
如果沒有 child 要處理事件,那么就ViewGroup 自身嘗試處理事件辩恼;如果有,那么遍歷 Touch Target list 谓形,每一個(gè)child 都嘗試處理事件灶伊。
下面讓我們看一下dispatchTransformedTouchEvent
的代碼:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
......
// 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.
//根據(jù) pointerId、child 的偏移量對(duì) MotionEvent 進(jìn)行轉(zhuǎn)換
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);
}
//如果 child 為null寒跳,那么調(diào)用 super.dispatchTouchEvent聘萨,即 View 的 dispatchTouchEvent 方法,
//看能否消耗該事件童太,否則米辐,事件傳遞給 child 進(jìn)行處理
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
......
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
邏輯不復(fù)雜胸完,主要就是對(duì) MotionEvent 進(jìn)行根據(jù) pointerId 、child 的偏移量進(jìn)行轉(zhuǎn)換翘贮,然后如果 child 不為null赊窥,那么調(diào)用 child 的 dispatchTouchEvent,將事件傳遞給 child 進(jìn)行處理狸页,如果 child 為null锨能,那么調(diào)用 super.dispatchTouchEvent,即viewgroup嘗試處理該事件芍耘。
上面就是 ViewGroup 分發(fā)事件基本框架代碼址遇,大致可以總結(jié)如下:
- 如果事件是 DOWN 類型,先判斷當(dāng)前 ViewGroup 是否攔截該事件(通常 ViewGroup 是不會(huì)攔截 DOWN 事件斋竞,否則child 完全接受不到事件了)倔约;如果不攔截事件,那么遍歷其 child坝初,尋找處理該事件的 child浸剩。如果沒有 child 處理事件,那么 ViewGroup 自行嘗試處理該事件脖卖,否則 child 處理該事件乒省,并將 mFirstTouchTarget 設(shè)置為該 child;
- 事件不是 DOWN畦木,且之前的事件沒有 child 進(jìn)行處理(mFirstTouchTarget 為 null)袖扛,那么ViewGroup 嘗試處理該事件;如果之前的事件有 child 進(jìn)行處理了十籍,那么先判斷 ViewGroup是否需要攔截該事件蛆封,不需要,則直接交由之前處理事件的 child 直接處理勾栗。
View
dispatchTouchEvent
不同于 ViewGroup惨篱,View 不能再向下分發(fā)事件烧董,要么自身處理事件缓溅,要么不處理返回給 parent處理,相當(dāng)于樹中的葉子诵叁,所以 View 沒有 onInterceptTouchEvent 方法界牡,它的 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;
}
由上可知,首先會(huì)判斷 View 是否設(shè)置了 OnTouchListener宿亡,如果是常遂,則將事件交給它處理,否則才會(huì)調(diào)用 VIew 的 onTouchEvent 方法挽荠】烁欤可見 OnTouchListener 的優(yōu)先級(jí)高于 View 的 onTouchEvent平绩,這是方便我們?cè)谕獠吭O(shè)置處理事件的方法。
接下來看看 View 的 onTouchEvent 方法:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == 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.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
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
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:
mHasPerformedLongPress = false;
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.
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:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
由上可知漠另,View 的 onTouchEvent 的處理邏輯大致如下:
- 只要 View 時(shí) clickable 的捏雌,總是返回 true,即總是消耗事件酗钞;
- 如果 View 時(shí) disabled 的腹忽,那么直接返回 TRUE,照樣消耗事件砚作,但是不執(zhí)行相應(yīng)的動(dòng)作窘奏;
- 如果 View 設(shè)置的 Delegate,那么直接把事件交給 Delegate處理(利用這點(diǎn)可以將事件交給其他 View 來處理)葫录;
- 如果 View 自己處理事件着裹,那么根據(jù)事件的類型處理方法如下:
- DOWN:設(shè)置狀態(tài)為 pressed,同時(shí)設(shè)置一個(gè)延時(shí)任務(wù)米同,用以判斷 longClick骇扇;
- MOVE:判斷事件是否超過了 View 的邊界,如果是面粮,重置狀態(tài)少孝,取消 longClick 的延時(shí)任務(wù)等;
- UP:如果狀態(tài)為 pressed熬苍,那么執(zhí)行 performClick稍走,并且重置狀態(tài);
- CANCEL:重置狀態(tài)
實(shí)戰(zhàn)分析
對(duì)于自定義 View柴底,如何正確地分發(fā)婿脸、處理事件非常重要,下面就大致說說 ViewGroup 和 View 分別是如何重寫相關(guān)方法柄驻,以實(shí)現(xiàn)需求狐树。
View
對(duì)于 View,因?yàn)椴恍枰职l(fā)事件鸿脓,所以 View 一般只需要重寫 onTouchEvent抑钟,然后根據(jù)事件類型分情況處理:
- Donw:一定要返回 TRUE,否則同一序列的后續(xù)事件都不會(huì)交給這個(gè) View 處理了野哭;
- Move:通常是我們處理的關(guān)鍵味赃,根據(jù)它來進(jìn)行相應(yīng)的邏輯處理,比如說移動(dòng) View虐拓,繪畫之類的;
- Up:重置狀態(tài)傲武,資源回收等處理蓉驹;
ViewGroup
對(duì)于 ViewGroup城榛,首先是要重寫 onInterceptTouchEvent:
- Down:返回 FALSE,這樣 child 才有可能接收到事件并進(jìn)行處理态兴;
- Move:根據(jù)需要進(jìn)行判斷返回 TRUE or FALSE狠持,返回 TRUE,說明 ViewGroup 要攔截事件瞻润,交由其 onTouchEvent 進(jìn)行處理喘垂,F(xiàn)ALSE 則繼續(xù)給 child 進(jìn)行處理;
- Up:返回 FALSE绍撞,這樣child 才有可能接收到 Up 事件正勒,進(jìn)行相應(yīng)的處理;
onTouchEvent:
- 返回TRUE傻铣,表示 ViewGroup 消耗了事件章贞,后續(xù)事件都會(huì)交由它處理;
- 返回 FALSE非洲,表示 ViewGroup 不再消耗事件鸭限,但是,如果ViewGroup dispatchTouchEvent(DownEvent)的時(shí)候返回了 TRUE两踏,即消耗了 DOWN 事件败京,那么就算他不消耗后續(xù)事件,后續(xù)事件依然會(huì)傳遞給它梦染,而不會(huì)交給 parent 來處理赡麦,最終這些未處理的事件會(huì)在 Activity 中處理;如果沒有消耗過 DOWN 事件弓坞,那么事件會(huì)直接交給 parent 來處理(因?yàn)?mFirstTouchTarget 為 null)隧甚;