在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艺术探索》这本书。