在Android進階知識:繪製流程(上)中主要是關於繪製流程中會遇到的基礎知識,這一篇開始來看具體View
繪製的流程。前篇中講過View
分爲ViewGroup
和通常的View
,ViewGroup
中能夠包含其餘View
或ViewGroup
,而且ViewGroup
繼承了View
,因此繪製流程中ViewGroup
相比通常View
除了要繪製自身還要繪製其子View
。View
的繪製流程分爲三個階段:測量measure
、佈局layout
、繪製draw
,接下來就開始對View
和ViewGroup
繪製流程的這三個階段一個個開始研究。android
View
的測量流程從View
的measure
方法開始,因此先來看它的measure
方法。canvas
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//optical用來判斷是否使用了光學邊界佈局的ViewGroup
boolean optical = isLayoutModeOptical(this);
//判斷當前View的mParent是否與本身同樣使用了光學邊界佈局的ViewGroup
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
//生成一個key做爲測量結果緩存的key
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
//緩存爲空就new一個新的LongSparseLongArray
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
//是否強制layout
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
//上次測量和此次結果是否相同
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
//寬高測量模式是否皆爲EXACTLY模式
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
//MeasureSpec中的size測量大小是否相等
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
//是否須要layout
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
//判斷是否須要layout或者強制layout
if (forceLayout || needsLayout) {
//清除已測量的flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
//測量以前還有先判斷緩存若是是強制layout則爲-1,不然去緩存中根據以前生成的key找對應的value
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
//若是cacheIndex小於0即強制layout或者忽略緩存
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
//就調用onMeasure方法測量寬高
onMeasure(widthMeasureSpec, heightMeasureSpec);
//onMeasure測量結束後再修改狀態flag
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
//不然就用緩存中的值
long value = mMeasureCache.valueAt(cacheIndex);
//經過setMeasuredDimensionRaw方法將緩存值賦給成員變量
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
//這裏對複寫onMeasure方法卻未調用setMeasuredDimension方法作了校驗拋出異常
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
//更新mOldWidthMeasureSpec,mOldHeightMeasureSpec
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
//更新緩存
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
複製代碼
註釋已經很詳細了,這裏再梳理總結一下measure
方法作了哪些事。其實measure
方法主要功能就是調用onMeasure
方法測量大小,可是不是每次都調用,這裏作了優化。先要判斷是不是須要layout
或者是強制layout
。判斷經過以後還要再判斷是不是強制測量或者忽略緩存,若是強制測量或者忽略緩存纔會去調用onMeasure
方法區測量,不然直接用緩存中上一次測量的緩存就行。數組
接下來看onMeasure
測量方法:緩存
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製代碼
onMeasure
方法裏就調用了setMeasuredDimension
方法。安全
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
複製代碼
setMeasuredDimension
方法裏又調用了setMeasuredDimensionRaw
方法,這裏就和以前從緩存中取數據調用方法是同樣的了,這裏將測量好的寬高賦給成員變量。回頭再看onMeasure
方法裏的setMeasuredDimension
方法傳參,這裏傳參都先調用了getDefaultSize
方法,而getDefaultSize
方法又調用了getSuggestedMinimumWidth
和getSuggestedMinimumHeight
方法。一個一個來看,這裏Width
和Height
方法是相似的,因此就看getSuggestedMinimumWidth
方法便可。bash
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
複製代碼
這個方法裏判斷了View
是否有mBackground
,沒有背景就返回View
的最小寬度,有背景就從View
的最小寬度和背景的最小寬度中選較大的返回。這裏的最小寬度能夠在xml
裏經過相關屬性設置或者調用setMinimumWidth
方法設置。app
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minWidth="100dp"
android:minHeight="200dp"
/>
複製代碼
public void setMinimumWidth(int minWidth) {
mMinWidth = minWidth;
requestLayout();
}
複製代碼
接下來看getDefaultSize
方法:ide
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
複製代碼
這裏傳遞的參數size
就是getSuggestedMinimumWidth
方法得到View
自身想要的大小,measureSpec
是View
的父佈局的MeasureSpec
信息。而後根據父佈局MeasureSpec
信息中的specMode
和specSize
來返回測量結果result
。父佈局specMode
爲UNSPECIFIED
即不限制大小因此result
就爲View
想要的大小size
。specMode
爲AT_MOST
或者EXACTLY
,則都返回父佈局的specSize
大小.佈局
從這裏就能夠看出View
中默認AT_MOST
或者EXACTLY
模式返回測量大小同樣的,因此在自定義View
的時,單單隻繼承了View
以後,默認寬高設置對應的MATCH_PARENT
和WRAP_CONTENT
屬性返回的結果也是同樣的,即默認的View
中WRAP_CONTENT
是無效的,須要咱們本身複寫onMeasure
方法設置AT_MOST
模式下的最小測量值。至此View
的measure
流程就結束了,下面用一張流程圖來梳理下View
的測量流程。post
接下來看View
的佈局流程,從layout
方法開始。
public void layout(int l, int t, int r, int b) {
//先判斷在layout以前是否須要先調用onMeasure測量
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//這裏仍是根據isLayoutModeOptical方法判斷mParent是否使用光學邊界佈局,分別調用setOpticalFrame和setFrame方法
//setOpticalFrame方法中計算Insets後一樣調用了setFrame方法
//最終返回一個布爾值changed表示佈局是否發生變化
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//若是發生了改變或者須要進行layout
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//就調用onLayout方法
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
//layout以後調用onLayoutChange方法
//獲取ListenerInfo
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
//獲取ListenerInfo不爲空且mOnLayoutChangeListeners就克隆一份OnLayoutChangeListener集合
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
//循環調用OnLayoutChangeListener的onLayoutChange方法
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
//修改強制layout的flag
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
//最後是一些關於焦點的處理
if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
//若是自身能獲取焦點就清除父母的焦點
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
//ViewRootImpl爲空或者ViewRootImpl不在layout
//這裏是一種奇怪的狀況,多是用戶而不是ViewRootImpl調用layout方法,最安全的作法在這裏仍是清除焦點
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
clearParentsWantFocus();
} else if (!hasParentWantsFocus()) {
// original requestFocus was likely on this view directly, so just clear focus
clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
// otherwise, we let parents handle re-assigning focus during their layout passes.
} else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
View focused = findFocus();
if (focused != null) {
// Try to restore focus as close as possible to our starting focus.
if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
}
}
}
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
複製代碼
總結一下layout
方法中最主要的就是調用setFrame
方法返回一個佈局是否發生變化的結果,判斷髮生了變化就會調用onLayout
方法從新計算佈局位置,其它還有一些特殊狀況的處理、監聽的調用和焦點的處理。下面就來看setFrame
方法。
protected boolean setFrame(int left, int top, int right, int bottom) {
//默認change爲false
boolean changed = false;
if (DBG) {
Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
//比較新舊left、top、right、bottom
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
//只要有一個不相等就說明有改變
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
//分別計算舊的寬高和新的寬高
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
//新舊有不相等sizeChanged就爲true
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
//更新新的left、top、right、bottom的值
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
//設置更新視圖顯示列表
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
//若是發生了改變調用sizeChange方法
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
//若是是可見的,就強制加上PFLAG_DRAWN狀態位
//防止在調用setFrame方法前PFLAG_DRAWN狀態爲被清除
//保證invalidate方法執行
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
// parent display list may need to be recreated based on a change in the bounds
// of any child
invalidateParentCaches();
}
// Reset drawn bit to original value (invalidate turns it off)
mPrivateFlags |= drawn;
mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
複製代碼
setFrame
方法中會比較新舊mLeft
、mTop
、mRight
、mBottom
,這四個值就是第一篇文章中提到的View
的四個get
方法獲取的值,表示本身相對於父佈局的位置。只要有一個不一樣就說明發生了變化,而後調用mRenderNode.setLeftTopRightBottom
方法設置更新視圖顯示列表,返回change結果爲true
。接下來進入查看onLayout
方法。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
複製代碼
View
的onLayout
方法是一個空方法。由於單個普通View
是沒有子View
的,他自身的位置計算在layout
方法中已經計算好了。若是是ViewGroup
則必須要複寫onLayout
方法,根據自身要求設置子View
的位置。
一樣先從View
的draw
方法開始閱讀。
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* draw遍歷執行的幾個步驟:
*
* 1. 繪製背景
* 2. 若是有必要,保存Canvas圖層
* 3. 繪製內容
* 4. 繪製子元素
* 5. 若是有必要,繪製邊緣和恢復圖層
* 6. 繪製裝飾 (例如滾動條scrollbars)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
......
// Step 2, save the canvas's layers int paddingLeft = mPaddingLeft; final boolean offsetRequired = isPaddingOffsetRequired(); if (offsetRequired) { paddingLeft += getLeftPaddingOffset(); } ...... // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); ...... // Step 5, draw the fade effect and restore layers final Paint p = scrollabilityCache.paint; final Matrix matrix = scrollabilityCache.matrix; final Shader fade = scrollabilityCache.shader; ...... // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); } 複製代碼
View
中的draw
方法註釋和邏輯就比較清晰了,一共主要分爲六個步驟繪製:
首先是drawBackground
方法:
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
//設置背景邊界
setBackgroundBounds();
......
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
//直接繪製背景
background.draw(canvas);
} else {
//若是 mScrollX 和 mScrollY 有值 則將畫布canvas平移
canvas.translate(scrollX, scrollY);
//以後在調用draw方法繪製背景
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
複製代碼
是drawBackground
方法中主要是作了兩個操做,一個是設置背景邊界,另外一個是繪製背景。若是有mScrollX
和mScrollY
有值說明有發生滾動,就先移動畫布以後再進行繪製。接下來看一下onDraw
方法:
protected void onDraw(Canvas canvas) {
}
複製代碼
和onLayout
同樣也是一個空方法,不一樣View
須要根據需求實現不一樣內容的繪製。再下來是dispatchDraw
方法。
protected void dispatchDraw(Canvas canvas) {
}
複製代碼
dispatchDraw
一樣仍是空方法,仍是由於單獨普通的View
是沒有子View
的因此不須要繪製子View
。最後來看onDrawForeground
方法繪製裝飾。
public void onDrawForeground(Canvas canvas) {
//繪製滾動指示器
onDrawScrollIndicators(canvas);
//繪製滾動條
onDrawScrollBars(canvas);
final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
if (foreground != null) {
if (mForegroundInfo.mBoundsChanged) {
mForegroundInfo.mBoundsChanged = false;
final Rect selfBounds = mForegroundInfo.mSelfBounds;
final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
if (mForegroundInfo.mInsidePadding) {
selfBounds.set(0, 0, getWidth(), getHeight());
} else {
selfBounds.set(getPaddingLeft(), getPaddingTop(),
getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
}
final int ld = getLayoutDirection();
Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
foreground.setBounds(overlayBounds);
}
//繪製前景
foreground.draw(canvas);
}
}
複製代碼
onDrawForeground
方法中主要是繪製了滾動指示器、滾動條和前景相關內容。View
的draw
流程就是這些,流程圖以下。
ViewGroup
的測量流程的起始和通常的View
是同樣的,也是從measure
開始,可是ViewGroup
類中並無複寫measure
方法,因此調用的也就是其父類View
中的measure
方法,因此這一部分ViewGroup
與普通View
是相同的。以後咱們知道在measure
方法中會調用onMeasure
方法進行測量。可是默認ViewGroup
類中依舊沒有複寫onMeasure
方法,這是由於每一個ViewGroup
的特性不一樣需求不一樣,須要根據要求本身複寫onMeasure
方法。onMeasure
方法的複寫邏輯是這樣的:首先在onMeasure
方法中須要測量每一個子View
的大小,接着計算全部子View
總的大小,最後經過setMeasuredDimension
方法,將獲得的總大小設置保存到成員變量中,完成測量。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
//全部子View數組
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//測量每一個子View大小,傳入子View和父佈局大小
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
複製代碼
ViewGroup
提供有measureChildren
這個用來測量子View
的方法。在measureChildren
方法中得到了全部子View
的數組,而後調用measureChild
方法,測量每一個子View
。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//得到子View的LayoutParams
final LayoutParams lp = child.getLayoutParams();
// 根據父佈局的MeasureSpec和子View的LayoutParams,計算子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//調用子View的measure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
measureChild
方法中獲取了子View
的LayoutParam
佈局參數。而後經過getChildMeasureSpec
方法傳入父佈局的MeasureSpec
和子View
的LayoutParams
獲取到子View
的MeasureSpec
,最後調用了子View
的measure
方法,完成最後的測量。那麼下面進入getChildMeasureSpec
方法,看一下實現。
/**
* 這三個參數分別爲:
* spec:父佈局的MeasureSpec
* padding:內邊距
* childDimension:子View的寬高尺寸
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//獲取父佈局的specMode和specSize
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//計算父佈局大小:用父佈局的specSize減去內邊距
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
//根據父佈局的specMode判斷
switch (specMode) {
case MeasureSpec.EXACTLY:
//父佈局的specMode爲EXACTLY時
if (childDimension >= 0) {
//childDimension大於0即子View的寬或高有具體的值
//返回的大小就爲childDimension大小,mode爲EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//若是子View寬或高爲MATCH_PARENT
//返回的大小就爲父佈局大小,mode爲EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//若是子View寬或高WRAP_CONTENT
//返回的大小就由子View本身肯定,最大不能超過父佈局大小,mode因此爲AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//父佈局的specMode爲AT_MOST時
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//childDimension大於0即子View的寬或高有具體的值
//返回的大小就爲childDimension大小,mode爲EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//若是子View寬或高爲MATCH_PARENT
//返回的大小就爲父佈局大小,mode爲AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//若是子View寬或高WRAP_CONTENT
//返回的大小就由子View本身肯定,最大不能超過父佈局大小,mode因此爲AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//父佈局的specMode爲UNSPECIFIED時
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//childDimension大於0即子View的寬或高有具體的值
//返回的大小就爲childDimension大小,mode爲EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//若是子View寬或高爲MATCH_PARENT
//返回的大小根據View.sUseZeroUnspecifiedMeasureSpec判斷,mode爲UNSPECIFIED
//sUseZeroUnspecifiedMeasureSpec是View類的成員變量由targetSdkVersion版本決定
//sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//若是子View寬或高爲WRAP_CONTENT
//返回的大小根據View.sUseZeroUnspecifiedMeasureSpec判斷,mode爲UNSPECIFIED
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//調用makeMeasureSpec方法建立一個MeasureSpec返回
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼
從這個方法能夠看出來一個子View
的MeasureSpec
是由其父佈局的MeasureSpec
和自身的LayoutParams
共同決定的,根據不一樣的組合最終返回不一樣的結果。在測量完全部的子View
以後,須要實現計算ViewGroup
總大小,最後調用setMeasuredDimension
方法存儲測量的結果就能夠了。ViewGroup
的measure
流程圖以下。
和單個普通View
同樣,ViewGroup
的layout
流程一樣從layout
方法開始。
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
mLayoutCalledWhileSuppressed = true;
}
}
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
複製代碼
ViewGroup
中複寫了layout
方法,添加了一些動畫過分效果的判斷,核心仍是調用了super.layout
方法,繼而會調用onLayout
方法,onLayout
方法是一個抽象方法,這是由於ViewGroup
一樣要根據自身要求特性實現不一樣的layout
邏輯,因此須要由不一樣的ViewGroup
自身來實現onLayout
方法。接下來看一下具體的一個ViewGroup
中的onLayout
方法,LinearLayout
中的onLayout
方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
複製代碼
根據不一樣方向分爲兩個方法,這裏來看layoutVertical
方法:
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// 計算子View最右的位置
final int width = right - left;
int childRight = width - mPaddingRight;
// 子View的空間
int childSpace = width - paddingLeft - mPaddingRight;
// 得到子View個數
final int count = getVirtualChildCount();
......
// 循環遍歷子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
// 可見性不爲GONE的子View得到其寬高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
// 得到子View的LayoutParams
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
......
//計算topMargin
childTop += lp.topMargin;
//調用setChildFrame方法,該方法中調用了子View的layout方法來肯定位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
// childTop增長子View的高度和bottomMargin,使下一個子View垂直在下方放置
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
複製代碼
這裏省略了Gravity
屬性的相關代碼,來看主要的流程,首先是去除內邊距計算好子View
的放置空間,而後遍歷子View
得到子View
的寬高,再經過setChildFrame
方法肯定位置,而後遞增childTop
,這樣接計算好了子View
的位置,使子View
一個個垂直排列下去。LinearLayout
的onLayout
方法主要實現方法就是這樣,其餘不一樣的ViewGroup
也都實現了本身的onLayout
方法。
ViewGroup
繪製流程一樣會調用View
的draw
方法,一樣會繪製背景,繪製自身內容等,不一樣的是dispatchDraw
方法,View
中該方法是一個空方法,可是ViewGroup
中就須要複寫這個方法,來看ViewGroup
中的dispatchDraw
方法。
@Override
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
//得到全部子View個數和子View數組
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
......
//循環遍歷子View
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 調用drawChild方法繪製子View
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
......
}
複製代碼
dispatchDraw
方法裏核心邏輯仍是循環遍歷了子View
,而後調用了drawChild
繪製子View
,而drawChild
中又調用了子View
的draw
方法繪製子View
。
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
複製代碼
這一篇主要講了View
和ViewGroup
的繪製流程,不管View
和ViewGroup
都經歷了measure
、layout
、draw
這三個階段。之間主要的區別就是View
沒有子View
的問題,而ViewGroup
的繪製流程中還須要調用其子View
的對應流程,完成子View
的繪製。那麼在瞭解了View
和ViewGroup
的繪製流程以後,新的問題又來了。View
樹中,子View
的了measure
、layout
、draw
方法是由其父級的ViewGroup
調用的,父ViewGroup
繪製又是由它的父級調用的,那麼根節點的繪製流程是由誰調用開啓的呢?繪製好的界面又是怎樣顯示的呢?最後一篇文章就來看看這些內容。