android控件测量,Android控件的测量流程梳理

总体流程

Android控件的测量从根布局开始,根布局即DecorView ;

测量开始的地方,由ViewRootImol类的performMeasure方法开启测量,调用了DecorView的onMeasure方法,

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);

其中参数widthMeasureSpec和heightMeasureSpec里包含的信息有两个,宽高尺寸和尺寸模式,其中宽高为屏幕的尺寸,尺寸模式为MeasureSpec.EXACTLY。

在DecorView的onMeasure方法中,根据传入的两个参数、子View的尺寸信息以及自身的布局逻辑,来判断需要给子View设定的宽高尺寸和尺寸模式,然后继续调用子View的measure方法;

子view在measure方法中会处理一系列逻辑,调用到自身的onMeasure方法,然后根据父布局传入的宽高尺寸、尺寸模式以及自身的具体需求来确定自己最终的尺寸大小;

在上一步中如果子View也是一个ViewGroup子类,则根据传入的参数和自身的逻辑继续测量自己的子View,直到最后一层。

以上就是整个流程的简要归纳,如果上面那段话中让你觉得云里雾里,或者你感觉有很多东西不知道从哪冒出来的,不要紧,我们一个一个点来说,捋清其中的细节。这个结论可以留到最后再来看,到时会更加清晰。

根布局

在Android应用中,你所写的每一个页面,都有一个根布局,这个根布局不是你调用setContentView()时设置的那个,而是DecorView。

我们来捋一捋Activity,DecorView,你填入的布局,还有一个:Window,这几个东西之间的联系。

Window是一个顶级窗口,它定义了窗体样式和行为,提供标准的UI规则,例如背景、标题、默认关键过程等。实际上,每当你写一个新的Activity,在Activity的 attach方法中,都会初始化一个PhoneWindow实例。

final void attach(Context context, ActivityThread aThread,

Instrumentation instr, IBinder token, int ident,

Application application, Intent intent, ActivityInfo info,

CharSequence title, Activity parent, String id,

NonConfigurationInstances lastNonConfigurationInstances,

Configuration config, String referrer, IVoiceInteractor voiceInteractor,

Window window, ActivityConfigCallback activityConfigCallback) {

//省略部分代码...

mWindow = new PhoneWindow(this, window, activityConfigCallback);

//省略部分代码...

}

在你调用setContentView方法的时候,实际上调用的是Window的setContentView方法:

public void setContentView(@LayoutRes int layoutResID) {

getWindow().setContentView(layoutResID);

initWindowDecorActionBar();

}

然后PhoneWindow的setContentView方法中调用了installDecor方法:

@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) {

installDecor();

} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

mContentParent.removeAllViews();

}

//省略部分代码...

}

installDecor()再调用generateLayout方法,这个方法中做了很多事情,会根据设置的主题样式来设置DecorView的风格,比如有没有TitleBar,有没有ActionBar等等;

这个方法中也为DecorView添加了子View,即你通过setContentView设置进来的布局。

protected ViewGroup generateLayout(DecorView decor) {

// Apply data from current theme.

TypedArray a = getWindowStyle();

//省略好多好多代码...

mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

//省略好多好多代码...

return contentParent;

}

onResourcesLoaded代码如下:

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {

mStackId = getStackId();

//省略部分代码...

//根据id实例化你填入的布局

final View root = inflater.inflate(layoutResource, null);

if (mDecorCaptionView != null) {

if (mDecorCaptionView.getParent() == null) {

addView(mDecorCaptionView,

new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

}

mDecorCaptionView.addView(root,

new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));

} else {

// Put it below the color views.

//将你的布局加入到DecorView

addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

}

mContentRoot = (ViewGroup) root;

//省略部分代码...

}

所以,到这里,我们应该明白了,一个页面中的几层关系:

每个Activity会持有一个Window实例

Window下有DecorView

DecorView下是你设置的页面内容布局

开启测量的起始点

现在我们知道根布局在哪里,然后我们来看从哪里开始测量。

View的绘制过程,是由ViewRootImpl这个类来完成的,测量工作当然也包含在其中。ViewRootImpl是连接WindowManager和DecorView的纽带,负责向DecorView分发收到的用户发起的event事件(如按键,触屏等),也负责完成View的绘制。

具体处理绘制流程是在一个performTraversals方法中,这个方法被调用的时候很多:控件焦点变化被调用、显示状态变化被调用、绘制刷新被调用、等等等等...

