android6.0 Activity(五) Activity的測量(Measure)、佈局(Layout)和繪製(Draw)
Android應用程式視窗的繪圖表面在建立完成之後,我們就可以從上到下地繪製它裡面的各個檢視了,即各個UI元素了。不過在繪製這些UI元素之前,我們還需要從上到下地測量它們實際所需要的大小,以及對它們的位置進行合適的安排,即對它們進行合適的佈局。在本文中,我們就將詳細地分析Android應用程式視窗的測量、佈局以及繪製過程。
一、簡介
Android應用程式視窗請求SurfaceFlinger服務建立了一個繪圖表面之後,就可以接著請求為該繪圖表面建立圖形緩衝區,而當Android應用程式視窗往這些圖形緩衝區填充好UI資料之後,就可以請求SurfaceFlinger服務將它們渲染到硬體幀緩衝區中去,這樣我們就可以看到應用程式視窗的UI了。
Android應用程式視窗一般不會直接去操作分配給它的圖形緩衝區,而是通過一些圖形庫API來操作。例如在開機畫面使用C++來開發的開機動畫應用程式bootanimation,它是通過OpenGL提供的API來繪製UI的。對於使用Java來開發的Android應用程式來說,它們一般是使用Skia圖形庫提供的API來繪製UI的。在Skia相簿中,所有的UI都是繪製在畫布(Canvas)上的,因此,Android應用程式視窗需要將它的圖形緩衝區封裝在一塊畫布裡面,然後才可以使用Skia庫提供的API來繪製UI。
我們知道,一個Android應用程式窗口裡麵包含了很多UI元素,這些UI元素是以樹形結構來組織的,即它們存在著父子關係,其中,子UI元素位於父UI元素裡面,因此,在繪製一個Android應用程式視窗的UI之前,我們首先要確定它裡面的各個子UI元素在父UI元素裡面的大小以及位置。確定各個子UI元素在父UI元素裡面的大小以及位置的過程又稱為測量過程和佈局過程。因此,Android應用程式視窗的UI渲染過程可以分為測量、佈局和繪製三個階段
Android應用程式視窗的頂層檢視是一個型別為DecorView的UI元素,這個頂層檢視最終是由ViewRootImpl類的成員函式performTraversals來啟動測量、佈局和繪製操作的。
二、performTraversals函式
之前的部落格中,我們分析過如果從ActivityThread的handleResumeActivity函式,最後到ViewRootImpl的performTraversals函式進行繪製。
private void performTraversals() { // cache mView since it is used so much below... //mView就是DecorView根佈局 final View host = mView; if (host == null || !mAdded) return; //是否正在遍歷 mIsInTraversal = true; //是否馬上繪製View mWillDrawSoon = true; ............. //頂層檢視DecorView所需要視窗的寬度和高度 int desiredWindowWidth; int desiredWindowHeight; ..................... //在構造方法中mFirst已經設定為true,表示是否是第一次繪製DecorView if (mFirst) { mFullRedrawNeeded = true; mLayoutRequested = true; //如果視窗的型別是有狀態列的,那麼頂層檢視DecorView所需要視窗的寬度和高度就是除了狀態列 if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) { // NOTE -- system code, won't try to do compat mode. Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else {//否則頂層檢視DecorView所需要視窗的寬度和高度就是整個螢幕的寬高 DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics(); desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; } } ............ //獲得view寬高的測量規格,mWidth和mHeight表示視窗的寬高,lp.widthhe和lp.height表示DecorView根佈局寬和高 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // Ask host how big it wants to be //執行測量操作 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ........................ //執行佈局操作 performLayout(lp, desiredWindowWidth, desiredWindowHeight); ....................... //執行繪製操作 performDraw(); }
該方法主要流程就體現了View繪製渲染的三個主要步驟,分別是測量,佈局,繪製三個階段。
這裡先給出Android系統View的繪製流程:依次執行View類裡面的如下三個方法:
1.measure(int ,int) :測量View的大小
2.layout(int ,int ,int ,int) :設定子View的位置
3.draw(Canvas) :繪製View內容到Canvas畫布上
三、測量
我們先看下在performTraversals函式中呼叫performMeasure之前會先呼叫getRootMeasureSpec函式,通過getRootMeasureSpec方法獲得頂層檢視DecorView的測量規格
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed! mWidth="
+ mWidth + " measuredWidth=" + host.getMeasuredWidth()
+ " mHeight=" + mHeight
+ " measuredHeight=" + host.getMeasuredHeight()
+ " coveredInsetsChanged=" + contentInsetsChanged);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
我們來看下getRootMeasureSpec函式
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
//匹配父容器時,測量模式為MeasureSpec.EXACTLY,測量大小直接為螢幕的大小,也就是充滿真個螢幕
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
//包裹內容時,測量模式為MeasureSpec.AT_MOST,測量大小直接為螢幕大小,也就是充滿真個螢幕
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
//其他情況時,測量模式為MeasureSpec.EXACTLY,測量大小為DecorView頂層檢視佈局設定的大小。
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
分析:該方法主要作用是在整個視窗的基礎上計算出root view(頂層檢視DecorView)的測量規格,該方法的兩個引數分別表示:
1.windowSize:當前手機視窗的有效寬和高,一般都是除了通知欄的螢幕寬和高
2.rootDimension 根佈局DecorView請求的寬和高,由前面的部落格我們知道是MATCH_PARENT
我們的DecorView根佈局寬和高都是MATCH_PARENT,因此DecorView根佈局的測量模式就是MeasureSpec.EXACTLY,測量大小一般都是整個螢幕大小,所以一般我們的Activity視窗都是全屏的。因此上面程式碼走第一個分支,通過呼叫MeasureSpec.makeMeasureSpec方法將DecorView的測量模式和測量大小封裝成DecorView的測量規格。
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
makeMeasureSpec只是剛mode封裝在高位中。我們再來看下performMeasure函式,就是呼叫了DecorView的measure函式,而DecorView中沒有measure函式,一直到View基類才有measure函式
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
我們先來看下View的measure函式,注意這個方法是final的,子類不能繼承。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);
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);
}
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||//和上次測量不一樣
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);//呼叫子類的onMeasure函式
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
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
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 = widthMeasureSpec;//儲存上一次測量值
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
引數widthMeasureSpec和heightMeasureSpec用來描述當前正在處理的檢視可以獲得的最大寬度和高度。對於應用程式視窗的頂層檢視來說,我們也可以認為這兩個引數是用來描述應用程式視窗的寬度和高度。
View類的成員變數mPrivateFlags的型別為int,如果它的某一個位的值不等於0,那麼就隱含著當前檢視有一個相應的操作在等待執行中。View類的另外兩個成員變數mOldWidthMeasureSpec和mOldHeightMeasureSpec用來儲存當前檢視上一次可以獲得的最大寬度和高度。
當ViewRoot類的成員變數mPrivateFlags的FORCE_LAYOUT位不等於0時,就表示當前檢視正在請求執行一次佈局操作,這時候函式就需要重新測量當前檢視的寬度和高度。此外,當引數widthMeasureSpec和heightMeasureSpec的值不等於ViewRoot類的成員變數mldWidthMeasureSpec和mOldHeightMeasureSpec的值時,就表示當前檢視上一次可以獲得的最大寬度和高度已經失效了,這時候函式也需要重新測量當前檢視的寬度和高度。
當View類的成員函式measure決定要重新測量當前檢視的寬度和高度之後,它就會首先將成員變數mPrivateFlags的MEASURED_DIMENSION_SET位設定為0,接著再呼叫另外一個成員函式onMeasure來真正執行測量寬度和高度的操作。View類的成員函式onMeasure執行完成之後,需要再呼叫另外一個成員函式setMeasuredDimension來將測量好的寬度和高度設定到View類的成員變數mMeasuredWidth和mMeasuredHeight中,並且將成員變數mPrivateFlags的EASURED_DIMENSION_SET位設定為1。這個操作是強制的,因為當前檢視最終就是通過View類的成員變數mMeasuredWidth和mMeasuredHeight來獲得它的寬度和高度的。為了保證這個操作是強制的,View類的成員函式measure再接下來就會檢查成員變數mPrivateFlags的EASURED_DIMENSION_SET位是否被設定為1了。如果不是的話,那麼就會丟擲一個型別為IllegalStateException的異常來。
View類的成員函式measure最後就會把引數widthMeasureSpec和heightMeasureSpec的值儲存在成員變數mldWidthMeasureSpec和mOldHeightMeasureSpec中,以便可以記錄當前檢視上一次可以獲得的最大寬度和高度。
View類的成員函式onMeasure一般是由其子類來重寫的。例如,對於用來應用程式視窗的頂層檢視的DecorView類來說,它是通過父類FrameLayout來重寫祖父類View的成員函式onMeasure的。因此,接下來我們就分析FrameLayout類的成員函式onMeasure的實現。
onMeasure我們先從子類DecorView的onMeasure入手,這個函式主要是調整了兩個入參高度和寬度,然後呼叫其父類的onMeasure函式。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
final boolean isPortrait = metrics.widthPixels < metrics.heightPixels;
final int widthMode = getMode(widthMeasureSpec);
final int heightMode = getMode(heightMeasureSpec);
boolean fixedWidth = false;
if (widthMode == AT_MOST) {
final TypedValue tvw = isPortrait ? mFixedWidthMinor : mFixedWidthMajor;
if (tvw != null && tvw.type != TypedValue.TYPE_NULL) {
final int w;
if (tvw.type == TypedValue.TYPE_DIMENSION) {
w = (int) tvw.getDimension(metrics);
} else if (tvw.type == TypedValue.TYPE_FRACTION) {
w = (int) tvw.getFraction(metrics.widthPixels, metrics.widthPixels);
} else {
w = 0;
}
if (w > 0) {
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
widthMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.min(w, widthSize), EXACTLY);
fixedWidth = true;
}
}
}
if (heightMode == AT_MOST) {
final TypedValue tvh = isPortrait ? mFixedHeightMajor : mFixedHeightMinor;
if (tvh != null && tvh.type != TypedValue.TYPE_NULL) {
final int h;
if (tvh.type == TypedValue.TYPE_DIMENSION) {
h = (int) tvh.getDimension(metrics);
} else if (tvh.type == TypedValue.TYPE_FRACTION) {
h = (int) tvh.getFraction(metrics.heightPixels, metrics.heightPixels);
} else {
h = 0;
}
if (h > 0) {
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.min(h, heightSize), EXACTLY);
}
}
}
getOutsets(mOutsets);
if (mOutsets.top > 0 || mOutsets.bottom > 0) {
int mode = MeasureSpec.getMode(heightMeasureSpec);
if (mode != MeasureSpec.UNSPECIFIED) {
int height = MeasureSpec.getSize(heightMeasureSpec);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(
height + mOutsets.top + mOutsets.bottom, mode);
}
}
if (mOutsets.left > 0 || mOutsets.right > 0) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
if (mode != MeasureSpec.UNSPECIFIED) {
int width = MeasureSpec.getSize(widthMeasureSpec);
widthMeasureSpec = MeasureSpec.makeMeasureSpec(
width + mOutsets.left + mOutsets.right, mode);
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);//呼叫父類的onMeasure函式
int width = getMeasuredWidth();
boolean measure = false;
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY);
if (!fixedWidth && widthMode == AT_MOST) {
final TypedValue tv = isPortrait ? mMinWidthMinor : mMinWidthMajor;
if (tv.type != TypedValue.TYPE_NULL) {
final int min;
if (tv.type == TypedValue.TYPE_DIMENSION) {
min = (int)tv.getDimension(metrics);
} else if (tv.type == TypedValue.TYPE_FRACTION) {
min = (int)tv.getFraction(metrics.widthPixels, metrics.widthPixels);
} else {
min = 0;
}
if (width < min) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(min, EXACTLY);
measure = true;
}
}
}
// TODO: Support height?
if (measure) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
再來看看FrameLayout的onMeasure函式,這個函式主要是遍歷所有的子View進行測量,然後設定高度、寬度。 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||//不是精確模式
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);//遍歷所有子View,然後進行測量
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||//把MATCH_PARENT整理出來,放在mMatchParentChildren變數
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));//設定高度、寬度
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {//重新調整mMatchParentChildren的高度和寬度
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
FrameLayout類是從ViewGroup類繼承下來的,後者用來描述一個檢視容器,它有一個型別為View的陣列mChildren,裡面儲存的就是它的各個子檢視。ViewGroup類所供了兩個成員函式getChildCount和getChildAt,它們分別用來獲得一個檢視容器所包含的子檢視的個數,以及獲得每一個子檢視。
FrameLayout類的成員函式onMeasure首先是呼叫另一個成員函式measureChildWithMargins來測量每一個子檢視的寬度和高度,並且找到這些子檢視的最大寬度和高度值,儲存在變數maxWidth和maxHeight 中。
FrameLayout類的成員函式onMeasure接著再將前面得到的寬度maxWidth和高度maxHeight分別加上當前檢視所設定的Padding值,其中,(mPaddingLeft,mPaddingRight,mPaddingTop,mPaddingBottom )表示當前檢視的內容區域的左右上下四條邊分別到當前檢視的左右上下四條邊的距離,它們是父類View的四個成員變數,(mForegroundPaddingLeft,mForegroundPaddingRight,mForegroundPaddingTop,mForegroundPaddingBottom)表示當前檢視的各個子檢視所圍成的區域的左右上下四條邊到當前視視的前景區域的左右上下四條邊的距離。從這裡就可以看出,當前檢視的內容區域的大小就等於前景區域的大小,而前景區域的大小大於等於各個子檢視的所圍成的區域,這是因為前景區域本來就是用來覆蓋各個子檢視所圍成的區域的。
加上各個Padding值之後,得到的寬度maxWidth和高度maxHeight還不是最終的寬度和高度,還需要考慮以下兩個因素:
1. 當前檢視是否設定有最小寬度和高度。如果設定有的話,並且它們比前面計算得到的寬度maxWidth和高度maxHeight還要大,那麼就將它們作為當前檢視的寬度和高度值。
2. 當前檢視是否設定有前景圖。如果設定有的話,並且它們比前面計算得到的寬度maxWidth和高度maxHeight還要大,那麼就將它們作為當前檢視的寬度和高度值。
經過上述兩步檢查之後,FrameLayout類的成員函式onMeasure就得到了當前檢視的寬度maxWidth和高度maxHeight。由於得到的寬度和高度又必須要限制在引數widthMeasureSpec和heightMeasureSpec所描述的寬度和高度規範之內,因此,FrameLayout類的成員函式onMeasure就會呼叫從View類繼承下來的成員函式resolveSize來獲得正確的大小。得到了當前檢視的正確大小之後,FrameLayout類的成員函式onMeasure就可以呼叫從父類View繼承下來的成員函式setMeasuredDimension來將它們為當前檢視的大小了。
我們看measureChildWithMargins函式最後呼叫了View的measure函式進行測量,在FrameLayout的onMeasure遍歷所有子View呼叫這個函式完成子View的測量
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);
}
我們再來看下resolveSizeAndState函式,這個函式先是把measureSpec入參的mode和size解析出來,mode封裝在高位中,然後根據mode來決定最後返回的size
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
再來看View的setMeasureDimension最後是呼叫setMeasuredDimensionRaw函式來把寬度和高度儲存在mMeasuredWidth 、mMeasuredHeight成員變數中。而當這兩個成員變數設定完成,也就是當前的View測量結束了。
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);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
measure總結
1.View的measure方法是final型別的,子類不可以重寫,子類可以通過重寫onMeasure方法來測量自己的大小,當然也可以不重寫onMeasure方法使用系統預設測量大小。
2.View測量結束的標誌是呼叫了View類中的setMeasuredDimension成員方法,言外之意是,如果你需要在自定義的View中重寫onMeasure方法,在你測量結束之前你必須呼叫setMeasuredDimension方法測量才有效。
3.在Activity生命週期onCreate和onResume方法中呼叫View.getWidth()和View.getMeasuredHeight()返回值為0的,是因為當前View的測量還沒有開始,這裡關係到Activity啟動過程,文章開頭說了當ActivityThread類中的performResumeActivity方法執行之後才將DecorView新增到PhoneWindow視窗上,開始測量。在Activity生命週期onCreate在中performResumeActivity還為執行,因此呼叫View.getMeasuredHeight()返回值為0。
4.子檢視View的大小是由父容器View和子檢視View佈局共同決定的。
四、佈局 layout
下面我們先看ViewRootImpl的performLayout函式
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
..................
//標記當前開始佈局
mInLayout = true;
//mView就是DecorView
final View host = mView;
..................
//DecorView請求佈局
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
//標記佈局結束
mInLayout = false;
..................
}
呼叫DecorView的layout函式四個位置左=0,頂=0,右=螢幕寬,底=螢幕寬,說明DecorView佈局的位置是從螢幕最左最頂端開始佈局,到螢幕最低最右結束。因此DecorView根佈局是充滿整個螢幕的。
該方法主要呼叫了View類的layout方法,跟蹤程式碼進入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;
}
//儲存上一次View的四個位置
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//設定當前檢視View的左,頂,右,底的位置,並且判斷佈局是否有改變
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//如果佈局有改變,條件成立,則檢視View重新佈局
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//呼叫onLayout,將具體佈局邏輯留給子類實現
onLayout(changed, l, t, r, b);
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);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
分析:
1.先是儲存本次佈局的四個位置,用於佈局變化的監聽事件,如果使用者設定了佈局變化的監聽事件,則程式碼就會執行設定監聽事件。
2.設定當前View的佈局位置,也就是當呼叫了setFrame(l, t, r, b)方法之後,當前View佈局基本完成,既然這樣為什麼還要onLayout方法呢?稍後解答,這裡來分析一下setFrame是怎麼設定當前View的佈局位置的。
進入setFrame方法
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
//當上,下,左,右四個位置有一個和上次的值不一樣都會重新佈局
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;
//判斷本次View的寬高和上次View的寬高是否相等
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
//清楚上次佈局的位置
invalidate(sizeChanged);
//儲存當前View的最新位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
//如果當前View的尺寸有所變化
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
...............
return changed;
}
View類的成員變數mLeft、mRight、mTop和mBottom分別用來描述當前檢視的左右上下四條邊與其父檢視的左右上下四條邊的距離,如果它們的值與引數left、right、top和bottom的值不相等,那麼就說明當前檢視的大小或者位置發生變化了。這時候View類的成員函式setFrame就需要將引數left、right、top和bottom的值分別記錄在成員變數mLeft、mRight、mTop和mBottom中。在記錄之前,還會執行兩個操作:
1. 將成員變數mPrivateFlags的DRAWN位記錄在變數drawn中,並且呼叫另外一個成員函式invalidate來檢查當前檢視上次請求的UI繪製操作是否已經執行。如果已經執行了的話,那麼就會再請求執行一個UI繪製操作,以便可以在修改當前檢視的大小和位置之前,將當前檢視在當前位置按照當前大小顯示一次。在接下來的Step 3中,我們再詳細分析View類的成員函式invalidate的實現。
2. 計算當前檢視上一次的寬度oldWidth和oldHeight,以便接下來可以檢查當前檢視的大小是否發生了變化。
當前檢視距離父檢視的邊距一旦設定好之後,它就是一個具有邊界的檢視了,因此,View類的成員函式setFrame接著還會將成員變數mPrivateFlags的HAS_BOUNDS設定為1。
View類的成員函式setFrame再接下來又會計算當前檢視新的寬度newWidth和高度newHeight,如果它們與上一次的寬度oldWidth和oldHeight的值不相等,那麼就說明當前檢視的大小發生了變化,這時候就會呼叫另外一個成員函式onSizeChanged來讓子類有機會處理這個變化事件。
View類的成員函式setFrame接下來繼續判斷當前檢視是否是可見的,即成員變數mViewFlags的VISIBILITY_MASK位的值是否等於VISIBLE。如果是可見的話,那麼就需要將成員變數mPrivateFlags的DRAWN位設定為1,以便接下來可以呼叫另外一個成員函式invalidate來成功地執行一次UI繪製操作,目的是為了將當前檢視馬上顯示出來。
View類的成員變數mPrivateFlags的DRAWN位描述的是當前檢視上一次請求的UI繪製操作是否已經執行過了。如果它的值等於1,就表示已經執行過了,否則的話,就表示還沒在等待執行。前面第一次呼叫View類的成員函式invalidate來檢查當前檢視上次請求的UI繪製操作是否已經執行時,如果發現已經執行了,那麼就會重新請求執行一次新的UI繪製操作,這時候會導致當前檢視的成員變數mPrivateFlags的DRAWN位重置為0。注意,新請求執行的UI繪製只是為了在修改當前檢視的大小以及大小之前,先將它在上一次設定的大小以及位置中繪製出來,這樣就可以使得當前檢視的大小以及位置出現平滑的變換。換句話說,新請求執行的UI繪製只是為了獲得一箇中間效果,它不應該影響當前檢視的繪製狀態,即不可以修改當前檢視的成員變數mPrivateFlags的DRAWN位。因此,我們就需要在前面第一次呼叫View類的成員函式invalidate前,先將當前檢視的成員變數mPrivateFlags的DRAWN位儲存下來,即儲存在變數drawn中,然後等到呼叫之後,再將變數drawn的值恢復到當前檢視的成員變數mPrivateFlags的DRAWN位中去。
分析:
1.如果當前View檢視的最新位置和上一次不一樣時,則View會重新佈局。
2.儲存當前View的最新位置,到此當前View的佈局基本結束。從這裡我們可以看到,四個全域性變數 mLeft,mTop,mRight,mBottom在此刻賦值,聯想我們平時使用的View.getWidth()方法獲得View的寬高,你可以發現,其實View.getWidth()方法的實現如下:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
那我們再來看看onLayout函式,為空函式。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
我們直接看DecorView的onLayout函式,這裡先是呼叫了FrameLayout的onLayout函式,然後是調整個別引數。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
getOutsets(mOutsets);
if (mOutsets.left > 0) {
offsetLeftAndRight(-mOutsets.left);
}
if (mOutsets.top > 0) {
offsetTopAndBottom(-mOutsets.top);
}
}
而在FrameLayout的onLayout函式,呼叫了layoutChildren函式,是遍歷各個子View。然後呼叫子View的layout函式。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom,
boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
mForegroundBoundsChanged = true;
//遍歷當前FrameLayout下的子View
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//當子檢視View可見度設定為GONE時,不進行當前子檢視View的佈局,這就是為什麼當你佈局中使用Visibility=GONE時,該view是不佔據空間的。
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//獲得子檢視View的寬高
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
//一下程式碼獲得子檢視View的四個位置,用於子檢視View佈局。
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
//子檢視佈局
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
給子檢視View進行佈局的。也就是說FrameLayout佈局其實在View類中的layout方法中已經實現,佈局的邏輯實現是在父檢視中實現的,不像View檢視的measure測量,通過子類實現onMeasure方法來實現測量邏輯。
1.遍歷獲得FrameLayout的子檢視View的四個位置,然後呼叫child.layout對子檢視View進行佈局操作。
2.對每個子檢視View的可見度進行了判斷,如果當前子檢視View可見度型別為GONE,則當前子檢視View不進行佈局,這也就是為什麼可見度GONE型別時是不佔據螢幕空間的,而其他兩種VISIBLE和INVISIBLE是佔據螢幕空間的。
layout佈局總結
1.檢視View的佈局邏輯是由父View,也就是ViewGroup容器佈局來實現的。因此,我們如果自定義View一般都無需重寫onMeasure方法,但是如果自定義一個ViewGroup容器的話,就必須實現onLayout方法,因為該方法在ViewGroup是抽象的,所有ViewGroup的所有子類必須實現onLayout方法。
2.當我們的檢視View在佈局中使用 android:visibility=”gone” 屬性時,是不佔據螢幕空間的,因為在佈局時ViewGroup會遍歷每個子檢視View,判斷當前子檢視View是否設定了 Visibility==GONE,如果設定了,當前子檢視View就會新增到父容器上,因此也就不佔據螢幕空間。
3.必須在View佈局完之後呼叫getHeight()和getWidth()方法獲取到的View的寬高才大於0。
五、繪製 draw
performDraw會呼叫draw函式,在draw會呼叫drawSoftware函式完成繪製
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
// Draw with software renderer.
final Canvas canvas;
try {
//從surface物件中獲得canvas變數
canvas = mSurface.lockCanvas(dirty);
// If this bitmap's format includes an alpha channel, we
// need to clear it before drawing so that the child will
// properly re-composite its drawing on a transparent
// background. This automatically respects the clip/dirty region
// or
// If we are applying an offset, we need to clear the area
// where the offset doesn't appear to avoid having garbage
// left in the blank areas.
if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
......................
try {
//調整畫布的位置
canvas.translate(-xoff, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
attachInfo.mSetIgnoreDirtyState = false;
//呼叫View類中的成員方法draw開始繪製View檢視
mView.draw(canvas);
}
.....................
return true;
}
我們先從mSurface中獲取一塊畫布,後面呼叫了mView的draw函式完成繪製。我們先看下DecorView的draw函式,先呼叫了父類的draw函式也就是View的draw函式,然後再呼叫選單背景繪製。
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mMenuBackground != null) {
mMenuBackground.draw(canvas);
}
}
我們來看下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);
}
流程大致如下:
1.繪製當前檢視的背景。
2.儲存當前畫布的堆疊狀態,並且在在當前畫布上建立額外的圖層,以便接下來可以用來繪製當前檢視在滑動時的邊框漸變效果。
3.繪製當前檢視的內容。
4.繪製當前檢視的子檢視的內容。
5.繪製當前檢視在滑動時的邊框漸變效果。
6.繪製當前檢視的滾動條。
5.1 繪製背景
先呼叫setBackgroundBounds函式,設定background的位置等,最後呼叫background.draw繪製
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
// Attempt to use a display list if requested.
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mHardwareRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
final RenderNode renderNode = mBackgroundRenderNode;
if (renderNode != null && renderNode.isValid()) {
setBackgroundRenderNodeProperties(renderNode);
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
return;
}
}
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
/**
* Sets the correct background bounds and rebuilds the outline, if needed.
* <p/>
* This is called by LayoutLib.
*/
void setBackgroundBounds() {
if (mBackgroundSizeChanged && mBackground != null) {
mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
rebuildOutline();
}
}
5.2 儲存畫布canvas的邊框引數
獲取當前檢視View水平或者垂直方向是否需要繪製邊框漸變效果,如果不需要繪製邊框的漸變效果,就無需執行上面的2,5了。那麼就直接執行上面的3,4,6步驟。這裡描述的就是我們的ListView滑動到最底端時,底部會有一個淡藍色的半圓形的邊框漸變背景效果。
// 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;
}
假如我們需要繪製檢視View的邊框漸變效果,那麼我們繼續分析步驟2,3,4,5,6。
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);
}
這段程式碼用來檢查是否需要儲存引數canvas所描述的一塊畫布的堆疊狀態,並且建立額外的圖層來繪製當前檢視在滑動時的邊框漸變效果。檢視的邊框是繪製在內容區域的邊界位置上的,而檢視的內容區域是需要排除成員變數mPaddingLeft、mPaddingRight、mPaddingTop和mPaddingBottom所描述的檢視內邊距的。此外,檢視的邊框有四個,分別位於檢視的左、右、上以及下內邊界上。因此,這段程式碼首先需要計算出當前檢視的左、右、上以及下內邊距的大小,以便得到邊框所要繪製的區域。
5.3 繪製檢視View的內容
呼叫了View的成員方法onDraw來繪製檢視View的內容,我們來看看onDraw成員方法的實現:
protected void onDraw(Canvas canvas) {
}
這裡主要在子類中實現我們看下DecorView的onDraw函式:
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
mBackgroundFallback.draw(mContentRoot, c, mContentParent);
}
5.4 繪製當前檢視View的子檢視
呼叫View的成員方法dispatchDraw(canvas);來繪製它的子檢視,在ViewGroup中實現了該方法
@Override
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
//判斷當前ViewGroup容器是否設定的佈局動畫
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;
final boolean buildCache = !isHardwareAccelerated();
//遍歷給每個子檢視View設定動畫效果
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child, params, i, childrenCount);
bindLayoutAnimation(child);
if (cache) {
child.setDrawingCacheEnabled(true);
if (buildCache) {
child.buildDrawingCache(true);
}
}
}
}
//獲得佈局動畫的控制器
final LayoutAnimationController controller = mLayoutAnimationController;
if (controller.willOverlap()) {
mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
}
//開始佈局動畫
cont