一、从setContentView(R.layout.activity_main);入手了解UI的绘制起始过程
1.Activity.java
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);//①
initWindowDecorActionBar();
}
2.getWindow()拿到的是Window的实现类PhoneWindow
mWindow = new PhoneWindow(this, window);
PhoneWindow源码:
在 com.android.internal.policy包下面
1.看源码需要找到程序的入口,这个入口就是setContentView,找到入口了以后根据线索往下看。
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
//首先看到了这个方法,就是初始化DecorView
installDecor();//②
}
……
//这个方法是将我们的布局加载到mContentParent当中
//我们还需要知道mContentParent是如何加载的,以及mContentParent是什么?带着疑问往下看
mLayoutInflater.inflate(layoutResID,mContentParent
//⑥最后将布局渲染到帧布局当中。
}
2.installDecor这个方法是初始化DecorView,以及mContentParent。
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();//③生成一个DecorView(继承的FrameLayout)
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);//④
}
3.generateLayout()这个方法就是根据我们设置的主题类型,来加载相应的布局,将状态栏以及标题栏以及一个加载我们布局的帧布局加载到DecorView当中。
protected ViewGroup generateLayout(DecorView decor) {//⑤
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
}
.............
上面判断加载哪一种布局,有的有actionBar 有的没有,还有其他种类的布局,然后加载添加到decor中。
下图中就是一种简单的布局。不含有ActionBar,但是有一个ViewStub方便以后添加标题栏
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
//然后将我们的布局填充到这个布局当中
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
}
}
mContentRoot 就是图中的FrameLayout。到此就将系统中的布局加载完成,然后回到上面调用mLayoutInflater.inflate(layoutResID,mContentParent{}这个方法,将我们的布局渲染到系统当中。
D:\Software\Android\SDK\platforms\android-23\data\res ,查看系统的资源文件
PS: 这里我们来看一下snackbar添加到布局中的原理,是不是很熟悉。
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if (view instanceof CoordinatorLayout) {
// We've found a CoordinatorLayout, use it
return (ViewGroup) view;
} else if (view instanceof FrameLayout) {
if (view.getId() == android.R.id.content) {
//见上图中的id/content 将SnackBar添加到DecorView中FrameLayout
// If we've hit the decor content view, then we didn't find a CoL in the
// hierarchy, so use it.
return (ViewGroup) view;
} else {
// It's not the content view but we'll use it as our fallback
fallback = (ViewGroup) view;
}
}
if (view != null) {
// Else, we will loop and crawl up the view hierarchy and try to find a parent
final ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
} while (view != null);
// If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
return fallback;
}
二、ViewRootImpl
View类中的三个方法比较重要
measure:测量,测量自己有多大,如果是ViewGroup的话会同时测量里面的子控件的大小
layout:摆放里面的子控件bounds(left,top,right,bottom)
draw:绘制 (直接继承了view一般都会重写onDraw)
但是这三个方法是在什么时候,在哪里被调用的呢?这个时候ViewRootImpl就登场了,首先看performTraversals()方法
1.performTraversals()
这里先贴一下伪代码
performTranversal(){
// Ask host how big it wants to be
·········
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
performDraw();
·········
}
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);
}
}
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
·······
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
·······
mInLayout = false;
}
draw方法需要经过几个方法的调用,代码太多这里就不贴了,有兴趣的可以去看一下源码。
这里们我们知道了ViewRootImpl如何调用View的几个方法,但是他们是如何绑定在一起的呢?
带着疑问我们接着看。
2.View与ViewRootImpl的绑定
另外View和ViewRootImpl是怎么绑定在一起的呢?通过view.getViewRootImpl可以获取到ViewRootImpl。
public ViewRootImpl getViewRootImpl() {
if (mAttachInfo != null) {
return mAttachInfo.mViewRootImpl;
}
return null;
}
而这个AttachInfo则是View里面一个静态内部类,它的构造方法
AttachInfo(IWindowSession session, IWindow window, Display display,
ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer) {
mSession = session;
mWindow = window;
mWindowToken = window.asBinder();
mDisplay = display;
mViewRootImpl = viewRootImpl;
mHandler = handler;
mRootCallbacks = effectPlayer;
}
可以看到viewRootImpl在它的构造方法里赋值了,那么这个方法肯定是在ViewRootImpl创建时创建的,而ViewRootImpl的创建是在调用WindowManagerGlobal.addView的时候
root = new ViewRootImpl(view.getContext(), display);
而构造方法中
public ViewRootImpl(Context context, Display display) {
mContext = context;
mWindowSession = WindowManagerGlobal.getWindowSession();
...
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
...
}
可以看到View与ViewRootImpl绑定一起了。
之后就可以通过view.getViewRootImpl获取到。这样我们就差不多了解到View是怎么绑定到ViewRootImpl,以及绘制的流程。
3.view的requestLayout()方法
这个方法大家肯定是非常的熟悉,因为在自定义控件的时候会经常调用这个方法。拿它是怎样刷新视图的呢?
先看一下代码
@CallSuper
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
我们看到了mParent.requestLayout();这个方法,看到这里肯定在想mParent是什么?我们看一下它的定义
/**
* The parent this view is attached to.
* {@hide}
* @see #getParent()
*/
protected ViewParent mParent;
了解后发现ViewParent 是一个接口,需要知道它有哪些实现类,以及mParent是如何赋值的,阅读代码后发现ViewRootImpl正好是他的一个实现类,同时mParent只有一个赋值的方法:
/*
* Caller is responsible for calling requestLayout if necessary.
* (This allows addViewInLayout to not request a new layout.)
*/
void assignParent(ViewParent parent) {
if (mParent == null) {
mParent = parent;
} else if (parent == null) {
mParent = null;
} else {
throw new RuntimeException("view " + this + " being added, but"
+ " it already has a parent");
}
}
如果我们知道这个方法在哪调用,我们离知道requestLayout()是如何刷新视图的真相不远了,联想到ViewRootImpl是ViewParent的实现类,带着激动的心情马上去看一下。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
view.assignParent(this);
}
原来那个mParent就是ViewRootImpl这个类。这一下就很清晰了。
然后我们再看看ViewRootImpl#requestLayout方法。看它是如何重汇整个视图的。
ViewRootImpl调用到requestLayout()来完成View的绘制操作,我们看下源码
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
View绘制,先判断当前线程
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
如果不是当前线程则抛出异常,这个异常是不是感觉很熟悉啊。没错,当你在子线程更新UI没使用handler的话就会抛出这个异常
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
抛出地方就是这里,一般在子线程操作UI都会调用到view.invalidate,而View的重绘会触发ViewRootImpl的requestLayout,就会去判断当前线程。
接着看,判断完线程后,接着调用scheduleTraversals()
void scheduleTraversals() {
if (!mTraversalScheduled) {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
scheduleTraversals中会通过handler去异步调用mTraversalRunnable接口
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
接着
void doTraversal() {
...
performTraversals();
...
}
可以看到,最后真正调用绘制的是performTraversals()方法。看到这里仿佛打通了任督二脉。太通畅了。终于明白整个绘制流程了。
到这里我们就知道了View的几个方法是如何被调用的,下面介绍一下View方法具体的执行流程。
4.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方法里面会调用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);
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;
}
}
.........
}
onMeasure根据传入父类的MeasureSpec,得到自身的大小。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
子类的MeasureSpec由父类和LayoutParams一起决定。
下面我们再来看看getDefaultSize这个方法:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
结合上图可以知道如何测量出View的大小。
我们再来看看:getSuggestedMinimumWidth()方法。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
public int getIntrinsicWidth() {
return -1;
}
这种情况是确保View设置了背景也能准确的测量出View的大小。
5 ViewGroup的测量流程
在View的测量流程中,通过makeMeasureSpec来保存宽高信息,在其他流程通过getMode或getSize得到模式和宽高。那么问题来了,上面提到MeasureSpec是LayoutParams和父容器的模式所共同影响的,那么,对于DecorView来说,它已经是顶层view了,没有父容器,那么它的MeasureSpec怎么来的呢?
阅读源码的时候一定要带着疑问,这时候我们提出了问题,那么接下来就要在源码中找出答案。
MeasureSpec肯定是在测量之前就已经准备好,这时候我们就想什么时候开始测量的呢?上面已经提到过ViewRootImpl#PerformTraveals这个方法是UI绘制的起点,那我们就去那里看一下。果不其然,看到了我们想看的东西。
private void performTraversals() {
...
if (!mStopped) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); // 1
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
·······
}
/**
* @param windowSize
* The available width or height of the window
*
* @param rootDimension
* The layout params for one dimension (width or height) of the
* window.
*
* @return The measure spec to use to measure the root view.
*/
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
//省略...
}
return measureSpec;
}
getRootMeasureSpec(desiredWindowWidth,lp.width)方法,其中desiredWindowWidth就是屏幕的尺寸,并把返回结果赋值给childWidthMeasureSpec成员变量(childHeightMeasureSpec同理),因此childWidthMeasureSpec(childHeightMeasureSpec)应该保存了DecorView的MeasureSpec。
下面我们来看一下ViewGroup的测量过程,ViewGroup是继承了View,调用的是View的measure方法,我们主要看一下ViewGroup的onMeasure方法,就以LinearLayout为例:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
int maxWidth = 0;
int childState = 0;
int alternativeMaxWidth = 0;
int weightedMaxWidth = 0;
boolean allFillParent = true;
float totalWeight = 0;
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
boolean skippedMeasure = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
int consumedExcessSpace = 0;
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// Optimization: don't bother measuring children who are only
// laid out using excess space. These views will get measured
// later if we have space to distribute.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
// The heightMode is either UNSPECIFIED or AT_MOST, and
// this child is only laid out using excess space. Measure
// using WRAP_CONTENT so that we can find out the view's
// optimal height. We'll restore the original height of 0
// after measurement.
lp.height = LayoutParams.WRAP_CONTENT;
}
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
/**
* If applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}
boolean matchWidthLocally = false;
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
matchWidth = true;
matchWidthLocally = true;
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
if (useLargestChild &&
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
int remainingExcess = heightSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final float childWeight = lp.weight;
if (childWeight > 0) {
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
remainingExcess -= share;
remainingWeightSum -= childWeight;
final int childHeight;
if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
childHeight = largestChildHeight;
} else if (lp.height == 0 && (!mAllowInconsistentMeasurement
|| heightMode == MeasureSpec.EXACTLY)) {
// This child needs to be laid out from scratch using
// only its share of excess space.
childHeight = share;
} else {
// This child had some intrinsic height to which we
// need to add its share of excess space.
childHeight = child.getMeasuredHeight() + share;
}
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
Math.max(0, childHeight), MeasureSpec.EXACTLY);
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
lp.width);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
// Child may now not fit in vertical dimension.
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
// We have no limit, so make all weighted views as tall as the largest child.
// Children will have already been measured once.
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
child.measure(
MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(largestChildHeight,
MeasureSpec.EXACTLY));
}
}
}
}
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
这个源码是比较复杂的,但是我们可以看出的是,如果自身大小是确定的,那么高度就是自身的大小,否则的话需要测量所有的子View,同时还有分割线之类的,将他们的高度叠加。设置自身的大小。
总结
一、measure的过程
如何去合理的测量一颗View树?
如果ViewGroup和View都是直接指定的宽高,我还要测量吗?
正是因为谷歌设计的自适应尺寸机制(比如Match_parent,wrap_content),造成了宽高不确定,所以就需要进程测量measure过程。
measure过程会遍历整颗View树,然后依次测量每一个View的真实的尺寸。(树的遍历–先序遍历)
MeasureSpec:测量规格
int 32位:010111100011100
拿前面两位当做mode,后面30位当做值。
1.mode:
1) EXACTLY: 精确的。比如给了一个确定的值 100dp
2) AT_MOST: 根据父容器当前的大小,结合你指定的尺寸参考值来考虑你应该是多大尺寸,需要计算(Match_parent,wrap_content就是属于这种)
3) UNSPECIFIED: 最多的意思。根据当前的情况,结合你制定的尺寸参考值来考虑,在不超过父容器给你限定的只存的前提下,来测量你的一个恰好的内容尺寸。
用的比较少,一般见于ScrollView,ListView(大小不确定,同时大小还是变的。会通过多次测量才能真正决定好宽高。)
2.value:宽高的值。
经过大量测量以后,最终确定了自己的宽高,需要调用:setMeasuredDimension(w,h)
写自定义控件的时候,我们要去获得自己的宽高来进行一些计算,必须先经过measure,才能获得到宽高—不是getWidth(),而是getMeasuredWidth()
也就是当我们重写onMeasure的时候,我们需要在里面调用child.measure()才能获取child的宽高。
从规格当中获取mode和value:
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
反过来将mode和value合成一个规格呢:
MeasureSpec.makeMeasureSpec(resultSize, resultMode);
ViewGroup:
设计它的目的是什么?
1)作为容器处理焦点问题。
2)作为容器处理事件分发问题;
3)控制容器添加View的流程:addView(),removeView()
4)抽出了一些容器的公共的工具方法:measureChildren,measureChild,measureChildWidthMargins方法。
——————-重点:———————–
玩自定义控件的时候,需要进行测量measure,如何做好这件事?
两种情况:
1.继承自View的子类
只需要重写onMeasure测量好自己的宽高就可以了。
最终调用setMeasuredDimension()保存好自己的测量宽高。
套路:
int mode = MeasureSpec.getMode(widthMeasureSpec);
int Size = MeasureSpec.getSize(widthMeasureSpec);
int viewSize = 0;
switch(mode){
case MeasureSpec.EXACTLY:
viewSize = size;//当前view的尺寸就为父容器的尺寸
break;
case MeasureSpec.AT_MOST:
viewSize = Math.min(size, getContentSize());//当前view的尺寸就为内容尺寸和费容器尺寸当中的最小值。
break;
case MeasureSpec.UNSPECIFIED:
viewSize = getContentSize();//内容有多大,久设置多大尺寸。
break;
default:
break;
}
//setMeasuredDimension(width, height);
setMeasuredDimension(size);
2.继承自ViewGroup的子类:
不但需要重写onMeasure测量自己,还要测量子控件的规格大小。
可以直接使用ViewGroup的工具方法来测量里面的子控件,也可以自己来实现这一套子控件的测量(比如:RelativeLayout)
套路:
//1.测量自己的尺寸
ViewGroup.onMeasure();
//1.1 为每一个child计算测量规格信息(MeasureSpec)
ViewGroup.getChildMeasureSpec();
//1.2 将上面测量后的结果,传给每一个子View,子view测量自己的尺寸
child.measure();
//1.3 子View测量完,ViewGroup就可以拿到这个子View的测量后的尺寸了
child.getChildMeasuredSize();//child.getMeasuredWidth()和child.getMeasuredHeight()
//1.4ViewGroup自己就可以根据自身的情况(Padding等等),来计算自己的尺寸
ViewGroup.calculateSelfSize();
//2.保存自己的尺寸
ViewGroup.setMeasuredDimension(size);
二、layout的过程
测量完毕就是摆放的过程,View中没有Layout的方法。只有ViewGroup才会摆放子控件
三、draw的过程
ViewGroup的onDraw方法默认是不会调用的,因为在ViewGroup构造方法里面就默认设置了
setFlags(WILL_NOT_DRAW, DRAW_MASK);//原因是因为ViewGroup本来就没东西显示,除了设置了背景,这样就是为了效率。
如果需要它执行onDraw可以,设置背景或者如下:
setWillNotDraw(false);
2.自绘控件View/ViewGroup
主要重写onDraw方法绘制,还会要处理onMeasure,onLayout
3.组合控件。
比如封装通用的标题栏控件。
并不需要自己去绘制视图上面显示的内容,而只是用系统原生的控件就可以了。
但是我们可以将几个原生控件组合到一起,可以创建出的控件就是组合控件。
比如:在构造方法里面LayoutInflater.from(context).inflate(R.layout.title,this);然后再加上业务逻辑。
最后的话:有些地方还有点粗糙,后面会继续补充。