編輯:關於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來啟動測量、布局和繪制操作的。
之前的博客中,我們分析過如果從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;
}
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布局共同決定的。
下面我們先看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 listenersCopy =
(ArrayList)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是占據屏幕空間的。
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。
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.繪制當前視圖的滾動條。
先調用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.
*
* This is called by LayoutLib. */ void setBackgroundBounds() { if (mBackgroundSizeChanged && mBackground != null) { mBackground.setBounds(0, 0, mRight - mLeft, mBottom - mTop); mBackgroundSizeChanged = false; rebuildOutline(); } }
獲取當前視圖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所描述的視圖內邊距的。此外,視圖的邊框有四個,分別位於視圖的左、右、上以及下內邊界上。因此,這段代碼首先需要計算出當前視圖的左、右、上以及下內邊距的大小,以便得到邊框所要繪制的區域。
調用了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);
}
調用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;
}
//開始布局動畫
controller.start();
mGroupFlags &= ~FLAG_RUN_ANIMATION;
mGroupFlags &= ~FLAG_ANIMATION_DONE;
if (cache) {
mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
}
//設置布局動畫的監聽事件
if (mAnimationListener != null) {
mAnimationListener.onAnimationStart(controller.getAnimation());
}
}
int clipSaveCount = 0;
//是否需要剪裁邊距
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
clipSaveCount = canvas.save();
//對畫布進行邊距剪裁
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
// We will draw our child's animation, let's reset the flag
mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
boolean more = false;
final long drawingTime = getDrawingTime();
if (usingRenderNodeProperties) canvas.insertReorderBarrier();
// Only use the preordered list if not HW accelerated, since the HW pipeline will do the
// draw reordering internally
final ArrayList preorderedList = usingRenderNodeProperties
? null : buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
//遍歷繪制當前視圖的子視圖View
for (int i = 0; i < childrenCount; i++) {
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);
}
}
if (preorderedList != null) preorderedList.clear();
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
final ArrayList disappearingChildren = mDisappearingChildren;
final int disappearingCount = disappearingChildren.size() - 1;
// Go backwards -- we may delete as animations finish
for (int i = disappearingCount; i >= 0; i--) {
final View child = disappearingChildren.get(i);
more |= drawChild(canvas, child, drawingTime);
}
}
if (usingRenderNodeProperties) canvas.insertInorderBarrier();
if (debugDraw()) {
onDebugDraw(canvas);
}
if (clipToPadding) {
canvas.restoreToCount(clipSaveCount);
}
// mGroupFlags might have been updated by drawChild()
flags = mGroupFlags;
if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
invalidate(true);
}
//更新布局動畫的監聽事件
if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
mLayoutAnimationController.isDone() && !more) {
// We want to erase the drawing cache and notify the listener after the
// next frame is drawn because one extra invalidate() is caused by
// drawChild() after the animation is over
mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
final Runnable end = new Runnable() {
public void run() {
notifyAnimationListener();
}
};
post(end);
}
}
我們再來看看drawChild函數,就是調用了View的draw函數
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
// 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);
}
在函數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);
}
}
1.View繪制的畫布參數canvas是由surface對象獲得,言外之意,View視圖繪制最終會繪制到Surface對象去。
2.父類View繪制主要是繪制背景,邊框漸變效果,進度條,View具體的內容繪制調用了onDraw方法,通過該方法把View內容的繪制邏輯留給子類去實現。因此,我們在自定義View的時候都一般都需要重寫父類的onDraw方法來實現View內容繪制。
3.不管任何情況,每一個View視圖都會繪制 scrollBars滾動條,且繪制滾動條的邏輯是在父類View中實現,子類無需自己實現滾動條的繪制。
4.ViewGroup繪制的過程會對每個子視圖View設置布局容器動畫效果,如果你在ViewGroup容器布局裡面設置了如下屬性的話:
android:animateLayoutChanges="true"
Android數據庫Realm實踐
Android開發中常用的數據庫有5個:1.OrmLiteOrmLite 不是 Android 平台專用的ORM框架,它是Java ORM。支持JDBC連接,Spring
android 解決asset下面文件太大報錯問題
Android 中對asset中存放文件 大小有一定限制,如果超過1M會報 Data exceeds UNCOMPRESS_DATA_MAX 這個錯誤那麼我們怎麼解決這個
仿360懸浮窗
仿360安全衛士懸浮窗雖然360安全衛士很流氓,但是我相信安裝的人不在少數,它有一個讓人很憂傷的功能,就是即時你關閉了360安全衛士,你手機的左邊距或者右邊距會有一個蟲蟲
java/android 使用swig編譯c/c++ 代碼類型轉換
今天被同時問到java/android 使用swig編譯c/c++ 代碼類型轉換。想起找個中文版swig看一下,雖然找到了,但也是基本是英文。中文版首頁的:http://