前面简单分析了View
的绘制流程,接下来会具体分析下绘制过程中ViewGroup
以及其下的View
是如何计算测量,布局这些数据的,以及View
如何绘制到界面上。
下面以FrameLayout
为例去进行具体了解。FrameLayout
是继承自ViewGroup
的。ViewGroup
是继承自View
的。
用到了设计模式中的模板方法模式,模板方法模式是一种行为型模式。适用于对一些复杂的算法进行分割,将其算法中固定不变的部分设计为模板方法和父类具体方法,而一些可以改变的细节由其子类来实现。即一次性地实现一个算法的不变部分,并将可变的行为留给子类来实现。
设计模式的艺术软件开发人员内功修炼之道
ViewGroup
的测量大小就是一种复杂的算法,需要考虑多种因素,例如子View的诉求或父容器的大小。套用模板方法的话结构如下:
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。
下面来看Foreground
。
出处:https://helpx.adobe.com/cn/photoshop/key-concepts/background.html
image.png
背景(在美术和摄影中)图像的元素可能包含前景 (A) 和背景 (B)。背景是图像中离查看者最远的部分。
在上图中B(可能是地毯或者瓷砖?)是图像中离查看者最远的部分,是背景
,最上面的即离查看者最近的是A(猫),是前景
。A和B中间还有垫子之类的东西,这种就是中景
。
前景是相对于背景而言的。
使用画图工具,绘制一张大小为50x50
像素的图片,填充色为粉色。这张作为前景图。
然后填充另一个颜色,将另存为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,算最大值。
再往下就是根据背景图和前景图大小,再次计算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》
网友评论