前面简单分析了View的绘制流程,接下来会具体分析下绘制过程中ViewGroup以及其下的View是如何计算测量,布局这些数据的,以及View如何绘制到界面上。
下面以FrameLayout为例去进行具体了解。FrameLayout是继承自ViewGroup的。ViewGroup是继承自View的。
用到了设计模式中的模板方法模式,模板方法模式是一种行为型模式。适用于对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即一次性地实现一个算法的不变部分,并将可变的行为留给子类来实现。
设计模式的艺术软件开发人员内功修炼之道
ViewGroup的测量大小就是一种复杂的算法,需要考虑多种因素,例如子View的诉求或父容器的大小。套用模板方法的话结构如下:
1.png
public class View {
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
..............
onMeasure(widthMeasureSpec, heightMeasureSpec);
.....................
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
.............
}
}
public abstract class ViewGroup extends View {}
public class FrameLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
............
}
}
通过在FrameLayout这种ViewGroup中重写onMeasure等onXXX函数,从而定义不同的容器控件,例如LinearLayout、RelativeLayout 等,以完成不同的布局需求。
测量的本质就是根据约束条件去做计算,计算出控件的大小。
那么FrameLayout的onMeasure中是如何计算出了FrameLayout以及FrameLayout下的子View的大小,但是具体到底是如何计算的呢?下面具体来看一看。
以下代码均为由源码改的不完全代码。
// FrameLayout.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
............
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
............
}
widthMeasureSpec和heightMeasureSpec代表FrameLayout的父容器对FrameLayout的要求。所以下面的代码意思是
如果父容器LinearLayout对FrameLayout没有一个具体的大小要求,那么就要测量FrameLayout下的所有的长或高设置为MATCH_PARENT的子View。可以想象成下面的格式。
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="AB"/>
</FrameLayout>
</LinearLayout>
FrameLayout的父容器大小设置为wrap_content,那么对FrameLayout的大小要求就不是一个确定的值,那么就需要对FrameLayout下所有设置为match_parent的子View例如TextView的大小进行二次测量。至于为什么要对TextView二次测量,具体原因往下看就知道了。接着往下看,下面这一段是遍历测量添加到framelayout下的子view。
//FrameLayout
//这段是重点关注部分
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
为了具体看看这部分的代码可以先假定一个具体的xml文件。如下所示:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="20dp"
android:layout_height="20dp"
android:text="AB"/>
</FrameLayout>
</LinearLayout>
先来看这一句measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
首先要对widthMeasureSpec有一个明确的认知。
widthMeasureSpec是framelayout的父容器(即LinearLayout)对framelayout的要求, LinearLayout大小设置为100x100,但是framelayout的大小设置为wrap_content。
表示LinearLayout要求framelayout的size最多为 100,因此widthMeasureSpec中的mode为 AT_MOST,size为100,widthMeasureSpec为-2147483548(通过调用makeMeasureSpec(100,MeasureSpec.AT_MOST)计算而来),(widthMeasureSpec这个参数的计算这一部分实际是ViewRootImpl里面做的,实际是ViewRootImpl中的performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);,ViewRootImpl中的childWidthMeasureSpec就是这里的widthMeasureSpec。但是这里先将这一部分模糊化,减少代码量,因为ViewRootImpl那里也有很多需要分析的部分),但是此时framelayout大小未定,framelayout的大小可能是10x10,也有可能是100x100,仍需继续向下测量framelayout下的view的大小,这里是一个TextView。那么TextView大小在定义了具体值的情况下如何测量。framelayout对TextView又有什么要求(即childWidthMeasureSpec)?
下面来看看measureChildWithMargins具体做了什么。
//FrameLayout
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);
}
其实看这部分代码的时候,我一度迷失在widthMeasureSpec,
parentWidthMeasureSpec以及childWidthMeasureSpec这三个参数中,后来解决办法就是将这些参数具体化,给予它们具体是数值。
首先,MeasureSpec表示父容器对子View的要求,这里的表示LinearLayout对FrameLayout的要求。然后parent和child都是站在FrameLayout的角度去考虑的。
widthMeasureSpec的侧重点是width的MeasureSpec,即LinearLayout对FrameLayout的宽度要求,存储了mode和size。
parentWidthMeasureSpec表示LinearLayout对FrameLayout的宽度要求,存储了mode和size。
childWidthMeasureSpec表示FrameLayout对TextView的宽度要求,存储了mode和size。
mode和size的获取是通过getMode 和getSize。
Log.d(TAG, "getMode = " + MeasureSpec.getMode(mHeightMeasureSpec) + " ,getSize = " + MeasureSpec.getSize(mHeightMeasureSpec));
这里先把xml文件修改一下。
假设TextView的mPaddingLeft设置为10,没有设置margin参数,width和height参数设置为20。
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="20dp"
android:layout_height="20dp"
android:paddingLeft="10dp"
android:text="AB"/>
</FrameLayout>
</LinearLayout>
来看childWidthMeasureSpec 的具体计算。
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
getChildMeasureSpec是将TextView的Padding,Margin等参数传递到measureChildWithMargins函数中,计算framelayout这个容器对TextView的要求,命名为childWidthMeasureSpec。
前面我们计算出了widthMeasureSpec为-2147483548,因此getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width)实际调用的是
getChildMeasureSpec(-2147483548,10,20)
//FrameLayout
//getChildMeasureSpec(-2147483548,10,20)
//-2147483548表示Mode 为 AT_MOST,size为100
//specMode 为 AT_MOST ,childDimension >0 所以
//resultSize = 20
// resultMode = MeasureSpec.EXACTLY;
// MeasureSpec.makeMeasureSpec(20, MeasureSpec.EXACTLY) 结果为 1073741844
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 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;
......
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
也就是说 framelayout要求TextView的size值为20, mode为EXACTLY,所以childWidthMeasureSpec = 1073741844(这个数据是调用makeMeasureSpec自行计算得出)。
接下来就是child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
childWidthMeasureSpec 值为 1073741844,然后调用child.measure(1073741844, 1073741844);即调用TextView.measure(1073741844, 1073741844);
1073741844表示framelayout要求TextView大小为20, mode为EXACTLY。
measureChildWithMargins这一函数在此时就确定了没有MatchParent的情况下child的大小(即某些设定下的TextView 的大小)。
前面是TextView size设置为固定值20dp的情况。
如果TextView 大小属性设置为wrapcontent,并且TextView 没有设置LayoutParams情况下,childDimension默认是-1(因为LayoutParams.width默认是-1),此时int size = Math.max(0, specSize - padding); resultSize 则为100 - 10 = 90。
这些流程都可以通过在framelayout中添加log显示出来。自己测试一遍看log是最直观的理解方式。
那么就调用MeasureSpec.makeMeasureSpec(90, MeasureSpec.AT_MOST),然后TextView 的测量会比较复杂。例如如果字符多一点就长一点少点就短点。
最后width在经过一系列复杂的计算后,跟90比取小。
//TextView.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...........
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
.............
setMeasuredDimension(width, height);
}
如果TextView 大小属性设置为MatchParent,那么就添加进mMatchParentChildren中。后面还要用到。
接着往下看。调用了getPaddingLeftWithForeground。
这里要明白Foreground以及padding的概念。
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
下面以xml文件为例说明。
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical"
tools:context="com.android.test.myapplication.MainActivity">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<TextView
android:layout_width="20dp"
android:layout_height="20dp"
android:text="AB"/>
</FrameLayout>
</LinearLayout>
image.png
FrameLayout的padding区域就是FrameLayout以内,TextView之外的区域。
我把FrameLayout的padding区涂成红色,如下图所示。这里我只设置了左右padding,没有设置上下padding。
image.png
下面来看Foreground。
出处:https://helpx.adobe.com/cn/photoshop/key-concepts/background.html
背景(在美术和摄影中)图像的元素可能包含前景 (A) 和背景 (B)。背景是图像中离查看者最远的部分。
image.png
在上图中B(可能是地毯或者瓷砖?)是图像中离查看者最远的部分,是背景,最上面的即离查看者最近的是A(猫),是前景。A和B中间还有垫子之类的东西,这种就是中景。
前景是相对于背景而言的。
使用画图工具,绘制一张大小为50x50像素的图片,填充色为粉色。这张作为前景图。
test.png
然后填充另一个颜色,将另存为background.png,这张作为背景图。
background.png
下面来简单对比设置background,foreground时View的显示。
1.设置background
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:background="@drawable/background">
<TextView
android:layout_width="20dp"
android:layout_height="20dp"
android:text="AB"/>
</FrameLayout>
</LinearLayout>
可以看到background在TextView下面。
image.png
2.设置foreground
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:foreground="@drawable/test">
<TextView
android:layout_width="20dp"
android:layout_height="20dp"
android:text="AB"/>
</FrameLayout>
</LinearLayout>
此时看不到中景TextView,因为foreground把TextView挡住了。
image.png
3.同时设置foreground和background。
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:foreground="@drawable/test"
android:background="@drawable/background">
<TextView
android:layout_width="20dp"
android:layout_height="20dp"
android:text="AB"/>
</FrameLayout>
</LinearLayout>
Framelayout设置了背景和前景,TextView是中景。
但是由于前景图片大小和背景大小一致,所以只能看到前景图,并且中景TextView被挡住。
image.png
再来看
//FrameLayout.java
int getPaddingRightWithForeground() {
return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) :
mPaddingRight + mForegroundPaddingRight;
}
由于前景图大小超出了android:paddingRight设置的区域,因此调用mPaddingRight + mForegroundPaddingRight ,计算padding,算最大值。
image.png
再往下就是根据背景图和前景图大小,再次计算framelayout这个ViewGroup的最大值。
//FrameLayout.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
....................
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
....................
}
最后通过调用setMeasuredDimension将测量所得的framelayout的大小进行存储。
要捋清楚这个3个函数:measure,onMeasure,setMeasuredDimension的作用。
framelayout的大小与TextView有一定的关系,所以是测量完TextView等子View的尺寸后,再加上padding等数据后所得的数据才是maxWidth。
最后存储的framelayout的测量数据,是比较了maxWidth,widthMeasureSpec和childState这3个数据后得出的数据。
因此,如果TextView设置为matchparent,由于前景图片导致framelayout大小发生变化,那么此时childWidthMeasureSpec计算方法就会发生改变,要重新传递参数。
以framelayout为例的ViewGroup的onMeasure的测量大小的计算过程基本就是这个样子了,在ViewGroup大小为给定具体数值的情况下,ViewGroup首先遍历测量子View的大小,然后再计算出自身的大小,再重新确定部分特殊的子View的大小。
现实生活中的绘制,首先你需要一张纸,一支笔,以及打算画的对象(例如一棵树,以及树上的叶子)。这里FrameLayout和TextView的关系就类似于树与叶子的关系。纸可以看做一个ViewRootImpl对象(ViewRootImpl的理解部分可以简单看看这篇ViewRootImpl源码分析事件分发)。
纸的大小是确定的,但是树和叶子要画多大呢?树和叶子的大小这个是需要由人来决定的。但是无论如何,树和叶子的大小都不可能超出纸的大小。
编程的本质是和计算机沟通,将人的想法告诉计算机。
为了确定树和叶子具体大小,首先你需要把这个你需要对树进行约束(例如要求不能超过纸的大小的二分之一又或者是不能超过整张纸的大小),告诉它们绝对不能超过这个大小,并且你需要定义树和叶子的大小,例如具体数值20px,或者match,又或者说wrap,并且,在定义了这些约束条件的情况下,才能测量树的大小。
总结可以看这一段话。
出处:[Android 自定义 View] —— 深入总结 onMeasure、 onLayout
从个体看
对于每一个 View:
1.运行前,开发者会根据自己的需求在 xml 文件中写下对于 View 大小的期望值
2.在运行的时候,父 View 会在 onMeaure()中,根据开发者在 xml 中写的对子 View 的要求, 和自身的实际可用空间,得出对于子 View 的具体尺寸要求
3.子 View 在自己的 onMeasure中,根据 xml 中指定的期望值和自身特点(指 View 的定义者在onMeasrue中的声明)算出自己的*期望
如果是 ViewGroup 还会在 onMeasure 中,调用每个子 View 的 measure () 进行测量.
1.父 View 在子 View 计算出期望尺寸后,得出⼦ View 的实际尺⼨和位置
2.⼦ View 在自己的 layout() ⽅法中将父 View 传进来的实际尺寸和位置保存
如果是 ViewGroup,还会在 onLayout() ⾥调用每个字 View 的 layout() 把它们的尺寸 置传给它们
下一篇来看看layout。其实测量与布局不能孤立地去看,要联系在一起。前面的设置前景图片的时候,影响了framelayout的最大值的计算,但是前景图的位置其实也是一个因素,如果前景图改变了位置,framelayout大小又会发生改变,只是这里暂时忽略了布局这个变量。
参考链接:
Android自定义控件系列七:详解onMeasure()方法中如何测量一个控件尺寸(一)
Android View 全解析(二) -- OnMeasure
Android -容器- FrameLayout
Android学习笔记---深入理解View#03
参考书籍:
《深入理解Android内核设计思想》
《深入理解ANDROID 卷3》

image.png









网友评论