緊接着上一篇文章重溫View繪製原理(一),繼續看view繪製原理。android
view的繪製是從根視圖 ViewRoot 的 performTraversals() 方法開始,從上到下遍歷整個視圖樹,每一個 View 控制負責繪製本身,而 ViewGroup 還須要負責通知本身的子 View 進行繪製操做。視圖操做的過程能夠分爲三個步驟,分別是測量(Measure)、佈局(Layout)和繪製(Draw)。performTraversals 方法在viewRoot的實現類 ViewRootImpl 裏面:canvas
view繪製流程圖:bash
看看performTraversals方法源碼:佈局
private void performTraversals() {
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
// 測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
// 佈局
performLayout(lp, mWidth, mHeight);
...
// 繪製
performDraw();
...
}
複製代碼
依次調用performMeasure、performLayout、performDraw方法,分別完成頂級View的measure、layout、draw流程。performMeasure會調用measure方法,而measure又會調用onMeasure方法,在onMeasure方法中又會對子元素進行measure,這樣重複下去就完成了整個View樹的遍歷。優化
performLayout、performDraw傳遞過程也很是相似,不過performDraw是在draw方法中經過dispatchDraw方法實現的。ui
measure過程決定了View的寬高,而Layout方法則肯定了四個頂點的座標和實際的寬高(每每等於measure中計算的寬高),draw方法則決定了View的顯示。只有完成了draw方法才能正確顯示在屏幕上。this
MeasureSpec是measure的重要參數。 MeasureSpec 表示的是一個 32 位的整數值,它的高 2 位表示測量模式 SpecMode,低 30 位表示某種測量模式下的規格大小 SpecSize(PS:這裏用到了位運算進行狀態壓縮來節省內存)。MeasureSpec 是 View 類的一個靜態內部類,用來講明應該如何測量這個View,有三種模式:lua
UNSPECIFIED:不指定測量模式,父視圖沒有限制子視圖的大小,子視圖能夠是想要的任何尺寸,一般用於系統內部,應用開發中不多使用到。spa
EXACTLY:精確測量模式,當該視圖的 layout_width 或者 layout_height 指定爲具體數值或者 match_parent 時生效,表示父視圖已經決定了子視圖的精確大小,這種模式下 View 的測量值就是 SpecSize 的值。.net
AT_MOST:最大值模式,當前視圖的 layout_width 或者 layout_height 指定爲 wrap_content 時生效,此時子視圖的尺寸能夠是不超過父視圖運行的最大尺寸的任何尺寸。
下表是普通View的MeasureSpec的建立規則對應表:
childLayoutParams/parentSpecParams | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY childSIze | EXACTLY childSIze | EXACTLY childSIze |
match_parent | EXACTLY parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
View的繪製從測量開始,看看performMeasure()方法:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
複製代碼
具體操做是分發給 ViewGroup 的,由 ViewGroup 在它的 measureChild 方法中傳遞給子 View。ViewGroup 經過遍歷自身全部的子 View,並逐個調用子 View 的 measure 方法實現測量操做。
View的measure過程由measure方法來完成,measure方法是一個final方法,不能重寫,它會調用VIew的onMeasure方法。onMeasure方法中會調用getDefaultSize方法,而getDefault方法中又會調用getSuggestedWidth和getSuggestedHeight方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製代碼
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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;
}
複製代碼
getDefaultSize方法所返回的就是測量後的View的大小。
接着看getSuggestedWidth和getSuggestedHeight方法:
/**
* Returns the suggested minimum width that the view should use. This
* returns the maximum of the view's minimum width * and the background's minimum width
* ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
* <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
* ensure the returned width is within the requirements of the parent.
*
* @return The suggested minimum width of the view.
*/
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
複製代碼
它在沒有指定background的狀況下,返回的是minSize這一屬性對應的值,而在指定了背景的狀況下,返回的是背景drawable的getMinimumWidth / getMinimumHeight方法對應的值
這兩個方法在Drawable有原始寬度的狀況下返回原始寬度,不然返回0
從getDefaultSize方法能夠看出,View的寬高由specSize決定。
ViewGroup除了完成本身的measure過程,還會遍歷調用子元素的measure方法,而後子元素再次遞歸執行,ViewGroup是一個抽象類,所以沒有重寫View的onMeasure方法。但它提供了一個measureChildren的方法,以下:
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
能夠看到,ViewGroup執行measure時,會遍歷子元素,調用measureChild方法對子元素進行measure。
在measureChild方法中,取出子元素的LayoutParams,經過getChildMeasureSpec方法建立子元素MeasureSpec,而後傳遞給View的measure方法進行測量。
ViewGroup沒有定義測量具體過程,由於它是個抽象類。具體的測量過程的onMeasure方法須要子類來實現,因爲它的子類的特性可能會很大不一樣,因此無法作統一處理(如LinearLayout和RelativeLayout)。
Layout流程的做用是ViewGroup肯定子元素的位置。當ViewGroup被肯定後,在onLayout中會遍歷全部子元素並調用layout方法,在layout方法中會調用onLayout方法。layout方法肯定View的位置,而onLayout方法則肯定全部子元素的位置。
ViewRootImpl 的 performLayout 以下:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(mTag, "Laying out " + host + " to (" +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
}
複製代碼
先看View的layout方法:
public void layout(int l, int t, int r, int b) {
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;
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);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
final boolean wasLayoutValid = isLayoutValid();
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if (!wasLayoutValid && isFocused()) {
mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
if (canTakeFocus()) {
// We have a robust focus, so parents should no longer be wanting focus.
clearParentsWantFocus();
} else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {
// This is a weird case. Most-likely the user, rather than ViewRootImpl, called
// layout. In this case, there's no guarantee that parent layouts will be evaluated // and thus the safest action is to clear focus here. 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()) { // Give up and clear focus once we've reached the top-most parent which wants
// focus.
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);
}
}
複製代碼
首先,經過setFrame方法設定View四個頂點的位置(初始化mLeft,mRight,mTop,mBottom)。四個頂點一旦肯定,則在父容器中的位置也肯定了,接着便會調用onLayout方法,來讓父容器肯定子容器的位置。onLayout一樣和具體佈局有關,所以View和ViewGroup均沒有實現onLayout方法。
draw流程是將View繪製到屏幕上。先看看performDraw 方法:
private void performDraw() {
....
try {
boolean canUseAsync = draw(fullRedrawNeeded);
....
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
...
}
複製代碼
private boolean draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
...
mView.draw(canvas);
...
}
複製代碼
最終調用到每一個 View 的 draw 方法繪製每一個具體的 View,繪製基本上能夠分爲六個步驟:
public void draw(Canvas canvas) {
...
// Step 1, draw the background, if needed
if (!dirtyOpaque) {
drawBackground(canvas);
}
...
// Step 2, save the canvas' layers saveCount = canvas.getSaveCount(); ... // 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 canvas.drawRect(left, top, right, top + length, p); ... canvas.restoreToCount(saveCount); ... // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); } 複製代碼
View繪製過程的傳遞是經過dispatchDraw實現的。dispatchDraw會遍歷調用全部子元素的draw方法。這樣draw事件就一層層傳遞了下來。
它有個比較特殊的setWillNotDraw方法。若是一個View不須要繪製任何內容,在咱們設定這個標記爲true後,系統就會對其進行相應優化。通常View沒有啓用這個標記位。但ViewGroup是默認啓用的。
它對實際開發的意義在於:咱們的自定義控件繼承於ViewGroup而且不具有繪製功能時,能夠開啓這個標記位方便系統進行後續優化。
關於view繪製的原理在網上也特別多,時間久了也容易忘記,看一遍別人的,還不如順便把他們的記錄下來,方便本身之後溫習。
重要參考: