Android 应用层开发中绕不开自定义 View 这个话题,虽然现在 Github 上有形形色色的开源库供大家使用,
但是作为一名开发者而言,虽然不提倡重复造轮子,但是轮子都是造出来的。碰到一些新鲜的 UI 效果时,
如果现有的控件无法完成任务,那么我们就应该想到要自定义一个 View 了。
我们知道,在 Android 中 View 绘制流程有测量、布局、绘制三个步骤,它们分别对应 3 个 API :onMeasure()、onLayout()、onDraw()。
- 测量 onMeasure() :测量View的尺寸,决定View的大小
- 布局 onLayout() :通过设置l,t,r,b确定view在父容器中的位置
- 绘制 onDraw():通过canvas绘制我们想要展示的内容
没有办法说这三个阶段,哪个阶段最重要,只是相对而言,测量阶段对于开发者而言难度相对其它两个要大,处理的细节也要多得多,
自定义一个 View,正确的测量是第一步,正因为如此今天我将从源码的角度来学习View的测量过程.
View在本质上是一个Rect矩形的区域.
在Android中View的测量是从View树的根节点开始的,一步一步的往下测量而成的.
那么,首先我们来了解下View树的结构:
我们在Activity中一般通过setContentView()方法来设置我们的View视图:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
在setContentView()方法中,我们调用了getWindow().setContentView()方法,而这里的
getWindow()就是PhoneWindow,因此我们转到PhoneWindow的setContentView()方法:
public void setContentView(int layoutResID) {
//如果顶层容器FrameLayout为空的话,需要从xml加载顶层容器
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//将我们通过setContentView设置的布局资源加载到顶层容器mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
在PhoneWindow的setContentView()中会调用installDecor()方法(部分代码省略):
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//生成DecorView
mDecor = generateDecor(-1);
} else {
//将DecorView关联到PhoneWindow上
mDecor.setWindow(this);
}
if (mContentParent == null) {
//同时生成Activity的setContentView方法需要加载的View的顶层容器,并添加到DecorView中去
mContentParent = generateLayout(mDecor);
}
}
分别调用generateDecor()方法生成DecorView:
protected DecorView generateDecor(int featureId) {
//生成顶层的DecorView
return new DecorView(context, featureId, this, getAttributes());
}
同时我们会看到在installDecor()方法中,也会通过generateLayout方法生成mContentParent对象:
protected ViewGroup generateLayout(DecorView decor) {
int layoutResource;
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
//在该方法中最终将mContentParent顶层view添加到DecorView中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//mContentParent顶层容器View的布局id为com.android.internal.R.id.content
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
return contentParent;
}
以上的流程中最终会将资源id为:R.layout.screen_simple的布局加载DecorView中:
可以看出我们的顶层布局DecorView中包含的是一个线性布局,包含两部分
ViewStub用于控制是否显示StatusBar或者TitleBar相关的显示
而id为content的FrameLayout则最终会加载我们的Activity通过setContentView()设置的布局资源
由此我们可以大致的画出Activity中view树的结构了:
在ActivityTherad中创建Activity,在Activity中创建PhoneWindow而PhoneWindow中包含了DecorView
因此Activity PhoneWindow DecorView这三者相互关联实现Android view树的整体流程:
可以看到,每一个Activity都包含了PhoneWindow,PhoneWindow又包含了一个DecorView,
而DecorView中包含了TitleView和一个ContentView,TitleView就是我们开发过程中需要设置
的ActionBar和StatusBar相关,而界面的主体结构都在这个ContentView中设置了,所以我们
每次给Activity设置布局文件的时候必须调用setContentVIew(int layoutResID)方法来设置。
而ContentView中就包含了一个或多个ViewGroup和View,关于ViewGroup和View,
整个ContentView的视图结构:最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup和View,
就好像布局文件来说,我们写布局文件的时候,最外层一般都是一个ViewGroup(LinearLayout或者是ConstraintLayout等),
之后这个ViewGroup内部可能还是一个ViewGroup(LinearLayout或者是RelativeLayout等)或者直接放置了一个或者
多个View(如TextView或者是Button等)。
了解了Activity的view树的组成结构,接下来我们就要了解View的measure测量流程了:
在View的测量流程中,主要对应的就是onMeasure()方法,在该方法中有两个参数,分别是
widthMeasureSpec heightMeasureSpec因此我们学习View的测量measure过程中首先就要学习,MeasureSpec这个类:
public static class MeasureSpec {
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
MeasureSpec是View的一个静态内部类代表着测量的规格(包含mode和size),该类主要用于封装父View对于子View的布局要求:
而它的手段主要是通过一个32位的int类型的数值来实现的,我们知道一个int类型的数值有32位组成,MeasureSpec将它的高2位,
用来代表测量模式mode,低30位用来代表在该测量模式下具体的数值大小size,如下图所示:
在MeasureSpec中分别有三个重要的变量和方法:
MeasureSpec.EXACTLY:
该模式表示,父容器已经检测出子view所需要的精确大小,在该模式下,子View的测量大小即为SpecSize
MeasureSpec.AT_MOST:
该模式表示,父容器未能检测出子view所需要的精确大小,但是指定了一个可用的大小即SpecSize,
在该模式下View的测量大小不能超过SpecSize
MeasureSpec.UNSPECIFIED:
该模式下父容器不对子view的大小做任何的限制,子view想要多大就多大
MeasureSpec.UNSPECIFIED这种模式一般用于我们的系统的内部,像我们的AdapterView,
ScrollView,ListView等,因此在以下的讨论中该模式不在我们的讨论范围内
而MeasureSpec的三个方法,则是获取和生成模式mode和size大小相关的:
getMode(measureSpec):根据测量规格获取测量的模式
getSize(measureSpec):根据测量规格获取测量的具体大小值
makeMeasureSpce(int mode,int size):根据指定的模式和大小生成一个指定的测量规格
在Android中由于View的树形结构,在测量时会从view树的顶端从上向下的依次进行遍历,完成对子view的测量
因此子View的测量是由父View发起的,并且子View的父view必须是一个容器,因为只有容器才有能力装载子
View因此我们可以判定,父view肯定是一个ViewGroup,由此我们可以从ViewGroup的源码来入手对view的测量流程
来一探究竟,由于ViewGroup继承自View并且ViewGroup并没有重写measure()和onMeasure()这两个方法,那么到底是怎么测量的呢?
我们可以从ViewGroup的子类中寻找答案,我们以我们不经常使用的AbsoluteLayout为例来进行讲解:
首先,我们应该知道绝对布局的布局特点是有以下的两个属性来决定的:
AbsoluteLayout_Layout_layout_x:控件相对于父容器左上角的left坐标 AbsoluteLayout_Layout_layout_y:控件相对于父容器左上角的top坐标
AbsoluteLayout中的子View通过设置自身的left top相对于父容器左上角的坐标位置来最终确定自身在容器中的位置
我们来看AbsoluteLayout的onMeasure()方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取容器中的子view的数量
int count = getChildCount();
int maxHeight = 0; //定义容器的高度
int maxWidth = 0; //定义容器的宽度
//首先通过measureChildren()方法来测量容器中的所有子view的宽高
measureChildren(widthMeasureSpec, heightMeasureSpec);
//接着遍历所有的子view,由于子view已经通过measureChildren()方法测量过一遍了
//即子view已经有确定的测量大小了
for (int i = 0; i < count; i++) {
//依次获取指定的子view
View child = getChildAt(i);
//如果子view为GONE的话就跳过
if (child.getVisibility() != GONE) {
int childRight;
int childBottom;
//获取每个子View的LayoutParams
AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) child.getLayoutParams();
//通过设置的绝对坐标x,y 分别获取view的右边 和底部的位置
childRight = lp.x + child.getMeasuredWidth();
childBottom = lp.y + child.getMeasuredHeight();
//获取容器的宽和高,容器的宽和高就是所有子view中(子view的x坐标 + 子view的宽度,子view的y坐标 + 子view的高度)的最大值
maxWidth = Math.max(maxWidth, childRight);
maxHeight = Math.max(maxHeight, childBottom);
}
}
//分别在水平和垂直方向上将padding考虑进去
maxWidth += mPaddingLeft + mPaddingRight;
maxHeight += mPaddingTop + mPaddingBottom;
//考虑layout_minWidth 和 layout_minHeight的值和我们计算出容器的宽高值,取最大值
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
//通过setMeasuredDimension将我们的值设置给容器自身,才算完成了一次完整的测量
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
}
通过以上对AbsoluteLayout的测量流程分析我们可以得出:
ViewGroup即容器的测量流程是先测量容器中的所有的子View,然后通过
测量好的子View的大小来计算自身的大小,最后通过setMeasureDimension()
将计算好的容器的大小值设置给自身才算完成了一次完整的测量
由于ViewGroup是一个容器,因此其不仅要测量自己还要测量其包含的子view
因此在ViewGroup中提供了几个测量子view的方法,用于子view的测量:
在以上的三个方法中,measureChildren()是一次测量完所有的子view而measureChild()和measureChildWithMargins()则是只单独对某一个
子View进行测量,我们就挑选一个最复杂的measureChildWithMargins()来进行分析:
//注意方法的参数:
//child:父容器要进行测量的子View
//parentWidthMeasureSpec:父容器在width宽度方向上的测量规格MeasureSpce
//widthUsed:父容器在width方向上已经使用的宽度值 比如 其它子view已经使用的宽度 + 其它子view的left_marging + 其它子view的right_margin + 父容器的padding_left + 父容器的padding_right
//parentHeightMeasureSpec:父容器在height高度方向上的测量规格
//heightUsed:父容器在height高度方向上已经使用的高度空间 比如 其它子view已经使用的高度 + 其它子view的top_marging + 其它子view的bottom_margin + 父容器的padding_top + 父容器的padding_bottom
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取要测量子View的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//通过getChildMeasureSpec()方法获取要测量子view在width水平方向的宽度的测量规格childWidthMeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
//通过getChildMeasureSpec()方法获取要测量子view在height垂直方向上的高度的测量规格childHeightMeasureSpec
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//通过以上计算出要测量的子View在水平和垂直方向上的测量规格并调用要测量子view的measure()方法来对子view 进行测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
在以上调用measureChildWithMargins()方法测量子view的过程中主要有以下步骤:
1.获取要测量子view的LayoutParams
2.通过getChildMeasureSpec()方法传入父容器的测量宽高尺寸规格widthMeasureSpec / heightMeasureSpec 和子view的LayoutParams的width / height
生成要测量子view在width 和 height方向上的测量规格MeasureSpec
3.调用要测量子view的measure()方法并将计算出的子view的宽高测量规格传递给要测量子view的measure()方法
那么,子view的宽高测量规格到底具体是怎么生成的呢,我们需要对getChildMeasureSpec()方法一探究竟:
//注意方法参数:
//spec:要测量的子view的父容器在width 或者 height方向上的测量规格 即父容器的MeasureSpec
//padding:父容器在水平或者垂直方向上的已经占用的空间,比如mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed
//表示父容器的左右或者上下padding值 以及该控件的左右或者上下margin值,以及其它已经测量过的子view的宽高值,这些已经使用的空间不能纳入到要测量子view的MeasureSpec的计算中
//childDimension:通过子View的LayoutParams获取到的width height (即通过xml layout_width / layout_height 或者 代码设置的layoutparams.width layout.height设置的值)
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec); //获取父容器的测量模式 mode
int specSize = MeasureSpec.getSize(spec); //获取父容器的测量大小 size
//获取父容器的剩余可用空间大小
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
//通过switch语句判断父容器的mode ,来生成要测量的子view的mode 和 size
switch (specMode) {
//父容器的specMode为EXACTLY时子view的mode 和 size的生成情况 即父容器的layout_width = 具体值 比如 122dp 或者 layout_width = match_parent
case MeasureSpec.EXACTLY:
//表示子的layout_width 为一个具体的值 比如:100dp 50dp 等
if (childDimension >= 0) {
//子view的size就是 childDimension 即子view设置的值
resultSize = childDimension;
//子view的mode为 EXACTLY
resultMode = MeasureSpec.EXACTLY;
//子View的layout_width为 match_parent
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view的size为 父容器在width 或者height方向上的可用的空间大小
resultSize = size;
//子view的mode为,EXACTLY
resultMode = MeasureSpec.EXACTLY;
//子view的layout_width 为 wrap_content
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view的size为 父容器在width 或者height 方向上的可用的空间大小
resultSize = size;
//子view的mode 为 AT_MOST
resultMode = MeasureSpec.AT_MOST;
}
break;
//父容器的mode为AT_MOST的情况 即父容器的layout_width = wrap_content
case MeasureSpec.AT_MOST:
//子view的layout_width 为 具体值 比如 50dp 100dp ..等的情况
if (childDimension >= 0) {
//子view的size为子view自己设置的layout_width值
resultSize = childDimension;
//子view的mode为EXACTLY
resultMode = MeasureSpec.EXACTLY;
//子view的layout_width为match_parent的情况
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view的size为 父容器在width 或者 height 方向上可用的剩余空间 并且子view的size 不能超过父容器在width 或者 height 方向上可用的剩余空间
resultSize = size;
//子view的mode为AT_MOST
resultMode = MeasureSpec.AT_MOST;
//子view的layout_width = wrap_content
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view的size为 父容器在width 或者 height 方向上可用的剩余空间 并且子view的size 不能超过父容器在width 或者 height 方向上可用的剩余空间
resultSize = size;
//子view的mode为AT_MOST
resultMode = MeasureSpec.AT_MOST;
}
break;
//父容器的mode为UNSPECIFIED
case MeasureSpec.UNSPECIFIED:
//子view的layout_width 为具体值 如layout_width = 100dp
if (childDimension >= 0) {
//子view的size为子view自己设置的值
resultSize = childDimension;
//子view的mode为EXACTLY
resultMode = MeasureSpec.EXACTLY;
//子view的layout_width 为match_parent
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子View的size 为 父容器的可用剩余空间或者为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
//子view的mode 为 UNSPECIFIED
resultMode = MeasureSpec.UNSPECIFIED;
//子view的layout_width = wrap_content
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view的size为父容器的可用剩余空间或者为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
//子view的mode为 UPSPECIFIED
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//通过MeasureSpec将测量计算出的size 和 mode组合成一个完整的MeasureSpec 返回
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
通过以上的getChildMeasureSpec()计算出一个要测量的子view的宽度或者高度的MeasureSpec有以下几个步骤:
1.获取父容器测量规格的mode 和 size
2.获取父容器在width 或者 height方向上的可用空间
3.通过switch语句判断父容器的MeasureSpec的mode来决定要测量子view的mode和size
由此我们知道子view的MeasuerSpec的生成是由其父容器的MeasureSpec和子view自身的LayoutParams来决定的
而至于这个决定的规则,我总结出了一个一下的表格:
从以上总结的这张表中我们总结出了以下的这几个结论(从左往右看,同时UNSPECIFIED模式不在考虑范围内):
1.如果子View的width 或者height为具体值的时候比如 50dp那么此时不管父容器的specMode是什么子view的specMode一定是EXACTLY并且子view的width 或者height的specSize就是我们设置的具体值
2.当子View的size为MATCH_PARENT的时候,子view的测量模式specMode跟随父容器的specMoe,即父容器的specMode是什么子view的测量模式specMode和其相同,但是子view的测量大小分为以下两种情况:
a.父容器的specMode为EXACTLY时,子view的size为父容器在width 或者height方向上可用的剩余空间
b.父容器的specMode为AT_MOST时,子View的size不能超过父容器在width 或者height方向上可用剩余空间的大小
3.当子view的size为WRAP_CONTENT时,子View的测量模式specMode一定为AT_MOST,并且子View的测量大小size一定不能超过父容器在width或者height方向上的可用剩余空间大小.