performTraversals方法内部逻辑相当相当多&复杂,截取相关部分代码:

private void performTraversals() {

// 省略巨多的代码…

if (!mStopped) {

// ……省略一些代码

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);

int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

// ……省省省

performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

}

// 省略巨多的代码…

}

可以看到在performTraversals方法中通过getRootMeasureSpec获取原始的测量规格并将其作为参数传递给performMeasure方法处理,这里我们重点来看getRootMeasureSpec方法是如何确定测量规格的,首先我们要知道mWidth, lp.width和mHeight, lp.height这两组参数的意义,其中lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定,mWidth和mHeight表示当前窗口的大小,其值由performTraversals中一系列逻辑计算确定,这里跳过,而在getRootMeasureSpec中作了如下判断:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {

int measureSpec;

switch (rootDimension) {

case ViewGroup.LayoutParams.MATCH_PARENT:

// Window不能调整其大小,强制使根视图大小与Window一致

measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);

break;

case ViewGroup.LayoutParams.WRAP_CONTENT:

// Window可以调整其大小,为根视图设置一个最大值

measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);

break;

default:

// Window想要一个确定的尺寸,强制将根视图的尺寸作为其尺寸

measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);

break;

}

return measureSpec;

}

所以最终的测量规格的确定走的是这一步:

measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);

这里解释一下传入onMeasure()的这个参数,measureSpec 这个整型值,包含了两个信息,一个是具体尺寸,一个是尺寸模式。具体尺寸很好理解,尺寸模式包括三种:

MeasureSpec.EXACTLY 代表父View已经为子View规定好了具体的尺寸,是否遵循看子view的意愿;

MeasureSpec.AT_MOST 代表父View为子View圈定了一片地方,子View最大不能超过这片地方;

MeasureSpec.UNSPECIFIED 代表父view对子view没有任何约束,子view想多大就多大;

(具体这个整形值如何包含着两个信息的自行查找资料学习,这里略过)

所以回到计算中,这里的结果很明显:尺寸是窗口大小,模式是MeasureSpec.EXACTLY ,顶层DecorView接收到的参数就是这两个。也就是说不管如何,我们的根视图大小必定都是全屏的…

好了,到这一步,我们测量的起点也找到了,顶层View接收到的尺寸参数也明确了,接下来我们看看它如何把测量流程继续往下走。

ViewGroup测量子View

顶层的ViewGroup从onMeasure方法中接收到了尺寸参数,大小确定为屏幕宽高,模式是MeasureSpec.EXACTLY 。

好了,现在顶层的DecorView要开始自己的工作了:我有这么大块地盘,我要好好安置我的儿子们,每人划一片地儿...安置的妥妥的......

为了理清脉络,DecorView自己的布局逻辑我们摒开不看,它在onMeasure方法中调用了父类的onMeasure方法,也就是FrameLayout的onMeasure方法。现在我们把常用的几个Layout拎出来,看看他们的onMeasure方法,比对一下,可以发现,大致都有这么一段逻辑:

1.对子View进行遍历;

2.调用measureChildWithMargins()方法获取对子view的建议尺寸规格(这个是ViewGroup本身的方法);

3.用获取到的尺寸规格,调用子view的measure方法;

由于各种布局自身的排列逻辑不同,相关的实现细节必定差异极大,但是测量的流程却都是几乎相同的,measureChildWithMargins()方法就是其中的共同点之一,来看看这个方法:

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);

}

很简单,获取子View的测量规格,然后调用子View的measure方法。接着看getChildMeasureSpec():

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

int specMode = MeasureSpec.getMode(spec);

int specSize = MeasureSpec.getSize(spec);

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);

}

首先判断自己作为父View的尺寸模式,再判断子view的宽高值:具体值、WRAP_CONTENT、MATCH_PARENT,根据这两个值的具体情况来返回相应的MeasureSpec信息。

然后用这个确定好的MeasureSpec信息传给子View,调用其measure方法,让它确定自身的尺寸。

到这里已经很清晰了...

接下来,就是View拿到父View建议的尺寸规格,结合自身情况,设置自身的具体尺寸大小。

View设定具体宽高

终于到了View这一层了。

View的measure方法逻辑中,会调用到onMeasure方法,其默认的实现是:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}

//onMeasure内用到的的相关方法

