編輯:關於Android編程
引言:這部分會分三個模塊來講,先講View對Touch的處理,再講ViewGroup的事件分發,最後講如何解決滑動沖突。
我習慣通過在源碼中添加注釋來理解源碼,以下是我提取出來幾個重要方法,將不重要的部分刪掉,並且添加了中文注釋。
如果一個View(比如Button)接收到Touch,那麼該Touch事件首先會傳入到它的dispatchTouchEvent( )方法,所以我們從這裡開始學習View對Touch事件的處理。
// 返回值表示Touch事件是否被該View消費
public boolean dispatchTouchEvent(MotionEvent event) {
//result的值決定最後該方法的返回值,也就是決定Touch事件是否被消費
boolean result = false;
/***/
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
//該if判斷中一共包含了4個條件,必須同時滿足時才表示Touch事件被消費
//在這四個條件中,我們通常最關心的就是最後一個:TouchListener的onTouch()方法。假如這四個條件中的任意一個不滿足,那麼result仍為false;則進入下一步調用自身的onTouchEvent()
if (li != null && li.mOnTouchListener != null &&
(mViewFlags&ENABLED_MASK)==ENABLED && li.mOnTouchListener.onTouch(this,event)) {
result = true;
}
//調用View自身的onTouchEvent()處理Touch事件,由onTouchEvent的返回值決定result的值(當然,如果在上一步中,如果已經將result設為true,就不會去判斷onTouchEvent()了)
if (!result && onTouchEvent(event)) {
result = true;
}
}
/***/
return result;
}
onTouchEvent()是決定事件是否被消耗的最後一道門,如果返回false,則它的父View的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();
//如果一個View是disable的,CLICKABLE,LONG_CLICKABLE,CONTEXT_CLICKABLE消耗掉,且不會觸發onClick事件回調
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//如果View不是disable的,會繼續執行,對CLICK,LONG_CLICK,CONTEXT_CLICKABLE進行處理
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_DOWN:
/***/
break;
case MotionEvent.ACTION_MOVE:
/***/
break;
case MotionEvent.ACTION_CANCEL:
/***/
break;
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
/***/
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
/***/
if (!focusTaken) {
/***/
if (!post(mPerformClick)) {
//performClick()中會回調onClick,所以我們平時常見的onClick回調都是在ACTION_UP的時候觸發的
performClick();
}
}
}
}
/***/
}
//這裡表示,只要View是enable的,Touch事件都會被消耗掉
return true;
}
return false;
}
引用一張谷歌的小弟畫的流程圖:
需要注意的點:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPm9uVG91Y2goKdPrb25Ub3VjaEV2ZW50KCnS1LywY2xpY2vI/dXftcTH+LHwus3Bqs+1IKO6PC9wPg0Kb25Ub3VjaCgp0+tvblRvdWNoRXZlbnQoKba8yse0psDttKXD/srCvP61xEFQSSBvblRvdWNoKCnK9NPaVG91Y2hMaXN0ZW5lcr3Tv9rW0LXEt723qKOsysdWaWV3sanCtrj408O7p7XEvdO/2rHj09q0psDttKXD/srCvP6jrLb4b25Ub3VjaEV2ZW50KCnKx0FuZHJvaWTPtc2z19TJ7bbU09pUb3VjaLSmwO21xMq1z9Ygz8i199PDb25Ub3VjaCgpuvO199PDb25Ub3VjaEV2ZW50KCmho7b4x9LWu9PQtbFvblRvdWNoKCnOtM/7t9FUb3VjaMrCvP6yxdPQv8nE3LX308O1vW9uVG91Y2hFdmVudCgpoaO8tG9uVG91Y2goKbXE08XPyLy2schvblRvdWNoRXZlbnQoKbXE08XPyLy2uPy436GjINTab25Ub3VjaEV2ZW50KCnW0LSmwO1BQ1RJT05fVVDKsbvhwPvTw0NsaWNrTGlzdGVuZXLWtNDQQ2xpY2vKwrz+oaPL+dLUVG91Y2i1xLSmwO3Kx9PFz8jT2kNsaWNrtcQgvPK1pbXYy7XI/dXf1rTQ0Muz0PLOqqO6b25Ub3VjaCgpJm5kYXNoOyZndDtvblRvdWNoRXZlbnQoKSZuZGFzaDsmZ3Q7b25DbGljaygpDQo8cD5WaWV3w7vT0MrCvP61xMC5vdgob25JbnRlcmNlcHRUb3VjaEV2ZW50KCApKaOsVmlld0dyb3VwssXT0KOsx+vO8Lvsz/08L3A+DQo8aHIgLz4NCjxoMiBpZD0="二viewgroup的事件分發">二、ViewGroup的事件分發 Touch事件會從PhoneWindow開始一直傳遞到最頂層的ViewGroup,然後調用到最頂層的dispatchTouchEvent()
事件分發體系最重要的幾個方法:
dispatchTouchEvent(event)
主要完成事件分發的邏輯,只要事件到達該View,一定會調用這個方法,返回值表示是否消耗當前事件。onInterceptTouchEvent(Event)
判斷是否攔截某個事件,返回值表示是否攔截onTouchEvent(Event)
用來處理Touch事件,返回值表示是否消耗該事件。他們的關系可以這樣表示:
public boolean dispatchTouchEvent(MotionEvent e) {
if(onInterceptTouchEvent(ev)) {
//如果攔截,就自己處理,調用自己的onTouchEvent,如果onTouchEvent返回true就消費掉,如果返回false就傳給上層處理
return onTouchEvent(ev);
} else {
//如果不攔截,就走子view的分發流程
return child.dispatchTouchEvent(ev);
}
}
來看看源碼:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
//如果是DOWN事件,進行初始化和還原操作
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 判斷是否需要攔截事件,根據intercepted的值確定
// mFirstTouchTarget用於多點觸控
// mFirstTouchTarget不為空,表示有子View消費了Touch事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果是DOWN或者有子View消費,則根據onInterceptTouchEvent判斷是否攔截
// ViewGroup的onInterceptTouchEvent默認返回false,也就是不攔截
// 所以我們往往可以在自定義控件中重寫這個方法,來決定什麼情況下攔截事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
// 如果disallowIntercept == false,就是不允許攔截,可以在子View設置不允許父View攔截
intercepted = false;
}
} else {
// 執行到這裡,說明mFirstTouchTarget為null或者不是DOWN事件,需要ViewGroup自己處理此次Touch事件
// 也就是攔截本次Touch事件
intercepted = true;
}
// 如果Touch事件沒有被取消也沒有被攔截,那麼ViewGroup將類型為ACTION_DOWN的Touch事件分發給子View。
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 根據Touch事件的坐標,找到觸摸到了哪個子View
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList 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);
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
/***/
// 找到之後,調用dispatchTransformedTouchEvent,傳入子view
// 子View沒有消費Touch事件則該方法的返回值為false,此時mFirstTouchTarget仍為null
// 如果子View消費掉了Touch事件那麼該方法的返回值為true,然後執行newTouchTarget = addTouchTarget(child, idBitsToAssign);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
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消費掉了Touch事件那麼該方法的返回值為true,然後執行
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
}
}
// mFirstTouchTarget為空,表示沒有子View消耗Touch事件,需要ViewGroup自己處理
if (mFirstTouchTarget == null) {
// 同樣也會調用dispatchTransformedTouchEvent,但是傳入Null,標明由父View自己處理這次Touch事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
/***/
}
/***/
}
}
最後會調用dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
/***/
// 如果child == null,表明沒有子View消費這次Touch事件
if (child == null) {
// 所以會調用super.dispatchTouchEvent,此時,ViewGroup就化身為了普通的View,它會在自己的onTouch(),onTouchEvent()中處理Touch
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());
}
// 如果child不為空,表示有子View處理這次Touch事件,直接調用child的dispatchTouchEvent
// 當然該view可能是一個View也可能是一個ViewGroup
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
最後會遞歸的執行子View的這套流程,或者被ViewGroup自身攔截掉,親自用onTouchEvent處理這次事件
onInterceptTouchEvent()表示是否攔截此次事件,ViewGroup的默認實現是不攔截,return false;所以我們往往可以在自定義控件中重寫這個方法,來決定什麼情況下攔截事件
總結下來就是:
Touch事件的傳遞順序為 :
Activity–>外層ViewGroup–>內層ViewGroup–>View
如果Touch事件在中間某一層被攔截了,DOWN事件將不會再傳遞給更底層的View
Touch事件的消費順序為 :
View–>內層ViewGroup–>外層ViewGroup–>Activity
如果Touch事件在中間某一層被消費了,將不會再通知更上層的View,只有當所有子View都不消費Touch事件,頂層ViewGroup才會自己處理這次Touch事件。
引發原因:兩個可以滑動的View互相嵌套,且滑動方向相同,則會產生滑動沖突。
有兩個比較常見的解決方案:
1、在父View中准確地進行事件分發和攔截
比如重寫onInterceptTouchEvent()和onTouchEvent(),對事件進行正確的分配,保證在合適的時候Touch時間可以傳遞給子View2、使用Google在support.v4包提供的兩個支持嵌套滾動的接口:onNestedScrollChild、onNestedScrollParent。(有一個例子,在我的Github上一個快速開發框架裡的下拉刷新SwipeLayout中有用到,貼上地址:https://github.com/miomin/Shareward)
一個事件序列指的是從手指按下的一刻起直到手指放開,通常情況下,一個事件序列只能被一個View攔截或消耗,攔截和消耗通常都是在DOWN事件進行,如果不對DOWN時間進行消費,則不會有機會消耗後續的MOVE事件,如果消耗了DOWN事件,後續的MOVE和UP事件同樣由這個View消費。(support.v4包中的NestedScrolling接口可以打破這個原則,允許多個View同時處理同一個事件序列)
ViewGroup的onInterceptTouchEvent默認返回false,也就是默認不會攔截事件,交給下層的View來處理。
View沒有onInterceptTouchEvent方法,一旦有事件到達,就會調用onTouchEvent。
可點擊的View的onTouchEvent默認返回true,也就是消耗事件。
題外話: 說來有些慚愧,對於這三者的初步認識居然是在背面試題的時候。那個時候自己接觸Android的時間還不長,學習的書籍也就是比較適合入門的《瘋狂Android講義》,
大家好,看我像不像蘑菇…因為我在學校呆的發霉了。 思而不學則殆麗麗說得對,我有奇怪的疑問,大都是思而不學造成的,在我書讀不夠的情況下想太多,大多等於白想,所
按照我一開始的打算,上面一篇文章應該是“Android動畫總結系列(5)——屬性動畫源碼分析”,不過屬性動畫源碼分析寫起來
下面為控件的實現歷程: 此控件高效,直接使用ondraw繪制,先亮照: 由於Android自身的星星評分控件樣式可以改,但是他的大小不好調整的缺點,只能用small no