引言
Android实际项目开发中,自定义View不可或缺,而作为自定义View的一种重要实现方式——继承View重绘尤其重要,前面很多文章基本总结了继承View的基本流程:自定义属性和继承View重写onDraw方法——>实现构造方法并完成相关初始化操作——>重写onMeasure方法——>onSizeChanged()拿到view的宽高等数据——>重写onLayout————>重写onTouch实现交互——>定义交互回调接口,但是由于当时的具体的业务需求并没有详解总结下关于onMeasure和onLayout方法,相信很多初学者都是处于知其然而不知其所以然,这篇文章就专门总结下。
一、View的系统架构
虽然前面已经总结过了,但是这在里还是重申下,加深印象。总所周知,在Android中每一个控件都会再界面中占据一块矩形的区域,这和大多数系统的控件机制都差不多。Android中控件是通过构造树的形式来管理的(所谓控件树如下图所示),主要分为View和ViewGroup两大类,其中ViewGroup直接继承自View,View作为系统所有可视组件的基类,而通过控件树,上层控件负责下层子控件的测量与绘制(即先执行onMeasure——>onLayout——>onDraw的),并负责分发交互事件的即事件是先传递到ViewGroup的,再由ViewGroup决定是否传递给下层子View。而这颗树的根节点ViewParent(其实质是一个接口定义了一系列管理View的方法)对于该控件树所有的交互事件惊喜统一管理和分发,从而实现对整个树进行整体控制。
二、View、ViewGroup的测量和绘制概述
Android中的GUI系统是客户端和服务端配合的窗口系统,即后台运行了一个绘制服务,每个应用程序都是该服务端的一个客户端,当客户端需要绘制时,首先请求服务端创建一个窗口,然后在窗口中进行具体的视图内容绘制;对于每个客户端而言,他们都感觉自己独占了屏幕,而对于服务端而言,它会给每一个客户端窗口分配不同的层值,并根据用户的交互情况动态改变窗口的层值,这就给用户造成了所谓的前台窗口和后台窗口的概念。当然这是屏幕绘制的原理简要描述,绘制离不开测量,无论是系统控件和自定义的View要想展示于界面之上都离不开测量工作。简而言之,当Activity获得焦点时,Activity将被通知要求绘制自己的布局,从而Android framework接到Activity的消息将会处理绘制过程,而Activity只需提供它的布局的根节点即可。绘制过程是从布局的根节点开始,从根节点开始测量和绘制整个View tree。每一个父级ViewGroup 负责要求它的每一个孩子被绘制,每一个子View负责绘制自己。因为整个View tree是按顺序遍历的,所以父节点会先被绘制,而兄弟节点会按照它们在树中出现的顺序被绘制。完整的绘制是包含两个过程:测量Measure 和布局Layout。测量过程(measuring pass)是在measure(int, int)中实现的,是从树的顶端由上到下进行的(top-down)。在这个递归过程中,每一个View会把自己的dimension specifications传递下去。在测量Measure 完成之后,每一个View都存储好了自己的测量结果。再者就是是布局Layout,它发生在 layout(int, int, int, int)中,仍然是从上到下进行,每一个父级都会负责用测量过程中得到的尺寸,把自己的所有孩子放在正确的地方。
1、View的测量
由父级ViewGroup负责要求子级View进行测量和绘制。我们都知道每一个控件都会占据一个矩形区域,但是Android系统在绘制前本身并不知道具体的大小和位置,所以它会先进行测量,主要是在View的onMeasure里去实现(这也是我们在自定义View里的构造方法里,无论是调用getMeasureWidth抑或getWidth获取宽度时得到的总是0的原因),而Android中还有一个功能类MeasureSpec(封装了从父节点传递到子节点下的布局信息包括View的测量模式和大小)用于辅助测量View,当我们重写了onMeasure方法之后,系统通过super.onMeasure方法去调用setMeasuredDimension(width, height)将测量的大小设置进去完成测量。
2、View的绘制
完成测量工作之后,View的根据ViewGroup传人的测量值和模式,对自己宽高进行确定(onMeasure中完成),然后在onDraw中在Canvas上完成对自己的绘制。
3、ViewGroup的测量
ViewGroup需要管理子View,所有其中一项重要的职责就是负责子View的大小,当ViewGroup大小设置为wrap_content,ViewGroup会对子View进行层级遍历,来决定自己的大小,而其他模式下则会取设置的值来为自己的大小。ViewGroup在测量时遍历所有子View,从调用子View对应的onMeasure方法获得子View的大小,完成测量之后再通过调用onLayout方法来决定子View的位置,同样是通过遍历调用子View的onLayout方法,最后在自己的onLayout中完成子View的位置布局工作。
4、ViewGroup的绘制
ViewGroup通常不需要绘制,因为它本身没有需要绘制的东西,所以不会触发自己的onDraw方法,但如果指定了background属性则会触发自身的onDraw完成背景的绘制。但ViewGroup会通过dispatchDraw方法来绘制其子View,原理也是一样通过遍历子View调用其子View对应的onDraw方法来完成最终的绘制工作。
三、View.MeasureSpec和ViewGroup.LayoutParams
1、View.MeasureSpec
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* UNSPECIFIED 模式:
* 父View不对子View有任何限制,子View需要多大就多大
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* EXACTYLY 模式:
* 父View已经测量出子Viwe所需要的精确大小,这时候View的最终大小
* 就是SpecSize所指定的值。对应于match_parent和精确数值这两种模式
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* AT_MOST 模式:
* 子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值,
* 即对应wrap_content这种模式
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
//将size和mode打包成一个32位的int型数值
//高2位表示SpecMode,测量模式,低30位表示SpecSize,某种测量模式下的规格大小
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//将32位的MeasureSpec解包,返回SpecMode,测量模式
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//将32位的MeasureSpec解包,返回SpecSize,某种测量模式下的规格大小
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
//...
}
View.MeasureSpec是View中的一个静态内部类,封装了从父级节点传递下来给子级节点的布局需求信息,每一个MeasureSpec体现的是子类的布局的尺寸大小size(包括宽度或高度)和模式mode的需求,但是并不是子级的实际尺寸就必须是父级要求的,我们可以通过重写onMeasure方法实现自己的规则,然后在子级中,而这里的模式来源于父ViewGroup去解析子View对应的在布局文件中layout_width和layout_height值来决定采用什么模式(至于怎么解析,这是后话),其中主要有三种模式:UNSPECIFIED、EXACTLY、AT_MOST:
UNSPECIFIED:说明父级没有对子级强加任何限制,子级可以是它想要的任何尺寸。用得比较少,表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST,换言之,表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中
EXACTLY:父级为子级决定了一个确切的尺寸,子级将会被强制赋予这些边界限制,不管子级自己想要多大(View类onMeasure方法中只支持EXACTLY),换言之,表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY,即在布局文件中可以解析指定的具体尺寸和match_parent,不支持wrap_content。
AT_MOST:子级可以是自己指定的任意大小,但是有个上限。比如说当MeasureSpec.EXACTLY的父容器为子级决定了一个大小,子级大小只能在这个父容器限制的范围之内。即在布局文件中可以解析wrap_content,换言之,表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST。
方法 | 说明 |
---|---|
static int getMode(int measureSpec) | 获取模式 |
static int getSize(int measureSpec) | 获取尺寸 |
static int makeMeasureSpec(int size, int mode) | 根据指定的模式和尺寸创建对应的测量规则 |
2、ViewGroup.LayoutParams
ViewGroup.LayoutParams直接继承于Object作为位置参数信息的父类,是View用来告诉它的父容器它想要怎样被放置的(包含高度、宽度、对齐方式、外边距、内边距等等),Android中的布局信息ViewGroup.LayoutParams家族来决定的,常见包括AbsListView.LayoutParams, AbsoluteLayout.LayoutParams, Gallery.LayoutParams, ViewGroup.MarginLayoutParams, ViewPager.LayoutParams, WindowManager.LayoutParams、ActionBar.LayoutParams, ActionMenuView.LayoutParams, AppBarLayout.LayoutParams, BaseCardView.LayoutParams, BoxInsetLayout.LayoutParams,CollapsingToolbarLayout.LayoutParams,CoordinatorLayout.LayoutParams,DrawerLayout.LayoutParams,FrameLayout.LayoutParams,GridLayout.LayoutParams, GridLayoutManager.LayoutParams, LinearLayout.LayoutParams, LinearLayoutCompat.LayoutParams,PercentFrameLayout.LayoutParams、RelativeLayout.LayoutParams等。不同的Layout提供了不同LayoutParams,它们共同承担起整个Android 的布局任务。
四、View中几大重要的方法的意义和作用
继承View/ViewGroup实现自定义View后,一般还需要复写最基本的二、三个方法:onMeasure(),onSizeChanged()拿到view的宽高等数据、onLayout()、onDraw()。
onMeasure:用于本View宽高的测量,布局复杂时可能触发多次。ViewGroup的onMeasure则负责处理它children的测量工作。由于View默认的onMeasure()仅仅支持EXACTLY模式,也就是说如果不重写onMeasure()方法的话则无法在正确解析布局文件里的wrap_content,因为onMeasure()是Android提供给我们告诉系统自己定义的View的实际大小(是否是仅仅依赖于父级要求的,也就是说自主定义View大小的)的机会,同时也是提供了我们自定义的解析规则的方法(如果你愿意,你可以完全实现match_parent和wrap_content和具体值一样的效果),最终调用setMeasuredDimension(int ,int)完成最终的测量(因为onMeasure方法没有返回值,所以测量的结果应该通过setMeasuredDimension方法告知系统)。
onSizeChanged():可拿到view的宽高等数据信息
onLayout:常复写于viewGroup的自定义子类。它有负责对它内部所有children进行处理,告知childrenView的位置,以正确摆放。ViewGroup中onLayout是抽象方法必须复写,这是children位置能正确摆放的保证。依靠mLeft,mTop,mRight,mBottom这四个值,以坐上为原点,这四个值分别为对应边到原点的距离。最后和onMeasure一样,记得调用child.layout()方法。
onDraw:UI最终呈现的过程,用户使用Paint(What to draw)、Canvas(How to
draw)两个类完成自定义画面。绘制的时候需要考虑下padding,与margin不同,padding是属于本View的属性,不同于margin(不需要自定义时做处理系统就能很好的使用margin),所以要在测量绘图时考虑它:测量时:desireSize=实际所需size+相应方向的padding。
绘图时:考虑padding,做相应的位移。
五、onMeasure方法详解及实现
onMeasure方法是测量View及其内容的,决定measured width和measured height的,这个方法由 measure(int, int)方法唤起,子类可以重写onMeasure来提供更加准确和有效的测量。(以前有一个约定:在重写onMeasure方法的时候,必须调用 setMeasuredDimension(int,int)来存储这个View经过测量得到的measured width and height。否则,将会由measure(int, int)方法抛出一个IllegalStateException。)View类onMeasure方法中只支持EXACTLY,如果不重写onMeasure的话就只支持EXACTLY模式。
1、onMeasure方法签名
/**
*这两个参数都是按赵View.MeasureSpec类来进行编码的
*@param :widthMeasureSpec 父级提出的水平宽度要求
*@param :heightMeasureSpec 父级提出的垂直高度要求
*/
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
2、实现onMeasure方法的步骤
从父级传递过来的View.MeasureSpec对象里获取测量模式和尺寸
然后根据不同的模式,给出不同的测量值,(即实际值)宽高都采用一样的机制,一般mode为EXACTLY时,直接使用父类传递过来的测量值specValue;mode为UNSPECIFIED时,直接指定为默认的大小(这个值需要我们自己定义);当mode为AT_MOST时也指定为默认的大小,但还需要我们拿指定的默认大小和测量值specValue比较取最小值。
调用父类测量方法setMeasuredDimension(测量宽度值,测量高度值)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measure(widthMeasureSpec);
measure(heightMeasureSpec);
Log.e("onMeasure", "realWidth: " + realWidth + "realHeiht: " + realHeiht + "widthMeasureSpec" + widthMeasureSpec + "heightMeasureSpec" + heightMeasureSpec);
setMeasuredDimension(realWidth, realHeiht);
}
private void measure(int measureValue) {
int defalueSize = 200;
int mode = View.MeasureSpec.getMode(measureValue);
int specValue = View.MeasureSpec.getSize(measureValue);
Log.e("onMeasure", "mode: " + mode + "specValue: " + specValue);
switch (mode) {
//指定一个默认值
case MeasureSpec.UNSPECIFIED:
realWidth = defalueSize;
realHeiht = defalueSize;
break;
//取测量值
case MeasureSpec.EXACTLY:
realHeiht = specValue;
realWidth = specValue;
break;
//取测量值和默认值中的最小值
case MeasureSpec.AT_MOST:
realWidth = Math.min(defalueSize, specValue);
realHeiht = Math.min(defalueSize, specValue);
break;
default:
break;
}
}
3、模仿谷歌官方写法实现onMeasure
这里主要就是模仿View.resolveSizeAndState(int size, int measureSpec, int childMeasuredState),childMeasuredState其中 View.getMeasuredState()是由返回的,最终布局将结合childMeasuredState通过View.combineMeasuredStates()完成最终的测量结果,作用应该是自定义viewGroup时才使用用于记录children测量状态的,一般自定义View传0即可,特殊情况下可以传递1。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//third param. usually 0. http://stackoverflow.com/questions/13650903/whats-the-utility-of-the-third-argument-of-view-resolvesizeandstate
int w = resolveSizeAndState2(getDesireW(), widthMeasureSpec, 0);
int h = resolveSizeAndState2(300, heightMeasureSpec, 0);
setMeasuredDimension(MeasureSpec.getSize(w), MeasureSpec.getSize(h));
}
private int getDesireW(){
return 300;
}
/**
*
* @param size How big the view wants to be.即传入你希望View的大小
* @param measureSpec Constraints imposed by the parent. 父级约束大小
* @param childMeasuredState 一般传递0即可,特殊情况还可以传入1
* @return
*/
private int resolveSizeAndState2(int size, int measureSpec, int childMeasuredState) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
//当specMode为AT_MOST,并且父控件指定的尺寸specSize小于View自己想要的尺寸时,
//我们就会用掩码MEASURED_STATE_TOO_SMALL向量算结果加入尺寸太小的标记
//这样其父ViewGroup就可以通过该标记其给子View的尺寸太小了,
//然后可能分配更大一点的尺寸给子View
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;//按味或
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result | (childMeasuredState&MEASURED_STATE_MASK);
}
五、一个简单的例子
/**
* Auther: Crazy.Mo
* DateTime: 2017/5/3 15:52
* Summary:
*/
public class MeasuredView extends View {
private Context context;
private int realWidth, realHeiht;
public MeasuredView(Context context) {
this(context, null);
}
public MeasuredView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MeasuredView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measure(widthMeasureSpec);
measure(heightMeasureSpec);
Log.e("onMeasure", "realWidth: " + realWidth + "realHeiht: " + realHeiht + "widthMeasureSpec" + widthMeasureSpec + "heightMeasureSpec" + heightMeasureSpec);
setMeasuredDimension(realWidth, realHeiht);
}
private void measure(int measureValue) {
int defalueSize = 200;
int mode = View.MeasureSpec.getMode(measureValue);
int specValue = View.MeasureSpec.getSize(measureValue);
Log.e("onMeasure", "mode: " + mode + "specValue: " + specValue);
switch (mode) {
//指定一个默认值
case MeasureSpec.UNSPECIFIED:
Log.e("onMeasure", "mode: " + mode + "UNSPECIFIED " );
realWidth = defalueSize;
realHeiht = defalueSize;
break;
//取测量值
case MeasureSpec.EXACTLY:
Log.e("onMeasure", "mode: " + mode + "EXACTLY " );
realHeiht = specValue;
realWidth = specValue;
break;
//取测量值和默认值中的最小值
case MeasureSpec.AT_MOST:
Log.e("onMeasure", "mode: " + mode + "AT_MOST " );
realWidth = Math.min(defalueSize, specValue);
realHeiht = Math.min(defalueSize, specValue);
break;
default:
break;
}
}
}
此时在布局中使用的话,
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:orientation="vertical"
android:background="#0f8">
<!-- <com.ce.sesamecredit.ClockView
android:layout_width="match_parent"
android:layout_height="match_parent" />-->
<com.ce.sesamecredit.MeasuredView
android:background="@color/colorAccent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
2、运行结果分析:
-
未重写onMeasure方法时,默认的onMeasure仅可以解析match_parent和指定的具体数值
-
重写onMeasure方法时,可以解析match_parent、指定的具体数值和wrap_content