protected int getSuggestedMinimumWidth() {

//mMinWidth和mMinHeight 好像都是100px

return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

}

public static int getDefaultSize(int size, int measureSpec) {

// 将我们获得的最小值赋给result

int result = size;

// 从measureSpec中解算出测量规格的模式和尺寸

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不限制,就按自己的背景大小或者最小值来显示,如果父view有限制,就按父view给的尺寸来显示。

按照这个逻辑,如果要自己写一个自定义View,大小可以在布局中确定的话,一般不用再重新onMeasure 再做什么工作了。

但是如果自己的自定义View在布局中使用WRAP_CONTENT,并且内容大小并不确定的话,还是要根据自己的显示逻辑做一些工作的。

比如,自己写一个显示图片的控件,布局中使用WRAP_CONTENT,那么根据以上的逻辑梳理,父view很可能就扔给你一个尺寸模式:大小是父view本身的大小,模式是MeasureSpec.AT_MOST;这样的话即使你布局里写的是WRAP_CONTENT,你也会使用父view建议给你的尺寸,占满父view全部的空间了,即使你的图片并没有那么大~是不是会很奇怪?

所以,一般情况下,展示内容尺寸不确定的自定义View,onMeasure可以作如下类似的逻辑:

@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

// 声明一个临时变量来存储计算出的测量值

int resultWidth = 0;

// 获取宽度测量规格中的mode

int modeWidth = MeasureSpec.getMode(widthMeasureSpec);

// 获取宽度测量规格中的size

int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);

/*

* 如果爹心里有数

*/

if (modeWidth == MeasureSpec.EXACTLY) {

// 那么儿子也不要让爹难做就取爹给的大小吧

resultWidth = sizeWidth;

}

/*

* 如果爹心里没数

*/

else {

// 那么儿子可要自己看看自己需要多大了

resultWidth = getSelfContentWidth();//自己实现...

/*

* 如果爹给儿子的是一个限制值

*/

if (modeWidth == MeasureSpec.AT_MOST) {

// 那么儿子自己的需求就要跟爹的限制比比看谁小要谁

resultWidth = Math.min(resultWidth, sizeWidth);

}

}

int resultHeight = 0;

int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

if (modeHeight == MeasureSpec.EXACTLY) {

resultHeight = sizeHeight;

} else {

resultHeight = getSelfContentHeight();//自己实现...

if (modeHeight == MeasureSpec.AT_MOST) {

resultHeight = Math.min(resultHeight, sizeHeight);

}

}

// 设置测量尺寸

setMeasuredDimension(resultWidth, resultHeight);

}

这样,既考虑了自身内容的尺寸,也适应了View的测量流程,就可以正确的显示大小了。当然,具体情况还是要看自定义view 的具体逻辑,这里只是一个示例,不一定适合各种场合。

那么,到这里,关于Android中控件尺寸测量流程的梳理,差不多就都结束了。现在你再去看开头的结论,会不会清晰一些了?

最后,如发现有问题,请斧正,不胜感激!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android 控件集合是指在Android开发中,用于构建用户界面的一组预定义控件的集合。Android提供了丰富的控件可供开发者使用,包括文本框、按钮、图像视图、列表视图、下拉框等。这些控件可以通过XML布局文件或者Java代码动态创建和配置。 Android 控件集合具有以下特点: 1. 多样性:Android控件集合中包含了丰富的控件种类,满足了不同应用场景下的需求。开发者可以根据具体的界面设计要求选择合适的控件进行使用。 2. 可定制性:Android控件集合提供了丰富的配置属性,开发者可以通过设置这些属性来满足自己的设计需求。同时还可以通过自定义控件继承或覆盖Android原生控件,实现更加个性化的效果。 3. 事件监听:Android控件集合支持事件监听机制,开发者可以为控件添加相应的事件处理器,对用户的交互行为做出响应。比如,可以为按钮控件添加点击事件监听器,实现点击按钮后执行相应的操作。 4. 布局管理:Android控件集合中的控件可以通过各种布局管理器进行灵活的排列和组合。比如,线性布局、相对布局、帧布局等。开发者可以根据需求选择合适的布局管理器,实现想要的页面布局效果。 综上所述,Android控件集合提供了丰富多样的控件供开发者使用,具备较高的可定制性和灵活性,方便开发者构建出各种各样的用户界面。通过合理运用和配置控件集合,可以实现强大的Android应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值