前言
友情提示:文章比较长,包括了大量代码和debug调用图。为了避免浪费大家的时间,开篇咱们看一段极为简单的代码(如果你能明确的解释这个想象,那么这篇文章没必要看下去):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black" />
</LinearLayout>
很简单的布局文件,大家再脑海里尝试预览一下这个布局的效果。如果你认为是满屏黑色,那么这篇文章对你来说是有意义的。
因为这个布局预览出来是这样的:
黑色只有很短的一条,而这里本质的问题就在于TextView的onMeasure()方法。OK,今天的文章让咱们好好了解measure的过程…
正文
从现象上来看,很明显咱们这个TextView在measure()的过程之后,就被认定为只有这个高。因此咱们今天就借这个case,好好来研究一番View在测量的过程中到底会受哪些因素影响。
一、我们布局中众多的View是怎么串起来的
开始前,咱们先不着急进入测量这部分。先进行一波准备工作,这一部分,咱们先来回顾一下[这前文章的内容]():
- 1、我们setContentView的layout文件被LayoutInflate解析完,会以DecorView为parent,和DecorView关联起来。
- 2、在Activity可见的流程中,Window会调用addView()传入DecorView,这其中会new一个ViewRootImpl,将DecorView加到ViewRootImpl中,同样以parent的形式。
- 3、这样整个View便串起来了:ViewRootImpl -> DecorView -> FrmeLayout -> …
- 4、而后在ViewRootImpl的addView(DecorView)时,会执行requestLayout()方法,开启measure、layout、draw的流程。
debug调用链,如下:
此时requestLayout(),由于parent是null,所以无从执行。而真正意义上的requestLayout()是下边的调用链。
二、如何理解View的测量
首先根据官方文档我们能够明确:measure()的过程是自上而下的。
必须吐槽一下!这是Google开发者文档(国内的官网)…这翻译也是无语了。
有些小伙伴,可能并不了解View的测量过程,但是onMeasure()方法总还是多少有些涉猎吧?(不了解也没关系,这篇文章就是从一个小的demo,来聊一聊measure过程中的关键点)measure的流程可以简单用一个串行流程图表示:
OK,有了上述知识储备,我们就可以开启开篇那个效果的分析了:
requestLayout()方法会调用到View的measure()中,而measure()又会调用到自身的onMeasure()中。而measure()并不是一个可重写的方法,所以既然测量是自上而下,那咱们就从外围LinearLayout中的omMeasure()开始。
三、LinearLayout的onMeasure()
onMeasure()中比较简单,但是这里我们需要明确一下,这个方法的参数是什么含义:
- MeasureSpec就不用多说了,记录当前View的尺寸和测量模式
- 另外明确一点,这里的MeasureSpec是父View的
/**
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
这里咱们就选measureVertical()追进去,方法里的边界条件非常的多,但其中对于子View的测量过程比较的简单,遍历所有的子View,挨个调用measureChildBeforeLayout()方法,而这个方法最终会走到ViewGroup中的measureChildWithMargins():
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 这个方法主要就是做了一件事情:通过子View的LayoutParams和父View的MeasureSpec来决定子View的MeasureSpec
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的LayoutParams是什么时候设置的?
四、View的LayoutParams,是什么时机被设置的?
这里咱们就插空解决一下标题的问题:View的LayoutParams,是什么时机被设置的?
总结起来就是一句话:在LayoutInflate中解析xml中设置的。具体什么样?直接上代码:
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
// 省略部分代码
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
上述代码描述比较清晰,说白了就是解析xml中,基于这个View在布局中的layout_width、layout_height属性来生成对应的LayoutParams。然后在通过addView()方法将View和LayoutParams绑定到一起。
五、子View的measure()
书归正传,measureChildWithMargins()方法中,同于父View的MeasureSpec和子View的LayoutParams来共通决定子View的MeasureSpec,然后调用子View的measure()方法。
这里一共包含了两个重点:
- 生成子View的MeasureSpec
- 执行子View的measure()方法
5.1、生成子View的MeasureSpec
这部分逻辑主要在getChildMeasureSpec()方法中,我们直接追进去就好了:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 省略部分初始化代码
switch (specMode) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
这部分代码,就是Google定的规则,也没什么好说的。总结起来就是《Android开发艺术探索》中的那张图:
看了这个,咱们就可以思考一下咱们开篇遇到的问题:父View(LinearLayout)是wrap_content,子View(TextView)是match_parent,那么子View的MeasureSpec是什么样子?
有了上边的分析,我们很容易得出答案:parentSize + AT_MOST。因此咱们就知道这种场景下,子View的match_parent意味自己的宽高就是父View的宽高。那么此时父View的宽高是多少呢?
由于这里的父View已经是根View了,那么它的外边便是DecorView,而DecorView的MeasureSpec相对简单些,直接基于Window的宽高和自身的LayoutParams进行计算。
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
而DecorView的LayoutParams也很明确,看过setContentView代码的同学应该都比较清楚:
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
因此这种场景下,DecorView的MeasureSpec是屏幕宽高 + EXACTLY,那么父View(LinearLayout)的宽高就很明确了:parentSize + AT_MOST。
是不是发现问题了?
- 子View(TextView)的MeasureSpec是parentSize + AT_MOST
- 父View(LinearLayout)的MeasureSpec是parentSize + AT_MOST
- DecorView的MeasureSpec是屏幕的size + AT_MOST
有了上述的推导:子View的size就应该是屏幕的size!从debug出来的结果也是如此:
可是开篇的实际效果已经否定了这个答案,那么问题出在哪呢?
既然在获取子View的MeasureSpec流程中我们已经明确是:parentSize + AT_MOST。不过咱们别忘了,咱们现在仅仅是获取了子View的MeasureSpec,有了MeasureSpec还需要一个最关键的一步:执行子View的measure()方法。
5.2、执行子View的measure()方法
接下来咱们去看一看子View的measure()方法,上述的部分我们已经知道measureChildWithMargins()方法中会基于父View的MeasureSpec和子View的LayoutParams计算子View的MeasureSpec然后调用子View的measure():
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 省略获取子View的MeasureSpec的过程
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
通过断点的调用链我们可以看到,子View的measure()会调用到子View的onMeasure()中,然后通过setMeasureDimension()最终定下View的测量宽高。
到此咱们可以大概有一个猜想:导致子View(TextView)最终高度不是parentSize的原因,极可能是因为自身的onMeasure()方法!
走进onMeasure()方法中,我们会发现TextView的onMeasure()方法实现比较长,因此这里主要抽取关键逻辑:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 省略部分代码
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
mDesiredHeightAtMeasure = -1;
} else {
int desired = getDesiredHeight();
height = desired;
mDesiredHeightAtMeasure = desired;
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
}
// 省略部分代码
setMeasuredDimension(width, height);
}
我们的子View(TextView)已经确定是AT_MOST,那么直接看计算结果:
因此我们接下来调查的重点便是getDesiredHeight()方法:
private int getDesiredHeight() {
return Math.max(
getDesiredHeight(mLayout, true),
getDesiredHeight(mHintLayout, mEllipsize != null));
}
// 这其中又间接的调到了Layout中的这个方法
layout.getHeight()
// 而这个方法的实现,就是用一行的height * 所有文字的行数
public int getHeight() {
return getLineTop(getLineCount());
}
可以看到,这种场景下,TextView的高度是期望mLayout或mHintLayout中max的那个,而这个也是TextView特有的逻辑。
OK,看过上面代码注释的同学,到这里应该就恍然大悟了。开篇的那一条黑条就是一行文本的高度。而这个高度就是TextView默认Paint的高度。
这里就不再基于源码展开了,有兴趣的同学可以自己追进去看一下,下边贴几张图来佐证这个结论:
<TextView
android:layout_width="match_parent"
android:background="@color/black"
android:text="111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
android:textColor="@color/white"
android:layout_height="match_parent"/>
<TextView
android:layout_width="match_parent"
android:background="@color/black"
android:textSize="60sp"
android:layout_height="match_parent"/>
六、延伸问题1
这里咱们思考一个小问题:我们能不能做到不输入text,就让TextView占满全屏呢?答案是肯定,因为这篇文章主要就是在聊对这个问题的理解。
咱们已经明确这种case下,子View的MeasureSpec是parentSize + AT_MOST,改变最终measure()结果的是onMeasure(),那么我们直接重写onMeasure()…
class TestTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : TextView(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec))
}
}
六、延伸问题2
由上述分析,其实咱们明白这种case下:子View的MeasureSpec = parentSize + AT_MOST。由于TextView本身复写了measure()才出现了开篇的效果。那么如果我们用View来替换TextView是不是就能够撑满全屏了?答案是肯定的:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000" />
</LinearLayout>
不贴图了,就是一屏黑….
尾声
这篇文章涉及的知识面着实有些不少,最开始属实没有预料到这篇文章会牵扯这么多精力。毕竟想要把众多知识点压缩到一篇文章中还是有些难度,何况自己还是一个彩笔。
希望这篇文章能给各位同学带来帮助吧,也欢迎大家留言一起讨论或者是分享给自己身边的好友~
2024最新激活全家桶教程,稳定运行到2099年,请移步至置顶文章:https://sigusoft.com/99576.html
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。 文章由激活谷谷主-小谷整理,转载请注明出处:https://sigusoft.com/15133.html