在Android的知識體系中,View扮演着很重要的的角色,簡單來理解,View就是Android在視覺上的呈現。在界面上Android提供了一套GUI庫,裏面有不少控件,但不少時候系統提供的控件都不能很好的知足咱們的需求,這時候就須要自定義View了,但僅僅瞭解基本控件的使用是沒法作出複雜的自定義控件的。爲全部了更好的自定義View,就須要掌握View的底層工做原理,好比View的測量、佈局以及繪製流程,掌握這幾個流程後,基本上就能夠作出一個比較完善的自定義View了。html
ViewRoot對應ViewRootImpl,它是鏈接WindowManager(實現類是WindowManagerImpl)和DecorView的紐帶,View繪製的三大流程均是經過ViewRoot來完成的。那麼一個activity是什麼時候開始繪製的尼?當建立activity成功而且onResume方法調用後,就會將DecorView添加進WindowManager中。代碼以下:java
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
...
// TODO Push resumeArgs into the activity for consideration
//回調activity的onResume方法
r = performResumeActivity(token, clearHide, reason);
if (r != null) {
...
if (r.window == null && !a.mFinished && willBeVisible) {
//拿到activity對應的PhoneWindow
r.window = r.activity.getWindow();
//拿到activity的根View->decorView
View decor = r.window.getDecorView();
//隱藏decorView
decor.setVisibility(View.INVISIBLE);
//拿到WindowManager->WindowManagerImpl
ViewManager wm = a.getWindowManager();
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
//將根佈局添加到WindowManager中
wm.addView(decor, l);
} else {
...
}
}
...
// The window is now visible if it has been added, we are not
// simply finishing, and we are not starting another activity.
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
...
WindowManager.LayoutParams l = r.window.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
!= forwardBit) {
...
if (r.activity.mVisibleFromClient) {
ViewManager wm = a.getWindowManager();
View decor = r.window.getDecorView();
//更新Window,從新測量、擺放、繪製界面
wm.updateViewLayout(decor, l);
}
}
...
}
...
} else {
//出錯則關閉當前activity
try {
ActivityManager.getService()
.finishActivity(token, Activity.RESULT_CANCELED, null,
Activity.DONT_FINISH_TASK_WITH_ACTIVITY);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
}
複製代碼
在調用wm.addView(decor, l);
中,就會去建立ViewRootImpl,而後在ViewRootImpl中進行繪製。代碼以下:android
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
//交給ViewRootImpl繼續執行
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
複製代碼
在ViewRootImpl中,在正式向WMS添加Window以前,系統會調用requestLayout();
來對UI進行繪製,經過查看requestLayout();
能夠發現系統最終調用的是performTraversals()
這個方法,在這個方法裏調用了View的measure、layout、draw方法。代碼以下:canvas
private void performTraversals() {
...
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
...
if (!mStopped || mReportNextDraw) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
updatedConfiguration) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// Implementation of weights from WindowManager.LayoutParams
// We just grow the dimensions as needed and re-measure if
// needs be
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (measureAgain) {
if (DEBUG_LAYOUT) Log.v(mTag,
"And hey let's measure once more: width=" + width
+ " height=" + height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layoutRequested = true;
}
}
} else {
...
}
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight);
}
...
if (!cancelDraw && !newSurface) {
if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
mPendingTransitions.get(i).startChangingAnimations();
}
mPendingTransitions.clear();
}
performDraw();
} else {
...
}
mIsInTraversal = false;
}
複製代碼
對於代碼中performMeasure
、performLayout
、performDraw
這三個方法有沒有一點熟悉?,沒錯,它們就對應着View的measure、layout、draw,到這裏就開始真正的繪製UI了。嗯,先來梳理一下從activity的onResume到開始繪製UI的流程,以下:緩存
在測量過程當中,MeasureSpec很是重要,它表明一個32位的int值,高2位表明表明SpecMode,低30位表明SpecSize,SpecMode是指測量模式,而SpecSize則是指在某種測量模式下的大小。 SpecMode有三類,每一類都表明不一樣的含義,以下:app
MeasureSpec經過將SpecMode與SpecSize打包成一個int值來避免過多的內存對象分配,爲了方便操做,提供了打包和解包的操做。ide
//解包
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//打包
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize,childWidthMode);
複製代碼
嗯,來舉個例子。當ScrollView嵌套ListView時,ListView只能顯示一個item,這時候的解決方案基本上都是將全部item展現出來。以下:佈局
public void onMeasure(){
//MeasureSpec打包操做
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
複製代碼
那爲何這麼寫就可以展開全部item尼?由於在ListView的onMeasure中,當heightMode爲MeasureSpec.AT_MOST時就會將全部的item高度相加。優化
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childWidth = 0;
int childHeight = 0;
int childState = 0;
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
//拿到第一個item的View
final View child = obtainView(0, mIsScrap);
// Lay out child directly against the parent measure spec so that
// we can obtain exected minimum width and height.
measureScrapChild(child, 0, widthMeasureSpec, heightSize);
//拿到第一個item的寬
childWidth = child.getMeasuredWidth();
//拿到第一個item的高
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
}
...
//當mode爲MeasureSpec.UNSPECIFIED時高度則爲第一個item的高度,而ScrollView、ListView等滑動組件在測量子View時,傳入的類型就是MeasureSpec.UNSPECIFIED
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
//當傳入類型爲heightMode時則計算所有item高度,全部須要重寫ListView的onMeasure而且傳入類型爲MeasureSpec.AT_MOST
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
//傳入測量出來的寬高
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
複製代碼
可是前面size時爲何是Integer.MAX_VALUE >> 2尼?按理說值Integer.MAX_VALUE就能夠了。這是由於MeasureSpec表明一個32位的int值,高兩位表明mode,若是直接與MeasureSpec.AT_MOST一塊兒打包,mode就可能會變成其餘類型。而Integer.MAX_VALUE >> 2後,高兩位就變爲00了,這樣在跟MeasureSpec.AT_MOST一塊兒打包,mode就不會變了,是MeasureSpec.AT_MOST。this
//示例:
int 32位:010111100011100將這個數向右位移2位,則變成000101111000111
而後將000101111000111與MeasureSpec.AT_MOST打包這樣在listView的onMeasure裏拿到的mode就是MeasureSpec.AT_MOST類型了。
複製代碼
在performTraversals()
這個方法中,首先調用了View的measure方法,在此方法裏就完成了對本身的測量,若是是一個ViewGroup的話,除了完成本身的測量,還會在onMeasure裏對子控件進行測量。
View的測量過程由measure方法實現,這個方法是一個final類型的方法,意味着子類不能重寫此方法,在此方法裏調用了onMeasure方法,這個是咱們自定義控件時重寫的方法並在此方法里根據子控件的寬高來給控件設置寬高。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if (forceLayout || needsLayout) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
//若是緩存不存在或者忽略緩存則調用onMeasure方法
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
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
}
複製代碼
在VIew的默認onMeasure裏調用的是setMeasuredDimension方法,setMeasuredDimension就是給mMeasuredWidth與mMeasuredHeight賦值,通常狀況下mMeasuredWidth與mMeasuredHeight的值就是控件真正的寬高了。
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;
}
複製代碼
對於ViewGroup,它沒有重寫View的onMeasure方法,可是咱們要基於ViewGroup作自定義控件時,通常都會重寫onMeasure方法,不然可能會致使這個控件的wrap_content沒法使用。在此方法裏會去遍歷全部子控件,而後對每一個子控件進行測量。在測量子控件時必須調用子控件的measure方法,不然測量無效,最後根據Mode來判斷是否將獲得的寬高給這個控件。ViewGroup提供一個measureChild
方法,固然咱們也能夠本身來實現。
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);
}
複製代碼
補充一點,在測量過程當中通常用的比較多的都是EXACTLY與AT_MOST這兩種測量模式,那麼UNSPECIFIED在那裏有應用尼?系統控件裏,那在那些系統控件中啊?嗯,那就是ListView與ScrollView中,在這兩個控件測量子控件的過程當中,都傳遞了UNSPECIFIED這個類型,首先來看ListView的測量子控件的代碼。
private void measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint) {
LayoutParams p = (LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
child.setLayoutParams(p);
}
p.viewType = mAdapter.getItemViewType(position);
p.isEnabled = mAdapter.isEnabled(position);
p.forceAdd = true;
final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
final int lpHeight = p.height;
final int childHeightSpec;
//當子控件的高設置爲match_parent或者wrap_content時,拿到高度lpHeight是小於0的,因此會走else
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(heightHint, MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
...
}
複製代碼
在ScrollView中,重寫了measureChildWithMargins
這個方法,在此方法裏對子控件進行測量,而且對子控件傳遞的高度都是UNSPECIFIED類型的。
@Override
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 usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
//高度的mode是MeasureSpec.UNSPECIFIED
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
在ListView中,當mode爲MeasureSpec.UNSPECIFIED時,計算的就是第一個item的高度,這也就是當ListView或者ScrollView嵌套ListView時,只會顯示一個item的緣由。 到此,測量過程就梳理完畢了,嗯,當在自定義ViewGroup時,最後必定要將測量出來的寬高傳遞給setMeasuredDimension,不然該控件不會顯示。
layout是來肯定控件的位置,在前面將控件的寬高測量完畢後,會將控件的left、top、right、bottom的的位置傳給layout,若是是一個ViewGroup的話,則會在onLayout方法裏對全部子控件進行佈局,在onLayout方法裏調用child.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) {
//在View裏,onLayout是空實現,通常在ViewGroup裏都是重寫onlayout
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);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
複製代碼
left、top、right、bottom這幾個參數很是重要,由於這四個值一旦肯定了,控件在父容器中的位置也就肯定了,且該控件的寬就是right-left
,高就是bottom-top
。
draw就比較簡單了,它的做用就是將View繪製到屏幕上面。View的繪製過程遵循以下幾步:
drawBackground(canvas);
onDraw(canvas);
dispatchDraw(canvas);
onDrawForeground(canvas);
代碼以下: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);
drawAutofilledHighlight(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);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...
}
複製代碼
上面就是View的繪製流程了,比較簡單。關於如何本身調用onDraw來繪製控件能夠閱讀Android自定義控件三部曲文章索引這一系列文章,這一系列關於自定義控件寫的很是詳細。補充一點,在View裏有一個特殊的方法setWillNotDraw
方法,它主要是設置優化標誌位的。若是一個View不須要繪製任何內容,那麼就會將這個標誌設爲true,系統會進行相應的優化,在ViewGroup裏會默認啓動這個優化標誌位。這個標誌位的對實際開發的意義是:當咱們的自定義控件繼承於ViewGroup而且自己不具備繪製功能時,就能夠開啓這個標記位從而便於系統進行後續的優化。固然,當明確知道一個ViewGroup須要經過onDraw來繪製內容時,能夠顯示的關閉WILL_NOT_DRAW
這個標記。
/** * If this view doesn't do any drawing on its own, set this flag to * allow further optimizations. By default, this flag is not set on * View, but could be set on some View subclasses such as ViewGroup. * * Typically, if you override {@link #onDraw(android.graphics.Canvas)} * you should clear this flag. * * @param willNotDraw whether or not this View draw on its own */
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
複製代碼
View的繪製流程到這就梳理完畢了,看到這裏基本上就對View的繪製流程有必定的瞭解了,最後感謝《Android藝術探索》這本書。