一、View繪制流程機制
1、View繪制起點
-
performTraversals()方法觸發(fā)了View 的繪制。
Activity調(diào)用流程 說明:
在Activity顯示時,WindowManager將View添加到DecorView,兩者通過ViewRoot連接起來。
具體實現(xiàn)類是ViewRootImpl。
再通過ViewRootImpl的一系列處理,最終調(diào)用performTraversals方法,在performTraversals方法中,依次調(diào)用了performMeasure(),performLayout(),performDraw(),將View 的measure,layout,draw` 過程從頂層View 分發(fā)了下去。開始了View的繪制。
2、View繪制流程
- 繪制過程分為三步:
measure(測量) -->layout(布局) -->draw(繪制),
在draw流程結束以后就可以在屏幕上看到view了。 -
流程圖如下:
繪制流程
.1、measure(測量)
測量的目的,是為了計算出View的大小,通過
MeasureSpec來進行計算的。
MeasureSpec是一個specSize和specMode信息的32 位int 值,其中高兩位表示specMode,低30位表示specSize。specMode模式:
UNSPECIFIED:父容器不對View有任何限制,要多大有多大。常用于系統(tǒng)內(nèi)部。
EXACTLY(精確模式):父視圖為子視圖指定一個確切的尺寸SpecSize。對應LyaoutParams中的match_parent或具體數(shù)值。
AT_MOST(最大[限制]模式):父容器為子視圖指定一個最大尺寸SpecSize,View的大小不能大于這個值。對應LayoutParams中的wrap_content。-
決定View大小的因素:
因素:widthMeasureSpec和heightMeasureSpec
MeasureSpec值由 子View的布局參數(shù)LayoutParams 和 父容器的MeasureSpec值 共同決定。具體規(guī)則見下圖:
MeasureSpec創(chuàng)建規(guī)則
頂級View(即DecorView)的測量在ViewRootImpl的源碼里(getRootMeasureSpec方法):
//desire的這2個參數(shù)就代表屏幕的寬高,
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//decorView的measureSpec就是在這里確定的,其實比普通view的measurespec要簡單的多
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
- 流程:
performMeasure() 會調(diào)用 measure()(final方法),將計算的MeasureSpec傳遞給調(diào)用的onMeasure()方法,此方法會調(diào)用setMeasuredDimension()來設置自身大??;
如果是ViewGroup,先遍歷子View,測量出子View的MeasureSpec,再調(diào)用measureChild*相關方法讓子View通過調(diào)用measure方法來測量自己的大??;再根據(jù)所有子View的大小確定自身的大小,其中的子View會重復此類的measure過程,如此反復至完成整個View樹的遍歷,確定各個View自身的大小。
注意:
- ViewGroup中沒有
onMeasure方法- View會進行多次的測量,第一次測量和最終測量的實際寬高不一定相等,在layout流程中可確定View的實際寬高
- 獲取measure()后的寬高方法:
- Activity#onWindowFocusChange()中獲取
- view.post(Runnable)將獲取的代碼投遞到消息隊列的尾部
- ViewTreeObservable方法
.2、layout(布局)
布局目的:確定View的 最終寬高 和 四個頂點的位置
-
流程:
performLayout()會調(diào)用頂級View的layout()方法,其中調(diào)用setFrame()方法來設置其四個頂點(mLeft、mRight、mTop、mBottom);
接著調(diào)用onLayout()(空方法),此方法由具體實現(xiàn)的View自身重寫,用來確定自身位置,及循環(huán)其子View來確定坐標位置,子View?會循環(huán)調(diào)用setChildFrame()(就是調(diào)用View.layout())。
layout流程 layout和onLayout方法有什么區(qū)別?
layout是確定本身view的位置,通過serFrame方法設定本身view的四個頂點的位置。
onLayout是確定所有子元素的位置。
View和ViewGroup的 onLayout 方法都是空方法。都留給我們自己給子元素布局。
.3、draw(繪制)
- 繪制目的:顯示View
- 流程:
performMeasure()會調(diào)用 ViewRootImpl的draw方法,再調(diào)用drawSoftWare()方法,其中會調(diào)用mView.draw(),是真正繪制步驟的開始。繪制步驟如下:(1、3、4、6四步為主要步驟)
- 1、Draw the background:繪制背景 ——
drawBackground(canvas)
- If necessary, save the canvas' layers to prepare for fading:保存圖層
- Draw view's content:繪制view自身的內(nèi)容 ——
onDraw(canvas)
- Draw children:繪制子View(分發(fā)) ——
dispatchDraw(canvas)
- If necessary, draw the fading edges and restore layers:繪制一些圖層
- Draw decorations (scrollbars for instance):繪制裝飾(如滾動條) ——
onDrawForeground(canvas)
注:View中的dispatchDraw(canvas) 是空方法,ViewGroup中的dispatchDraw(canvas)調(diào)用了其drawChild方法
-
繪制流程圖:
draw流程 說明:
setWillNotDraw:用于設置繪制的標志位,view是否需要draw繪制。
若自定義的View 不需要draw,則可以設置這個方法為true。系統(tǒng)會根據(jù)此標記來優(yōu)化執(zhí)行速度。
ViewGroup 一般都默認設置這個為true,因為ViewGroup多數(shù)都是只負責布局,不負責draw的。
View 的這個標志位默認一般都是關閉的。
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
- 一般執(zhí)行動畫,會多次調(diào)用onDraw方法,通過監(jiān)聽動畫的參數(shù)值變化,不斷
invalidate,不斷重繪。
invalidate是在 主線程 中進行調(diào)用,會引發(fā)onDraw進行重繪
postInvalidate是在 子線程 中調(diào)用,最終調(diào)用的仍是invalidate
參考鏈接:
Android View繪制13問13答
要點提煉|開發(fā)藝術之View
二、View及ViewGroup的事件機制
1、相關概念:
- MotionEvent事件
- 事件類型:
ACTION_DOWN:手指剛接觸屏幕,按下去的那一瞬間產(chǎn)生該事件
ACTION_MOVE:手指在屏幕上移動時候產(chǎn)生該事件
ACTION_UP:手指從屏幕上松開的瞬間產(chǎn)生該事件- 事件序列:
從ACTION_DOWN開始到ACTION_UP結束我們稱為一個事件序列
- TouchSlop
系統(tǒng)所能識別的被認為是滑動的最小距離。
即當手指在屏幕上滑動時,如果兩次滑動之間的距離小于這個常量,那么系統(tǒng)就不認為你是在進行滑動操作。
該常量和設備有關,可用它來判斷用戶的滑動是否達到閾值,獲取方法:
ViewConfiguration.get(getContext()).getScaledTouchSlop()
VelocityTracker
速度追蹤,用于追蹤手指在滑動過程中的速度,包括水平和豎直方向的速度。GestureDetector
手勢檢測,用于輔助檢測用戶的單擊、滑動、長按、雙擊等行為。
2、事件分發(fā)的要點
事件分發(fā)的本質:
是對MotionEvent事件分發(fā)的過程,并將事件消費處理。事件分發(fā)的傳遞順序:
Activity(Window) --> ViewGroup --> View
即最終調(diào)用的是 View的dispatchTouchEvent(MotionEvent event)方法事件分發(fā)的重要方法:
1、dispatchTouchEvent(MotionEvent event):分發(fā)事件,返回boolean類型,表示事件是否消費
2、onInterceptTouchEvent(MotionEvent event):中斷事件(僅ViewGroup有),返回boolean類型,表示事件是否中斷
3、onTouchEvent(MotionEvent event):消費事件(僅View有),返回boolean類型,表示事件是否消費
4、perform*Click():執(zhí)行事件(僅View有),最終是onClick(View view),或長按、雙擊等事件處理。事件分發(fā)偽代碼:
// ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
// 事件是否被消費
boolean consume = false;
// 調(diào)用onInterceptTouchEvent判斷是否攔截事件
if (onInterceptTouchEvent(ev)) {
// 如果攔截則調(diào)用自身的onTouchEvent方法
consume = onTouchEvent(ev);
} else {
if (targetChild == null) {
// 沒有找到目標child,則調(diào)用父容器的分發(fā)方法
consume = super.dispatchTouchEvent(ev);
} else {
// 不攔截調(diào)用子View的dispatchTouchEvent方法
consume = child.dispatchTouchEvent(ev);
}
}
// 返回值表示事件是否被消費,true事件終止,false調(diào)用父View的onTouchEvent方法
return consume;
}
// View
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
// 是否實現(xiàn)了TouchListener#onTouch方法
if (onTouchListener != null) {
// 調(diào)用實現(xiàn)的onTouchListener#onTouch方法
consume = onTouchListener.onTouch(ev);
} else {
// onTouchEvent()中調(diào)用了perform*Click()等方法
consume = onTouchEvent(ev);
}
//返回值表示事件是否被消費,true事件終止,false調(diào)用父View的onTouchEvent方法
return consume;
}
3、事件分發(fā)的機制&流程
-
示意圖
View及ViewGroup事件流程示意圖
流程詳述:
- 1、ViewGroup分發(fā)開端:
Acivity#dispatchTouchEvent(MotionEvent)
事件最開始從Activity開始,由Acivity的dispatchTouchEvent方法來對事件進行分發(fā)。
// Activity源碼:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 事件分發(fā)并返回結果
if (getWindow().superDispatchTouchEvent(ev)) {
//事件被消費
return true;
}
// 無View 消費事件,則調(diào)用Activity#onTouchEvent方法
return onTouchEvent(ev);
}
PhoneWindow#superDispatchTouchEvent(MotionEvent)
getWindow().superDispatchTouchEvent(ev)是Window的抽象方法,具體由PhoneWindow實現(xiàn)
其內(nèi)部是調(diào)用的頂級View(DecorView)的superDispatchTouchEvent方法
// PhoneWindow源碼:
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
DecorView.superDispatchTouchEvent(MotionEvent)
頂級View(DecorView)一般為ViewGroup,其方法中調(diào)用了ViewGroup#dispatchTouchEvent方法
public boolean superDispatchTouchEvent(MotionEvent event) {
// 此處調(diào)用的是ViewGroup的dispatchTouchEvent方法
return super.dispatchTouchEvent(event);
}
- 2、ViewGroup事件分發(fā) —
onInterceptTouchEvent:
MotionEvent.ACTION_DOWN事件:
先判斷是否為MotionEvent.ACTION_DOWN事件,是,則清除FLAG_DISALLOW_INTERCEPT設置并且mFirstTouchTarget 設置為null,然后根據(jù)條件調(diào)用onInterceptTouchEvent方法,來處理攔截事件。
如果不是MotionEvent.ACTION_DOWN事件,且mFirstTouchTarget == null,則直接設置中斷標記為true(intercepted = true),ViewGroup直接攔截其他事件(如MOVE和UP等)進行處理。
FLAG_DISALLOW_INTERCEPT標志位:
如果通過requestDisallowInterceptTouchEvent方法設置了此標志位,則子View可以以此來干預父View的事件分發(fā)過程(ACTION_DOWN事件除外,上面的原因),而這就是我們處理滑動沖突常用的關鍵方法。
// ViewGroup源碼:
public boolean dispatchTouchEvent(MotionEvent 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);
//清除FLAG_DISALLOW_INTERCEPT設置并且mFirstTouchTarget 設置為null
resetTouchState();
}
// Check for interception.
final boolean intercepted;//是否攔截事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//FLAG_DISALLOW_INTERCEPT是子View通過
//requestDisallowInterceptTouchEvent方法進行設置的
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//調(diào)用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;
}
...
}
-
3、ViewGroup事件分發(fā) — 子View遍歷
mFirstTouchTarget:
當ViewGroup不攔截事件,則遍歷子View,找到事件接收的目標View(mFirstTouchTarget),條件:- View可見且沒有播放動畫:
canViewReceivePointerEvents方法 - 事件的坐標落在View的范圍內(nèi):
isTransformedTouchPointInView
當mFirstTouchTarget不為null,則說明已經(jīng)找到過了目標child,則newTouchTarget不為null,會跳出循環(huán)。
但此時還沒有將事件分發(fā)給子View,所以newTouchTarget為null,mFirstTouchTarget也是null。
如果dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法返回了true,即子View消費了事件,則會將mFirstTouchTarget進行賦值為該子View,終止子View的遍歷。此時,子View的遍歷完成。
在dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法中,如果子View不為null,則調(diào)用了子View的child.dispatchTouchEvent分發(fā)方法,進行View的分發(fā)。 - View可見且沒有播放動畫:
// ViewGroup源碼:
public boolean dispatchTouchEvent(MotionEvent ev) {
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;
}
//判斷1,View可見并且沒有播放動畫。2,點擊事件的坐標落在View的范圍內(nèi)
//如果上述兩個條件有一項不滿足則continue繼續(xù)循環(huán)下一個View
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
//如果有子View處理即newTouchTarget 不為null則跳出循環(huán)。
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第三個參數(shù)child這里不為null
//實際調(diào)用的是child的dispatchTouchEvent方法
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();
//當child處理了點擊事件,那么會設置mFirstTouchTarget 在addTouchTarget被賦值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
//子View處理了事件,然后就跳出了for循環(huán)
break;
}
}
}
dispatchTransformedTouchEvent方法:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
mFirstTouchTarget的賦值:
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
- 4、ViewGroup事件分發(fā) —
View#dispatchTouchEvent:
若遍歷子View后,ViewGroup沒有找到事件處理者(① ViewGroup沒有子View 或 ② 子View處理了事件卻在dispatchTouchEvent方法返回了false),則ViewGroup會去處理這個事件。
即dispatchTouchEvent方法返回了false,mFirstTouchTarget 必然為null,則再次調(diào)用自身的dispatchTransformedTouchEvent方法(但傳入的child為null),其內(nèi)部會調(diào)用super.dispatchTouchEvent(event);方法,將調(diào)用View#dispatchTouchEvent,將事件傳給View,至此,ViewGroup的分發(fā)過程完成。
// 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);
}
- 5、View的事件分發(fā) —
View#dispatchTouchEvent
mOnTouchListener.onTouch監(jiān)聽:
View中首先判斷是否設置了OnTouchListener監(jiān)聽(開發(fā)者自己實現(xiàn)的),若設置了且onTouch返回true,則之后的onTouchEvent方法不會調(diào)用;若沒有設置監(jiān)聽或onTouch返回false,則會調(diào)用onTouchEvent方法。
onTouchEvent方法:
在此方法中,具體的處理了各個事件。
如果View設置成了disabled狀態(tài)(即不可用),只要CLICKABLE和LONG_CLICKABLE有一個為true,就一定會消費這個事件(即onTouchEvent返回true),只是它看起來不可用。只有不可點擊(clickable和longClickable同時為false),才會返回false,即onTouchEvent不消費此事件。
performClick()方法:
就點擊事件而言,在ACTION_UP事件的條件下,會調(diào)用performClickInternal方法(內(nèi)部實際是performClick());在performClick()方法中,如果設置了OnClickListener,則會回調(diào)onClick方法。
dispatchTouchEvent(MotionEvent)
// View源碼:
//如果窗口沒有被遮蓋
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
//當前監(jiān)聽事件
ListenerInfo li = mListenerInfo;
//需要特別注意這個判斷當中的li.mOnTouchListener.onTouch(this, event)條件
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//result為false調(diào)用自己的onTouchEvent方法處理
if (!result && onTouchEvent(event)) {
result = true;
}
}
onTouchEvent(MotionEvent)
// View源碼:
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:
...
if (!post(mPerformClick)) {
// 其內(nèi)部調(diào)用的是performClick方法
performClickInternal();
}
...
break;
...
}
return true;
}
return false;
}
performClick()
// View源碼:
/**
* Call this view's OnClickListener, if it is defined. Performs all normal
* actions associated with clicking: reporting accessibility event, playing
* a sound, etc.
*
* @return True there was an assigned OnClickListener that was called, false
* otherwise is returned.
*/
public boolean performClick() {
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);
return result;
}
說明: setClickable 失效的原因:
View的setOnClickListener會默認將View的clickable設置成true。
View的setOnLongClickListener同樣會將View的longClickable設置成true。
// View源碼:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
4、事件分發(fā)的相關問題:
-- 問題:如果一個事件序列的 ACTION_DOWN 事件被 ViewGroup 攔截,此時子 View 調(diào)用 requestDisallowInterceptTouchEvent 方法有沒有用?
子View可以通過 requestDisallowInterceptTouchEvent方法干預父View的事件分發(fā)過程(ACTION_DOWN事件除外),而這就是我們處理滑動沖突常用的關鍵方法。
在
requestDisallowInterceptTouchEvent中,設置了FLAG_DISALLOW_INTERCEPT標志位,表示子View不希望此父級及其祖先使用ViewGroup.onInterceptTouchEvent(MotionEvent)攔截觸摸事件。
而 ACTION_DOWN 事件是清除了這個標志位的,所以,requestDisallowInterceptTouchEvent 的設置對ACTION_DOWN無效。
// ViewGroup源碼,實現(xiàn)的是ViewParent的抽象方法
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
// ViewGroup#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
...
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
....
//清除FLAG_DISALLOW_INTERCEPT設置并且mFirstTouchTarget 設置為null
resetTouchState();
}
//是否攔截事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//FLAG_DISALLOW_INTERCEPT是子View通過
//requestDisallowInterceptTouchEvent方法進行設置的
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//調(diào)用onInterceptTouchEvent方法判斷是否需要攔截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
}
-- 問題:ACTION_DOWN 事件被子 View 消費了,那 ViewGroup 能攔截剩下的事件嗎?如果攔截了剩下事件,當前這個事件 ViewGroup 能消費嗎?子 View 還會收到事件嗎?
在ACTION_DOWN 事件被子 View 消費后,mFirstTouchTarget 則不為null了,就會直接攔截其他事件,intercepted = true;,見下面的源碼。
設置了攔截,就不會再遍歷子View進行事件分發(fā)了。則會取消cancelChild,攔截子View的事件。
// ViewGroup#dispatchTouchEvent
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// onInterceptTouchEvent方法的判斷
......
} else {
// 重點在這里
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true; // <---------------------------------------重點
}
....
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// 沒有子 View 消費事件,則傳入 null 去分發(fā),最終調(diào)用的是自身的 onTouchEvent 方法,進行處理 touch 事件
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 (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 如果 intercepted 就取消 cancelChild,這便是攔截子 View 事件的原理
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted; // <---------------------------------------重點
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
//內(nèi)部會比較 pointerIdBits 和當前事件的 pointerIdBits,一致才會處理
//這便是 Down 事件處理后后續(xù)事件都交給該 View 處理的原理
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
// 沒有next則為null,就結束了循環(huán)
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
-- 問題:當 View Disable 時,會消費事件嗎?
會消費事件,只是設為了不可用,可以看到,在源碼中的注釋為:
A disabled view that is clickable still consumes the touch events, it just doesn't respond to them.
[一個可點擊的禁用view,仍然可以消費事件,它只是沒有響應它們(事件)而已。]
// View#onTouchEvent
// 判斷是否不可用,但仍會消費事件
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;
}
參考鏈接:
一文讀懂Android View事件分發(fā)機制
必問的事件分發(fā),你答得上來嗎
Android事件分發(fā)機制詳解:史上最全面、最易懂
要點提煉|開發(fā)藝術之View





