View的工作原理
当Activity对象被创建的时候,会将DecorView添加到Window中,同时创建ViewRootImpl对象并将它和DecorView关联起来
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot完成的
View的绘制是从ViewRoot的performTraversals方法开始
MeasureSpec
在测量过程中,系统会将View中的LayoutParams根据父容器所施加的规则转化为对应的MeasureSpec,然后根据MeasureSpec来测量出View的最终宽与高
记住,这是测量,它不一定为View的最终的宽与高
MeasureSpec是一个32位int值,高2位代表SpecMode,剩下的代表SpecSize
1.SpecMode
SpecMode分为3种,
- UNSPECIFIED,是父容器对子View没有限制,要多大给多大
- EXACTLY,是父容器检测出子View的实际大小,View的最终大小就是SpecSize所指定的值
- AT_MOST,View的值不能超过这个值
2.MeasureSpec与LayoutParams的关系
在View的测量过程中,系统会将LayoutParams在父容器的约束下转化为对应的MeasureSpec,然后根据MeasureSpec来确定view测量后的宽/高
但是MeasureSpec并不只是由LayoutParams一个决定。它也由父容器决定
View种类 | 父容器 | LayoutParams |
---|---|---|
顶级View(DecorView) | 窗口尺寸 | 自身的LayoutParams |
普通View | 父容器的MeasureSpec | 自身的LayoutParams |
一旦MeasureSpec确定后,onMeasure可以确定View的测量宽/高
3.获得DecorView的MeasureSpec相关源码
3.1ViewRootImpl的measureHierarchy
在ViewRootImpl中measureHierarchy方法中
childwidthMeasureSpec = getRootMeasureSpec(desiredwindowwidth,lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height); performMeasure(childwidthMeasureSpec,childHeightMeasureSpec);
展示了DecorView的MeasureSpec的创建过程,把屏幕的宽度,屏幕的高度分别传给childwidthMeasureSpec与childHeightMeasureSpec
然后,performMeasure()
方法被调用该方法会触发子视图的测量过程,包括调用子视图的onMeasure()
方法。
点击getRootMeasureSpec看它怎么根据期望的窗口宽度和子视图的布局参数计算子视图的宽度测量规格
根据期望的窗口高度和子视图的布局参数(lp.height
)计算子视图的高度测量规格(childHeightMeasureSpec
)。
3.2getRootMeasureSpec
先看看源码
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;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
rootDimension是视图的布局参数
根据rootDimension来进行选择
- 如果rootDimension==ViewGroup.LayoutParams.MATCH_PARENT,表示根视图需要填充整个窗口,因此测量规格采用
EXACTLY
模式,并将测量尺寸设置为窗口大小(windowSize
)。 - 如果rootDimension==ViewGroup.LayoutParams.WRAP_CONTENT,表示根视图需要根据内容自适应大小,因此测量规格采用
AT_MOST
模式,并将测量尺寸设置为窗口大小(windowSize
)。 - 否则的话,根视图需要具体的固定大小,因此测量规格采用
EXACTLY
模式,并将测量尺寸设置为布局参数的值(rootDimension
)。
这里的rootDimension感觉和MeasureSpec有点相似
-
当rootDimension为LayoutParams.MATCH_PARENT,则为精确模式,测量尺寸的大小就是窗口的大小。和MeasureSpec中的EXACTLY很像
-
当rootDimension为LayoutParams.WRAP_CONTENT,则为最大模式,大小不确定,但是测量尺寸的大小不能超过窗口的大小。和MeasureSpec中的AT_MOST很像
-
default中的就是精确模式,大小为LayoutParams中指定的大小,你看最后传的是MeasureSpec.EXACTLY
可是看了这么多,感觉还是不太理解windowSize和rootDimension分别表示的是什么
我只知道传入getRootMeasureSpec中左边那个参数是windowSize,右边那个是rootDimension
chatGpt中说到
屏幕的大小是指设备屏幕的物理尺寸,例如手机屏幕的宽度和高度。
而子视图的布局参数是指在布局文件(XML)中为视图指定的宽度(width)和高度(height)等属性,用于确定视图在父容器中的大小和位置。
4.获得普通View的MeasureSpec相关源码
布局中的View的measure过程由ViewGroup传递过来,
先看一下ViewGroup中的measureChildWithMargins方法
4.1measureChildWithMargins方法源码
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);
}
我们从中可以得到:
1.在调用子元素的measure方法之前会先通过getChildMeasureSpec得到子元素的MeasureSpec
2.子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,还与View的margin与padding有关
这时候我们查看getChildMeasureSpec里面的源码,看看它在里面干了些什么
4.2getChildMeasureSpec方法源码
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 边距(外边距margin 内边距 padding) 最终影响是size
// 且 size 最终在 childDimension 是 LayoutParams.MATCH_PARENT,保存到返回值上面
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
根据父容器的MeasureSpec同时结合View本身的LayoutParams确定子元素的MeasureSpec
参数中的padding为父容器已占用的空间的大小。
子元素的可用空间为父容器的尺寸大小减去padding
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0,specSize - padding);
下图是普通View的MeasureSpec的创建规则
当子View采用固定大小时,即第一行,不管父类ViewGroup多大,View的MeasureSpec都是精确模式,遵循它自身的LayoutParams
当子View为宽/高为match_parent,如果父容器的模式是精确模式,那么View也是精确模式,大小是父容器的剩余空间;如果父容器为最大模式,那么View也是最大模式,大小不会超过父容器剩余空间。
当View的宽/高为wrap_content,不管父容器模式是最大模式还是精确模式,View都是不超过父容器剩余空间
UNSPECIFIED模式不进行考虑,因为它主要用于系统内部多次Measure情形
View的工作流程
1.Measure
1.1View的Measure
View的Measure动作是在measure方法中完成的,但是它的measure是由final修饰的,及它的方法不能进行重写。
但是在调用measure的时候,还会调用onMeasure
下面是onMeasure的源码
1.1.1onMeasure源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
虽然我们不知道这个是干什么的,但是
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)
和
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
我们应该可以看出来,它会设置View的宽/高
所以我们看看getDefaultSize的源码
1.1.2getDefaultSize源码
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: //一般用于系统内部的测量,Vew的大小为getDefaultSize的第一个参数,即宽/高分别为
//getSuggestedMinimumWidth和getSuggestedMinimumHeight
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY://从这里可以看出,直接继承View的自定义控件,需要重写onMeasure方法并设置
//wrap_comtent时,自身大小。否则wrap_comtent 就相当与使用match_parent
result = specSize;
break;
}
return result; //返回测量后的大小,即MeasureSpec的getSize
}
我们先看下面的代码,因为在上面我们说过了
我们一般不考虑MeasureSpec.UNSPECIFIED,因为它主要用于系统内部多次Measure情形
所以我们就主要看的是MeasureSpec.AT_MOST
和MeasureSpec.EXACTLY
result最开始的初值是getSuggestedMinimumWidth()或者getSuggestedMinimumHeight()。
在MeasureSpec.EXACTLY中,将测量出来的specSize赋值给它
而如果是在MeasureSpec.AT_MOST中,result的值是还是getSuggestedMinimumWidth()或者getSuggestedMinimumHeight()
那么getSuggestedMinimumWidth()和getSuggestedMinimumHeight()到底是什么呢?我们继续看它的源码
1.1.3getSuggestedMinimumWidth()与getSuggestedMinimumHeight()的源码
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//如果View没有设置背景,那么View的长度为mMinWidth(即android:minWidth这个属性指定的值,如果不指定为0)
//如果指定的背景,则为 max(mMinWidth, mBackground.getMinimumWidth())
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
上下两个实现原理基本一样,所以就先讲上面这个
它先进行判断,如果view没有设置背景,则返回mMinWidth
mMinWidth对应于android:minWidth这个属性特指的值,一般不指定的话,那么它默认为0
如果view设置了背景,那它会判断是mMinWidth大还是**mBackground.getMinimumWidth()**更大
那么
mBackground.getMinimumWidth()是什么呢?
// mBackground.getMinimumWidth()
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;//返回的是Drawable的原始宽度
}
可以看出getMinimumWidth()返回的是Drawable的原始宽度,前提是Drawable由原始宽度,如果没有原始宽度则返回0
否则返回原始宽度
1.1.4View的Measure的大体流程
所以所以我们可以理清思路,View的Measure的大体流程是:
1.1.5解决自定义view中wrap_content问题
直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时自身的大小,否则在布局中使用wrap_content就相当于使用match_partent。View在布局中使用wrap_content,那么它的specMode时AT_MOST模式,宽高=specSize,这种情况下View的specSize是parentSize,parentSize是父容器中目前可以使用的大小,这种效果和在布局中使用match_parent完全一致。
重写的代码
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = View.MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = View.MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = View.MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode== MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight);
}else if(widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSpecSize);
}else if(heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,mHeight);
}
其中
int widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = View.MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = View.MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = View.MeasureSpec.getSize(heightMeasureSpec);
其中知道它的Mode和测量出来的宽高,注意在Mode为MeasureSpec.AT_MOST情况下,它测量出来的宽高就基本上等于ViewGroup的宽高了,
然后进行判断
- 如果它的宽和高都设置的是MeasureSpec.AT_MOST,就代表它的宽高都想设置为wrap_content,那么就给setMeasuredDimension传入mWidth与mHeight
- 如果他得到宽设置成MeasureSpec.AT_MOST,则它的宽设置为wrap_content,那么setMeasuredDimension传入mWidth与heightSpecSize
- 这个和2差不多
1.2ViewGroup的Measure
对于ViewGroup来说,除了完成自己的measure过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。ViewGroup是一个抽象类,因此没有重写View的onMeasure,但是提供了一个叫measureChildren的方法。
1.2.1measureChildren源码
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec); //调用子元素的measure
}
}
}
调用measureChildren()方法的时候,会对每一个child进行measure
1.2.2measureChild源码
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();//1.获取子元素的LayoutParams
//2.根据getChildMeasureSpec创建子元素的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//3.调用子元素的measure
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
它的作用就是获取子元素的LayoutParams,然后根据getChildMeasureSpec创建子元素的MeasureSpec,最后调用子元素的measure
getChildMeasureSpec在上面说过了,
就是这张表
在ViewGroup并没有定义其测量的具体的过程,ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类具体实现。
ViewGroup不像View一样对其onMeasure方法做统一实现,因为不同的ViewGroup子类有不同的布局特性,导致他们的测量细节各不相同。
1.3LinearLayout的onMeasure方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);//竖直布局
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);//水平布局
}
}
看看measureVertical的核心代码
1.3.1measureVertical源码
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
···
// 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); //这个方法内部会调用子元素法measure方法,对子元素进行measure过程
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)); //mTotalLength存储LinearLayout初步高度。每测量一个子元素,mTotalLength就会增加。
系统会遍历元素,并对每个元素执行 measureChildBeforeLayout方法,
这个方法内部会调用子元素法measure方法,对子元素进行measure过程并且系统会通过mTotalLength存储LinearLayout在竖直方向的高度的初步高度。每测量一次子元素,mTotalLength就会增加,增加的既包括了子元素的高度,又包括了竖直向上的margin
子元素测量完成后,LinearLayout会自己测量自己的高度
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);
···
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
//如果布局采用的match_parent或者具体数值,那么测量过程和View一致,高度为specSize
//如果采用的是wrap_content,那么他的高度是所有子元素所占有的高度总和,但仍然不超过父容器的剩余空间。详见下面的源码
针对竖直方向的LinearLayout,它的水平方向的测量过程遵循View的
竖着方向,如果是match_parent,则和view一致,如果是wrap_content,它的高度为所有子元素的所占有的高度的总和。仍然不超过父容器的剩余空间
我们看看resolveSizeAndState里面的源码,看看它是怎么做的
1.3.2resolveSizeAndState源码
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的getDefaultSize
都是先对specMode进行判断
- 当specMode为MeasureSpec.AT_MOST且specSize < size,则将 result 设置为 specSize 并标记为 MEASURED_STATE_TOO_SMALL
- 当specMode为MeasureSpec.AT_MOST且specSize>=size,将View的size赋值给result
- 当specMode为MeasureSpec.EXACTLY,result = 测量出来的specSize
- 当specMode为MeasureSpec.UNSPECIFIED,表示 View 的大小没有限制,直接将 result 设置为 View 的大小。
1.3.3流程图
1.4如何保证在Activity的生命周期类完成对View的测量
1.4.1Activity/View#onWindowFocusChanged
这个方法的含义是:View已经初始化完毕,宽/高已经准备好了,这个时候获取宽/高没有问题了。
onWindowFocusChanged会被调用好多次,当Activity的窗口得到焦点和失去焦点的时候均会被调用一次。当Activity继续执行和暂停执行的时候,onWindowFocusChanged均会被调用。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
1.4.2view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了
在Android中,View
类的post(Runnable)
方法可以将一个Runnable
对象投递到视图所关联的消息队列的尾部。当Looper
处理该消息时,会调用Runnable
对象的run()
方法。
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
1.4.3ViewTreeObserver
使用ViewTreeObserer的众多回调可以完成这个功能。比如使用onGlobalLayoutListener这个接口,当View树状态或View树内部的View的可见性发生改变时,onGlobalLayout方法被回调。伴随View树的改变,onGlobalLayout方法将会被多次调用。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width= view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
1.4.4view.measure()
1.4.4.1match_parent
直接放弃,因为如果自定义的view为match_parent,那么构造MeasureSpec的时候,我们最后自定义的view与父类的ViewGroup有关
需要知道父容器的剩余空间,但是我们没办法知道ViewGroup的剩余空间,所以也没办法做
1.4.4.2固定数值
如果为固定数值的话,自定义的view与它childSize有关,View的MeasureSpec都是精确模式,遵循它自身的LayoutParams
比如说现在我的宽/高 = 100px
那么我想得到它的宽和高我就可以通过代码
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
我当时在想,match_parent和wrap_content用这种**view.measure()**我到时能想的通,本来它们的宽/高就是未知数,测一下很正常。但是为固定数值的情况下,为什么还要测量,它不是已经给定了100px了嘛
但是
viewGroup不一样的话,虽然你的子view有100px,最后可能显示出来不是100Px,因为子view的大小不仅与自身的Layout有关,也与ViewGroup有关
1.4.4.3wrap_content
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT.MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT.MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
为什么这么写了因为
View的尺寸使用30位二进制表示,最大是30个1(2^31-1),也就是(1<<30)-1
1.4.5总结
方法 | 具体操作 |
---|---|
onWindowFocusChanged | 重写 onWindowFocusChanged,当它失去焦点或者得到焦点的时候view.getMeasuredWidth()/view.getMeasureHeight() |
view.post(runnable) | post会将一个runnable对象发送到Looper的队尾,等Looper调用此Looper的时候,view也就初始化好了 view.post(new Runnable() { @Override public void run() { int width = view.getMeasuredWidth(); int height = view.getMeasuredHeight(); } }); |
ViewTreeObserver | 使用onGlobalLayoutListener这个接口,当View树状态或View树内部的View的可见性发生改变时,onGlobalLayout方法被回调。伴随View树的改变,onGlobalLayout方法将会被多次调用。 |
view.measure() | 自己分析,三种情况下,进行判断 |
2.layout过程
ayout是确定View本身的位置,onLayout是确定子view的位置
2.1layout()源码
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);//通过setFrame来设定View的四个顶点位置
//即初始化mLeft,mTop,Bottom,mRight
//那么View在父容器中的位置也确定了
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
//接着调用onLayout方法
//父容器确定子元素的位置,但View和ViewGroup中均没有真正实现。因为其具体实现和布局有关
····
}
大概就记住这段源码里面两个方法就行
setFrame();
通过setFrame来设定View的四个顶点位置,即初始化mLeft,mTop,Bottom,mRight,那么View在父容器中的位置也确定了
onLayout(changed, l, t, r, b);
这个就是父容器确定子元素的位置
2.2LinearLayout中的onLayout
LinearLayout中的onLayout和onMeasure差不多
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
上面这个是LinearLayout中onLayout的源码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);//竖直布局
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);//水平布局
}
}
这个是LinearLayout中onMeasure的源码
和之前一样,我们看其中一个的源码
2.3layoutVertical源码
void layoutVertical(int left, int top, int right, int bottom) {
......
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth(); //width和height实际上就是子元素的测量宽/高
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
layoutVertical就是遍历所有的子元素,然后调用setChildFrame方法为子元素指定对应的位置,其中childTop会逐渐增大,后面的元素也就放在靠下的位置,setChildFrame仅仅只是调用了子元素的layout方法,这样父元素在layout完成定位后,通过onLayout,调用子View的layout方法,子View就会通过自己的layout方法确定自己的位置。
setChildFrame源码
private void setChildFrame(View child,int left,int top,int width,int height){
child.layout(left,top,left+width,top+height);
}
2.4View的测量宽高和最终宽高有什么区别
即View的getMeasuredWidth/getMeasuredHeight和getHeight有什么区别?
测量宽高形成于View的measure过程,最终宽高形成于View的layout过程,日常开发中可认为两者相等,存在特殊情况会导致两者不一致。
如重写View的layout方法:
@Override
public void layout( int l , int t, int r , int b){
// 改变传入的顶点位置参数
super.layout(l,t,r+100,b+100);
// 如此一来,在任何情况下,getWidth() / getHeight()获得的宽/高 总比 getMeasuredWidth() / getMeasuredHeight()获取的宽/高大100px
// 即:View的最终宽/高 总比 测量宽/高 大100px
}
3.draw过程
将View绘制到屏幕上面
(1)绘制背景 background.draw(canvas)
(2)绘制自己 (onDraw)
(3)绘制children(dispatchDraw)
(4)绘制装饰 (onDrawScrollBars)
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绘制过程的传递通过dispatchDraw来实现,dispatchDraw会遍历调用所有子元素的draw方法
View有一个特殊的方法setWillNotDraw
/**
* 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);
}
作用:设置WILL_NOT_DRAW 标记位
一个View不需要绘制任何内容,设置这个标记位为true后,系统会进行相应的优化。
默认情况下,View没有启用这个优化标记为,但ViewGroup会默认启用这个优化标记位。
对开发的实际意义:当我们自定义控件继承于ViewGroup并且本身不具备绘制功能,可以开启这个标记位从而便于系统进行后续的优化。