Android(安卓)View的测量、布局、绘制过程详解(上)
Android View的绘制过程主要有三步:
- 测量 Measure
- 布局 Layout
- 绘制 Draw
首先理解MeasureSpec的含义,然后跟踪ViewGroup的measure、layout、draw三个方法即可
view的绘制流程是我们在自定义View中通常会使用到的一个知识点,也是一个面试常问的点。简直是Android开发必备知识。
1、理解ViewRootImpl和DecorView两个类
DecorView我们相对比较熟悉,因为开发中就会不时的用到,它是整个Activity的顶层View,我们设置的布局文件都是它的子view。
而ViewRootImpl是连接WindowManager和DecorView的纽带,View的绘制三大流程都是通过ViewRootImpl来完成的。Activity创建后,会把DecorView添加到WindowManager中,同时会创建ViewRootImpl对象,使DecorView和ViewRootImpl建立联系,代码如下:
//代码地址:frameworks/base/core/java/android/view/WindowManagerGlobal.java addView方法root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);mParams.add(wparams);try { root.setView(view, wparams, panelParentView);} catch (RuntimeException e) {...}
View的绘制也是从ViewRootImpl的performTraversals方法开始的,然后调用view的measure、layout、draw方法,流程图如下:
2、理解MeasureSpec
在真正调用measure方法进行测量前,必须哟啊计算出MeasureSpec,然后才能用MeasureSpec去测出View的大小,那什么是MeasureSpec?
Android系统通过MeasureSpec来测量View的宽高,并且把测量模式和测量数据都放在MeasureSpec中,它由一个32位int值组成,其中高2位代表测量模式,也就是SepcMode,低30位代表测量值,也就是SpecSize,表示View宽或高的具体大小。MeasureSpec是View.java的一个内部类, MeasureSpec以及View的源码路径: /frameworks/base/core/java/android/view/View.java
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; public @interface MeasureSpecMode {} 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(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); }}
MeasureSpec主要就提供了三个方法,上述的makeMeasureSpec方法相当于把mode和size组装在一起,形成一个MeasureSpec。
低30位表示宽或高的大小,那实际可以表示的大小为2^30 -1 ,是一个非常大的值了,完全够用。
高两位代表测量模式,按照00, 01, 10, 11四种组合来看,可以形成四种测量模式,实际上有三种测量模式:
- UNSPECFIED : 此种模式表示父容器不对View有任何限制,要多大给多大,一般用于系统内部,仅表示一种测量状态;
- EXACTLY : 表示父容器已经检测出view所需要的精确大小,此时View的最终大小就是SpecSize的值,它对应于LayoutParams中的match_parent或者设置的某个具体数值;
- AT_MOST: 父容器制定了一个可用的最大数值,但是不知道子View会使用多少,而子View的大小不能超过父容器给的值,但具体也要看View的具体实现,这种模式对应于wrap_content;
3、MeasureSpec的生成过程
3.1 顶级View(即DecorView)的MeasureSpec生成过程
第2节提到, Android系统通过MeasureSpec来进行View的测量,并保存View的宽或高数据。正常情况下,我们在xml布局里面设置宽高或match_parent等, 或者在代码里通过LayoutParams来设置数据, 然后在系统测量View时,系统会将LayoutParams在父容器的约束下转换为对应的MeasureSpec,然后根据这个MeasureSpec测量子view的宽高。注意,是由LayoutParams和父View一起决定View的MeasureSpec。
但是,有一个特殊情况,那就是最顶层的DecorView, 它没有父view,它的MeasureSpec是由手机屏幕的尺寸和自身的LayoutParams来共同决定的。DecorView的MeasureSpec在ViewRootImpl中的measureHierarchy方法中创建:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
其中,desiredWindowWidth和desiredWindowHeight分别表示屏幕的宽高。 测量DecorView的宽高都调用了getRootMeasureSpec:
private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec;}
顶级View的测量属性中,测量大小就是屏幕大小,测量模式就是EXACTLY。
3.2普通View的MeasureSpec生成过程
普通View的测量,View的测量是通过ViewGroup传递过来的,因为每个view肯定都是存在于一个ViewGroup中, 先看ViewGroup中的measureChildWithMargins方法:
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);}
以上代码主要分三步:
- 1、通过父view宽的MeasureSpec, 然后加上横向的padding和margin,传入childView的宽, 然后算出childView的宽的测量模式MeasureSpec;
- 2、通过父view高的MeasureSpec, 然后加上纵向的padding和margin,传入childView的高, 然后算出childView的高的测量模式MeasureSpec;
- 3、通过前面计算的宽高的MeasureSpec,去调用childView的measure方法进行测量,得到真实宽高并记录的childView的MeasureSpec中;
进入getChildMeasureSpec方法查看一下是如何得到childView的测量模式的:
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) {//注释1 通过父view的测量模式,做不同操作 // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) {//注释2 如果childView的宽高是一个具体的值, 如100dp resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY;//注释3 如果父view是EXACTLY,子view是match_parent, 则子view也是EXACTLY } 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; // 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; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}
上述代码逻辑很简单,就是通过父view和子view的LayoutParams, 共同确定子view的测量模式MeasureSpec,总结如下:
-
1、当View为固定宽高时,测量模式是EXACTLY模式,测量值就是布局参数中的大小。
-
2、当View为WRAP_CONTENT时,测量模式是AT_MOST模式,测量值是父容器的剩余空间大小。
-
3、当View为MATCH_PARENT时,测量值是父容器的剩余空间大小,测量模式分两种情况,如果父容器是EXACTLY模式,那就是EXACTLY模式,如果父容器是AT_MOST模式,那么View也是AT_MOST模式。
如第3.2小节开头的代码所展现的,当获取了子view的宽和高的MeasureSpec,就可以真正开始对view进行测量了:
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
今天先写到这里,具体的measure、layout、draw过程的分析,请见下一篇博客Android View的测量、布局、绘制过程详解(下)
更多相关文章
- 2020.9.8 oppo Java开发(Android)一面面经
- Android(安卓)内功心法(1.5)——android常用设计模式之命令模式
- android 之view的测量和绘制(群英传读书笔记1)
- Android(安卓)Mvp模式详解(Kotlin篇)
- Android日记之2012/02/11——浅谈Iterator设计模式
- Android屏幕的大小、密度以及字符缩放比例——DisplayMetrics类
- Android检测手机中存储卡及剩余空间大小的方法(基于Environment,
- Android(安卓)- Toast字体修改
- Android(安卓)Activity启动机制流程和四种启动模式