從本章節開始,我們來共同學習下Android中比較重要的View相關知識,這一節我們先來看View的繪製流程。
我們知道,Android中的任何一個佈局、控件,其最終都會直接或者間接地繼承View類(ViewGroup最終也繼承了View),也就是說所有相關的控件或者佈局都會使用同樣的繪製流程。我們知道Android繪製流程的起點是在ViewRootImpl類的performTraversals()方法。這個方法主要作用是根據之前設置狀態來判斷是否需要重新計算視圖大小(Measure),是否需要重新放置視圖位置(layout),以及是否需要重新繪製View(draw)。這個方法裏有巨量的代碼和邏輯,詳讀下來太過傷神,我們刪減代碼看下這個類的核心內容:
private void performTraversals() {
...
//獲取了尺寸規格,這是一個32位的int值,前兩位代表測量模式(SpecMode),後30位代表數值(SpecSize),
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
//後續所有View的MeasureSpec都是從這裏來的
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
mView.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
mView.draw(canvas);
}
可以看到,這裏通過performTraversals()來判斷是否對控件進行測量放置繪製等等,與View繪製過程相關的幾個重要步驟有measure(測量)、layout(位置)、draw(繪製),其執行流程是:
下面我們就這三個步驟一一來進行講解:
Measure
我們知道,我們的所有控件佈局歸根結底都是ViewGroup或者View(ViewGroup也是一種特殊的View,在談到繪製流程和事件分發的時候我們都會將兩者區分開來講)。我們先來看看View類的measure方法:
/**
* 這個方法是爲了確定View的大小,並且會爲子View提供寬高的約束條件
*爲整個View樹計算實際的大小,然後設置實際的高和寬,每個View控件的實際寬高都是由父視圖和自身決定的。
*實際的測量是在onMeasure方法進行,所以在View的子類需要重寫onMeasure方法,
*這是因爲measure方法是final的,不允許重載,所以View子類只能通過重載onMeasure來實現自己的測量邏輯。
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
/**
* 繼承View若有需求需要複寫該方法,因爲measure是final的
*
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
我們從上面ViewRootImpl的performTraversals方法可以看到,這裏measure方法的兩個尺寸規格是由其父控件傳過來的,寬高的尺寸規格是一個32位的int型變量,其中存儲了測量所需要的信息。其中前兩位存儲了測量模式,測量模式總共有三種:
- MeasureSpec.EXACTLY:表示確定大小
- MeasureSpec.AT_MOST:表示獲取最大尺寸
- MeasureSpec.UNSPECIFIED:不確定
後三十位保存了大小也就是SpecSize,也就是父控件的大小。對於系統Window類的DecorVIew對象Mode一般都爲MeasureSpec.EXACTLY ,而size分別對應屏幕寬高。對於子View來說大小是由父View和子View共同決定的。
對於非ViewGroup的View來說,具體測量會在onMeasure中完成,View默認的setMeasuredDimension就可以很好地幫助系統設置View的大小。我們來看看View默認進行measure,最後設置的寬高是多少呢:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
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;
}
可以看到, 這裏獲取了測量模式,並且根據不同的測量模式對寬高進行了初始化,若是UNSPECIFIED,則設置爲最小值(最小值由該View的背景和minHeight屬性決定),若爲AT_MOST或者EXACTLY,則設置爲父控件的最大寬高。那麼到這裏最底部View的測量流程就完成了。
前面我們也說到ViewGroup的測量過程和View有些許不同:
我們可以看到ViewGroup中給我們提供了四個供我們測量使用的方法,分別是measureChildren、measureChild、measureChildWithMargin、getChildMeasureSpec
我們知道View是一個嵌套的樹結構,其measure執行流程也是一個遞歸的過程。我們以一個ViewGroup的子類LinearLayout的測量過程來看:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
}
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
可以看到LinearLayout經過一系列調用之後,走到了ViewGroup的measureChildWithMargins方法,下面我們來看一看這個方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
可以很明顯地看到,這裏將子View的padding信息和margin信息以及其他子View已經佔用的大小相加然後進行計算,獲取了子View的尺寸規格,然後調用子View的measure方法完成測量。這裏有一個很重要的方法getChildMeasureSpec(),我們來看看這個方法中完成了什麼操作:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//獲取測量模式和測量尺寸
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//獲取最大尺寸
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
//根據測量模式分別計算
switch (specMode) {
//該ViewGroup測量模式是EXACTLY確定大小
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//子View的寬高大於等於0,說明設置過,則將其設置爲EXACTLY,size爲原大小
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子View請求獲取父控件大小,設置爲EXACTLY並將父控件剩餘最大尺寸設置給該測量模式
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子View要求獲取自適應大小,設置爲AT_MOST並將父控件剩餘最大尺寸設置給該測量模式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//該ViewGroup測量模式爲AT_MOST
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//子View的寬高大於等於0,說明設置過,則將其設置爲EXACTLY,size爲原大小
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子View請求獲取父控件大小,設置爲與父控件相同的AT_MOST並將父控件剩餘最大尺寸設置給該測量模式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子View要求獲取自適應大小,設置爲AT_MOST並將父控件剩餘最大尺寸設置給該測量模式
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//該ViewGroup測量模式爲UNSPECIFIED
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//子View的寬高大於等於0,說明設置過,則將其設置爲EXACTLY,size爲原大小
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子View請求獲取父控件大小,設置爲與父控件相同的UNSPECIFIED並將父控件剩餘最大尺寸設置給該測量模式
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子View要求獲取自適應大小,設置爲UNSPECIFIED並將父控件剩餘最大尺寸設置給該測量模式
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//返回一個MesureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
可以看到,該方法使用自身的MesureSpec和子View的寬高值來最終確定子View的MesureSpec。
到這裏我們就搞清楚了ViewGroup的測量過程:
- 首先調用ViewGroup的mesure方法,我們一般繼承ViewGroup會複寫其onMeasure方法,如前面的LinearLayout所示
- 調用ViewGroup的onMeasure方法,在方法裏對其所有子View進行遍歷並測量子View的大小
- 使用自身measure方法中產生的MeasureSpec和子View大小來確定子View的尺寸規格,並交給子VIew的measure方法去測量
- measure會默認調用setMeasuredDimension()通過最小尺寸和子VIew的MeasureSpec來完成最終測量。
測量過程總結
- MeasureSpec:測量規格,32位的int類型數字,前兩位保存測量模式SpecMode,後三十位保存具體尺寸SpecSize。其中SpecMode分爲三種:
MeasureSpec.EXACTLY //確定大小,父View希望子View的大小是確定的,由specSize決定;
MeasureSpec.AT_MOST //最大尺寸,父View希望子View的大小最多是specSize指定的值;
MeasureSpec.UNSPECIFIED //未指定,父View完全依據子View的設計值來決定;
- 若要複寫View的measure方法,由於measure方法是final關鍵字修飾的因此不能被覆蓋,若需要可以複寫onMeasure方法
- 最頂層DecorView測量時的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法確定的(LayoutParams寬高參數均爲MATCH_PARENT,specMode是EXACTLY指定大小,specSize爲屏幕大小)
- ViewGroup內部提供了measureChildren、measureChild、measureChildWithMargin、getChildMeasureSpec等方法簡化其子類獲取MeasureSpec的方式
- 一個ViewGroup的大小由其子View和其父View共同決定
- View的測量流程是measure -> onMeasure -> setMeasuredDimension
layout
與測量流程類似,layout的過程也是一個遞歸調用,我們將目光轉回到iewRootImpl類的performTraversals()方法中,可以看到繼measure方法之後,緊着着調用了mView.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
這麼一個方法,我們來查看這個方法的代碼:
public void layout(int l, int t, int r, int b) {
...
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
...
onLayout(changed, l, t, r, b);
...
}
}
與測量過程相類似,最終layout方法調用了onLayout,View類中的onLayout是一個空方法
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
那我們順藤摸瓜,去看看ViewGroup中的layout和onLayout:
@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 {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
/**
* {@inheritDoc}
*/
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
這裏可以看到layout方法是一個final修飾的,不能被子類複寫的方法,而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);
}
}
//橫向排列
void layoutHorizontal(int left, int top, int right, int bottom) {
final boolean isLayoutRtl = ViewUtils.isLayoutRtl(this);
final int paddingTop = getPaddingTop();
int childTop;
int childLeft;
//獲取當前ViewGroup高度(bottom - top)
final int height = bottom - top;
//獲取底部View的位置
int childBottom = height - getPaddingBottom();
//ViewGroup可供子View 顯示的高度
int childSpace = height - paddingTop - getPaddingBottom();
//獲取子View個數
final int count = getVirtualChildCount();
//獲取Gravity相關信息
final int majorGravity = mGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final boolean baselineAligned = mBaselineAligned;
final int[] maxAscent = mMaxAscent;
final int[] maxDescent = mMaxDescent;
final int layoutDirection = ViewCompat.getLayoutDirection(this);
//根據Gravity來設置childLeft值
switch (GravityCompat.getAbsoluteGravity(majorGravity, layoutDirection)) {
case Gravity.RIGHT:
// mTotalLength contains the padding already
childLeft = getPaddingLeft() + right - left - mTotalLength;
break;
case Gravity.CENTER_HORIZONTAL:
// mTotalLength contains the padding already
childLeft = getPaddingLeft() + (right - left - mTotalLength) / 2;
break;
case Gravity.LEFT:
default:
childLeft = getPaddingLeft();
break;
}
int start = 0;
int dir = 1;
//In case of RTL, start drawing from the last child.
if (isLayoutRtl) {
start = count - 1;
dir = -1;
}
//對各個子View進行遍歷
for (int i = 0; i < count; i++) {
int childIndex = start + dir * i;
final View child = getVirtualChildAt(childIndex);
if (child == null) {
childLeft += measureNullChild(childIndex);
} else if (child.getVisibility() != GONE) {
//獲取子View的寬度和高度(onMeasure計算出來的)
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
int childBaseline = -1;
final LinearLayoutCompat.LayoutParams lp =
(LinearLayoutCompat.LayoutParams) child.getLayoutParams();
if (baselineAligned && lp.height != LayoutParams.MATCH_PARENT) {
childBaseline = child.getBaseline();
}
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
//根據Gravity來設置childTop
switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) {
case Gravity.TOP:
childTop = paddingTop + lp.topMargin;
if (childBaseline != -1) {
childTop += maxAscent[INDEX_TOP] - childBaseline;
}
break;
case Gravity.CENTER_VERTICAL:
// Removed support for baseline alignment when layout_gravity or
// gravity == center_vertical. See bug #1038483.
// Keep the code around if we need to re-enable this feature
// if (childBaseline != -1) {
// // Align baselines vertically only if the child is smaller than us
// if (childSpace - childHeight > 0) {
// childTop = paddingTop + (childSpace / 2) - childBaseline;
// } else {
// childTop = paddingTop + (childSpace - childHeight) / 2;
// }
// } else {
childTop = paddingTop + ((childSpace - childHeight) / 2)
+ lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = childBottom - childHeight - lp.bottomMargin;
if (childBaseline != -1) {
int descent = child.getMeasuredHeight() - childBaseline;
childTop -= (maxDescent[INDEX_BOTTOM] - descent);
}
break;
default:
childTop = paddingTop;
break;
}
//累加childLeft
if (hasDividerBeforeChildAt(childIndex)) {
childLeft += mDividerWidth;
}
childLeft += lp.leftMargin;
//調用子View的layout方法
setChildFrame(child, childLeft + getLocationOffset(child), childTop,
childWidth, childHeight);
//累加childLeft
childLeft += childWidth + lp.rightMargin +
getNextLocationOffset(child);
i += getChildrenSkipCount(child, childIndex);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
從代碼中我們可以看出,一般獲取到onMeasure設置好的寬高,然後對子View進行遍歷,根據控件自身需求對子VIew的擺放位置進行設置並調用子View的layout方法進行擺放。
這就是整個layout過程的總結,比measure要簡單一些,而且View中我們可以不用關心layout過程
layout過程總結
整個layout過程比較簡單,從頂層View向下遞歸調用子View的layout方法的過程,其中ViewGroup通過measure過程中獲取到的子View寬高來確定子View的left、top、right、bottom等值,並通過這些值來將各個View放到合適的位置
- View的layout方法可以被複寫,而ViewGroup的layout方法被final關鍵字修飾,不能再被複寫
- 使用View的getWidth()和getHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onLayout流程之後被調用才能返回有效值。
- measure操作完成後得到各View的measuredWidth和measuredHeight,layout操作完成後得到每個View的left、top、right、bottom(相對於父控件)
draw
接下來就到了View繪製流程的最後一步,我們可以在ViewRootImpl的performTraversals()方法看到:
final Canvas canvas;
canvas = mSurface.lockCanvas(dirty);
mView.draw(canvas);
ok,我們繼續來看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 traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
/*
* Here we do the full fledged routine...
* (this is an uncommon case where speed matters less,
* this is why we repeat some of the tests that have been
* done above)
*/
boolean drawTop = false;
boolean drawBottom = false;
boolean drawLeft = false;
boolean drawRight = false;
float topFadeStrength = 0.0f;
float bottomFadeStrength = 0.0f;
float leftFadeStrength = 0.0f;
float rightFadeStrength = 0.0f;
// Step 2, save the canvas' layers
int paddingLeft = mPaddingLeft;
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
final ScrollabilityCache scrollabilityCache = mScrollCache;
final float fadeHeight = scrollabilityCache.fadingEdgeLength;
int length = (int) fadeHeight;
// clip the fade length if top and bottom fades overlap
// overlapping fades produce odd-looking artifacts
if (verticalEdges && (top + length > bottom - length)) {
length = (bottom - top) / 2;
}
// also clip horizontal fades if necessary
if (horizontalEdges && (left + length > right - length)) {
length = (right - left) / 2;
}
if (verticalEdges) {
topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
drawTop = topFadeStrength * fadeHeight > 1.0f;
bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
}
if (horizontalEdges) {
leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
drawLeft = leftFadeStrength * fadeHeight > 1.0f;
rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
drawRight = rightFadeStrength * fadeHeight > 1.0f;
}
saveCount = canvas.getSaveCount();
int solidColor = getSolidColor();
if (solidColor == 0) {
final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if (drawTop) {
canvas.saveLayer(left, top, right, top + length, null, flags);
}
if (drawBottom) {
canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
}
if (drawLeft) {
canvas.saveLayer(left, top, left + length, bottom, null, flags);
}
if (drawRight) {
canvas.saveLayer(right - length, top, right, bottom, null, flags);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// 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;
if (drawTop) {
matrix.setScale(1, fadeHeight * topFadeStrength);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, right, top + length, p);
}
if (drawBottom) {
matrix.setScale(1, fadeHeight * bottomFadeStrength);
matrix.postRotate(180);
matrix.postTranslate(left, bottom);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, bottom - length, right, bottom, p);
}
if (drawLeft) {
matrix.setScale(1, fadeHeight * leftFadeStrength);
matrix.postRotate(-90);
matrix.postTranslate(left, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(left, top, left + length, bottom, p);
}
if (drawRight) {
matrix.setScale(1, fadeHeight * rightFadeStrength);
matrix.postRotate(90);
matrix.postTranslate(right, top);
fade.setLocalMatrix(matrix);
p.setShader(fade);
canvas.drawRect(right - length, top, right, bottom, p);
}
canvas.restoreToCount(saveCount);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
從註釋中可以看出繪製過程分爲6個步驟:
- 對View背景進行繪製,這一步調用其私有方法drawBackground來實現,其內部牽扯到Canvas的使用,後期會單開文章講解
保存畫布層次(可以略過)- 對View內容進行繪製,調用onDraw來實現,默認onDraw是一個空方法,需要我們自身來實現
- 使用dispatchDraw對所有子View進行draw操作(View類中是空方法,因爲沒有字View)
繪製漸變效果並回復圖層(可以略過)- 繪製前景色、滾動條,調用onDrawForeground實現
這裏我們需要關注ViewGroup中的dispatchDraw方法:
/**
* {@inheritDoc}
*/
@Override
protected void dispatchDraw(Canvas canvas) {
//核心代碼,遍歷挨個調用子View的draw方法
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) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
while (transientIndex >= 0) {
// there may be additional transient views after the normal views
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
break;
}
}
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
可以看到最終調用還是回到了View的draw方法,而後開始執行上述流程。
繪製流程總結
繪製就是將View顯示到屏幕上的一個過程,過程中有一些細節是需要我們多多注意的:
- View默認只會繪製背景,具體內容的繪製需要在子類中實現,也就是需要在onDraw方法中來進行繪製
- ViewGroup需要複寫dispatchDraw方法來繪製所有子View
- Android中繪製流程離不開畫布Canvas的支持(後面也會介紹paint)
- 默認情況下子View的ViewGroup.drawChild繪製順序和子View被添加的順序一致,但是你也可以複寫ViewGroup.getChildDrawingOrder()方法提供不同順序。
總結
到這裏我們Android的View繪製流程就算結束了,洋洋灑灑很多東西,但是核心的東西其實並不多,只要深入理解了遞歸,就能明白整個的流程是怎麼樣的了。
在接下來我會嘗試去理解View和ViewGroup中使用較多的invalidate和postInvalidate以及requestLayout,來解析這三個方法的使用方法和原理以及它們的作用,在接下來也會嘗試學習自定義View的過程和Canvas等的使用,敬請期待~
我的個人博客,歡迎前來交流~
enjoy~