一、初识 ViewRoot 和 DecorView
Activity中有一个成员为Window,其实例化对象为PhoneWindow,PhoneWindow为抽象Window类的实现类。
这里先简要说明下这些类的职责:
1.Window是一个抽象类,提供了绘制窗口的一组通用API。
2.PhoneWindow是Window的具体继承实现类。而且该类内部包含了一个DecorView对象,该DectorView对象是所有应用窗口(Activity界面)的根View。
3.DecorView是PhoneWindow的内部类,是FrameLayout的子类,是对FrameLayout进行功能的修饰(所以叫DecorXXX),是所有应用窗口的根View 。
依据面向对象从抽象到具体我们可以类比上面关系就像如下:
Window是一块电子屏,PhoneWindow是一块手机电子屏,DecorView就是电子屏要显示的内容,Activity就是手机电子屏安装位置
先简单介绍一下当我们在Activity方法 onCreate里执行 setContentView之后View是如何显示到屏幕上的,这里我们就不分析源码过程了,因为这个过程不是我们要分析的重点,只是辅助我们去理解,有助于我们对整个流程有更好的理解和把握。
当调用 Activity 的setContentView 方法后会调用PhoneWindow 类的setContentView方法,PhoneWindow类是抽象类Window的实现类,Window类用来描述Activity视图最顶端的窗口显示和行为操作,PhoneWindow类的setContentView方法中最终会生成一个 DecorView对象,DecorView是PhoneWindow类的内部类,继承自FrameLayout,所以调用 Activity方法 setContetnView后最终会生成一个FrameLayout类型的 DecorView组件,该组件将作为整个应用窗口的顶层图,然后在 DecorView容器中添加根布局,根布局中包含一个 id为 contnet的FrameLayout内容布局,我们的 Activity加载的布局 xml最后通过LayoutInflater将 xml内容布局解析成 View树形结构,最后添加到 id为content的FrameLayout布局当中,至此,View最终就会显示到手机屏幕上
用一个实例来进行说明,如下是一个简单的App主界面布局文件:
在AndroidManifest.xml文件,我们将主题设置为NoTitleBar
Android:theme="@android:style/Theme.Black.NoTitleBar"
我们将主题设置为NoTitleBar,此时系统使用这个screen_simple.xml布局文件,如下:
看下下面这个hierarchyviewer图谱
我们了解了上面得到流程后下面梳理一下如何进入到 view的绘制流程:
ViewRoot对应的实现类是 ViewRootImpl类,他是连接 WindowManager和DecorView的纽带,view的三大流程均是通过 ViewRoot来完成的。在 ActivityThread中,当 activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建 ViewRootImpl对象,并将 ViewRootImpl对象和DecorView建立关联。这个流程可以参考下图
View 的绘制流程是从 ViewRoot的 performTraversals方法开始的,它经过 measure、layout、draw三个过程才能最终将一个 View绘制出来,其中 measure用来测量 View的宽和高,layout用来确定 View在父容器的放置位置,而 draw则负责将 View绘制在屏幕上,参考下图
performTraversals会依次调用 performMeasure、performLayout、performDraw三个方法,这三个方法分别完成顶级 View 的 measure、layout和 draw这三大流程,其中 performMeasure会调用 measure方法,在measure方法中又会调用 onMeasure方法,在 onMeasure方法中对所有的子元素进行measure过程,这个时候 measure流程就会从父容器传递到子元素中了,这样就完成了一次 measure过程。接着子元素就会重复父容器的 measure过程,如此反复就完成了整个 View树的遍历,同理 perFormLayout和 performDraw的流程也是类似。
这里先给出Android系统View的绘制流程:依次执行View类里面的如下三个方法:
- measure(int ,int) :测量View的大小
- layout(int ,int ,int ,int) :设置子View的位置
- draw(Canvas) :绘制View内容到Canvas画布上
measure过程决定了 view的宽高,在几乎所有的情况下这个宽高都等同于 view最终的宽高,但特殊情况除外。layout过程决定了 view 的四个顶点的坐标和view实际的宽高,通过 getWidth
和 getHeight
方法可以得到最终的宽高。draw过程决定了view的显示。
三、measure
1.理解MeasureSpec
MeasureSpec 是 View 测量过程中的一个关键参数,很大程度上决定了 View 的宽高,父容器会影响 View 的 MeasureSpec 的创建,MeasureSpec 不是唯一由 LayoutParams 决定的,LayoutParams 需要和父容器一起才能决定 View 的MeasureSpec,从而进一步确定 View 的宽高,在 View 测量过程中,系统会将该 View 的 LayoutParams 参数在父容器的约束下转换成对应的 MeasureSpec ,然后再根据这个 measureSpec 来测量 View 的宽高。
MeasureSpec 代表一个32位 int 值,高2位代表 SpecMode(测量模式),低30位代表 SpecSize(在某个测量模式下的规格大小),MeasureSpec 通过将 SpecMode 和 SpecSize 打包成一个 int 值来避免过多的内存分配,为了方便操作,其提供了打包和解包方法源码如下:
- <span style="font-size:14px;">//通过将 SpecMode 和 SpecSize 打包,获取 MeasureSpec
- public static int makeMeasureSpec(int size, int mode) {
- if (sUseBrokenMakeMeasureSpec) {
- return size + mode;
- } else {
- return (size & ~MODE_MASK) | (mode & MODE_MASK);
- }
- }
- //将 MeasureSpec 解包获取 SpecMode
- public static int getMode(int measureSpec) {
- return (measureSpec & MODE_MASK);
- }
- //将 MeasureSpec 解包获取 SpecSize
- public static int getSize(int measureSpec) {
- return (measureSpec & ~MODE_MASK);
- }</span>
SpecMode 有三类,每一类都表示特殊的含义:
- UNSPECIFIED 父容器不对 View 有任何的限制,要多大给多大,这种情况下一般用于系统内部,表示一种测量的状态。
- EXACTLY 父容器已经检测出 View 所需要的精确大小,这个时候 View 的最终大小就是 SpecSize 所指定的值,它对应于LayoutParams 中的 match_parent 和具体的数值这两种模式
- AT_MOST 父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体是什么值要看不同 View 的具体实现。它对应于 LayoutParams 中的 wrap_content。
2.MeasureSpec 和 LayoutParams 的对应关系
对于DecorView,它的 MeasureSpec 由窗口的尺寸和其自身的 LayoutParams 来决定;对于普通 View,它的MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定。
对普通的 View 的 measure 方法的调用,是由其父容器传递而来的,这里先看一下 ViewGroup 的 measureChildWithMargins 方法:
- /<span style="font-size:14px;">* @param child 要被测量的 View
- * @param parentWidthMeasureSpec 父容器的 WidthMeasureSpec
- * @param widthUsed 父容器水平方向已经被占用的空间,比如被父容器的其他子 view 所占用的空间
- * @param parentHeightMeasureSpec 父容器的 HeightMeasureSpec
- * @param heightUsed 父容器竖直已经被占用的空间,比如被父容器的其他子 view 所占用的空间
- */
- protected void measureChildWithMargins(View child,
- int parentWidthMeasureSpec, int widthUsed,
- int parentHeightMeasureSpec, int heightUsed) {
- //第一步,获取子 View 的 LayoutParams
- final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
- //第二步,获取子 view 的 WidthMeasureSpec,其中传入的几个参数说明:
- //parentWidthMeasureSpec 父容器的 WidthMeasureSpec
- //mPaddingLeft + mPaddingRight view 本身的 Padding 值,即内边距值
- //lp.leftMargin + lp.rightMargin view 本身的 Margin 值,即外边距值
- //widthUsed 父容器已经被占用空间值
- // lp.width view 本身期望的宽度 with 值
- final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
- mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
- + widthUsed, lp.width);
- //获取子 view 的 HeightMeasureSpec
- final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
- mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
- + heightUsed, lp.height);
- // 第三步,根据获取的子 veiw 的 WidthMeasureSpec 和 HeightMeasureSpec
- //对子 view 进行测量
- child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- }</span>
从上代码第二步可以看出,子 view 的 MeasureSpec 的创建与父容器的 MeasureSpec 、子 view 本身的 LayoutParams 有关,此外还与 view 本身的 margin 和 padding 值有关,具体看一下 getChildMeasureSpec 方法:
- <span style="font-size:14px;">/*
- * @param spec 父容器的 MeasureSpec,是对子 View 的约束条件
- * @param padding 当前 View 的 padding、margins 和父容器已经被占用空间值
- * @param childDimension View 期望大小值,即layout文件里设置的大小:可以是MATCH_PARENT,
- *WRAP_CONTENT或者具体大小, 代码中分别对三种做不同的处理
- * @return 返回 View 的 MeasureSpec 值
- */
- public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
- // 获取父容器的 specMode,父容器的测量模式影响子 View 的测量模式
- int specMode = MeasureSpec.getMode(spec);
- // 获取父容器的 specSize 尺寸,这个尺寸是父容器用来约束子 View 大小的
- int specSize = MeasureSpec.getSize(spec);
- // 父容器尺寸减掉已经被用掉的尺寸
- int size = Math.max(0, specSize - padding);
- int resultSize = 0;
- int resultMode = 0;
- switch (specMode) {
- // 如果父容器是 EXACTLY 精准测量模式
- case MeasureSpec.EXACTLY:
- //如果子 View 期望尺寸为大于 0 的固定值,对应着 xml 文件中给定了 View 的具体尺寸大小
- //如 android:layout_width="100dp"
- if (childDimension >= 0) {
- //那么子 View 尺寸为期望值固定尺寸,测量模式为精准测量模式 EXACTLY
- resultSize = childDimension;
- resultMode = MeasureSpec.EXACTLY;
- //如果子 View 期望尺寸为 MATCH_PARENT 填充父布局
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
- // 那么子 View 尺寸为 size 最大值,即父容器剩余空间尺寸,为精准测量模式 EXACTLY
- //即子 View 填的是 Match_parent, 那么父 View 就给子 View 自己的size(去掉padding),
- //即剩余全部未占用的尺寸, 然后告诉子 View 这是 Exactly 精准的大小,你就按照这个大小来设定自己的尺寸
- resultSize = size;
- resultMode = MeasureSpec.EXACTLY;
- //如果子 View 期望尺寸为 WRAP_CONTENT ,包裹内容
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
- //子 View 尺寸为 size 最大值,即父容器剩余空间尺寸 ,测量模式为 AT_MOST 最大测量模式
- //即子 View 填的是 wrap_Content,那么父 View 就告诉子 View 自己的size(去掉padding),
- //即剩余全部未占用的尺寸,然后告诉子 View, 你最大的尺寸就这么多,不能超过这个值,
- //具体大小,你自己根据自身情况决定最终大小。一般当我们继承 View 基类进行自定义 View 的时候
- //需要在这种情况下计算给定 View 一个尺寸,否则当使用自定义的 View 的时候,使用
- // android:layout_width="wrap_content" 属性就会失效
- resultSize = size;
- resultMode = MeasureSpec.AT_MOST;
- }
- break;
- // 父容器为 AT_MOST 最大测量模式
- case MeasureSpec.AT_MOST:
- // 子 View 期望尺寸为一个大于 0的具体值,对应着 xml 文件中给定了 View 的具体尺寸大小
- //如 android:layout_width="100dp"
- if (childDimension >= 0) {
- //那么子 View 尺寸为期望固定值尺寸,为精准测量模式 EXACTLY
- resultSize = childDimension;
- resultMode = MeasureSpec.EXACTLY;
- //如果子 View 期望尺寸为 MATCH_PARENT 最大测量模式
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
- //子 View 尺寸为 size,测量模式为 AT_MOST 最大测量模式
- //即如果子 View 是 Match_parent,那么父 View 就会告诉子 View,
- //你的尺寸最大为 size 这么大(父容器尺寸减掉已经被用掉的尺寸,即父容器剩余未占用尺寸),
- //你最多有父 View的 size 这么大,不能超过这个尺寸,至于具体多大,你自己根据自身情况决定。
- resultSize = size;
- resultMode = MeasureSpec.AT_MOST;
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
- //同上
- resultSize = size;
- resultMode = MeasureSpec.AT_MOST;
- }
- break;
- // 父容器为 UNSPECIFIED 模式
- case MeasureSpec.UNSPECIFIED:
- // 子 View 期望尺寸为一个大于 0的具体值
- if (childDimension >= 0) {
- //那么子 View 尺寸为期望值固定尺寸,为精准测量模式 EXACTLY
- resultSize = childDimension;
- resultMode = MeasureSpec.EXACTLY;
- //如果子 View 期望尺寸为 MATCH_PARENT 最大测量模式
- } else if (childDimension == LayoutParams.MATCH_PARENT) {
- //子 View 尺寸为 0,测量模式为 UNSPECIFIED
- // 父容器不对 View 有任何的限制,要多大给多大
- resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
- resultMode = MeasureSpec.UNSPECIFIED;
- //如果子 View 期望尺寸为 WRAP_CONTENT ,包裹内容
- } else if (childDimension == LayoutParams.WRAP_CONTENT) {
- //子 View 尺寸为 0,测量模式为 UNSPECIFIED
- // 父容器不对 View 有任何的限制,要多大给多大
- resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
- resultMode = MeasureSpec.UNSPECIFIED;
- }
- break;
- }
- return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
- }</span>
以上代码主要作用就是根据父容器的 MeasureSpec 和 view 本身的 LayoutParams 来确定子元素的 MeasureSpec 的整个过程,这个过程清楚的展示了普通 view 的 MeasureSpec 的创建规则,整理一下可得到如下表格(来源艺术探索截图):
总结:
- 当 View 采用固定宽高时,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式,并且大小是LayoutParams 中的大小。
- 当 View 的宽高是 match_parent 时,如果父容器的模式是精确模式,那么 View 也是精确模式,并且大小是父容器的剩余空间;如果父容器是最大模式,那么 View 也是最大模式,并且大小是不会超过父容器的剩余空间。
- 当 View 的宽高是 wrap_content 时,不管父容器的模式是精确模式还是最大模式,View 的模式总是最大模式,并且大小不超过父容器的剩余空间。
3. View 的 measure 过程
View 的 measure 过程由 measure
方法来完成, measure
方法是一个 final 类型,子类不可以重写,而 View 的 measure() 方法中会调用 onMeasure 方法,因此我们只需要分析 onMeasure 方法即可,源码如下:
- <span style="font-size:14px;">/**
- * @param widthMeasureSpec 父容器所施加的水平方向约束条件
- * @param heightMeasureSpec 父容器所施加的竖直方向约束条件
- */
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- //设置 view 高宽的测量值
- setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
- getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
- }</span>
上面方法很简单,就是给 View 设置了测量高宽的测量值,而这个测量值是通过 getDefaultSize 方法获取,那么接着分析 getDefaultSize 方法:
- <span style="font-size:14px;">/**
- * @param size view 的默认尺寸,一般表示设置了android:minHeight属性
- *或者该View背景图片的大小值
- * @param measureSpec 父容器的约束条件 measureSpec
- * @return 返回 view 的测量尺寸
- */
- public static int getDefaultSize(int size, int measureSpec) {
- int result = size;
- //获取测量模式
- int specMode = MeasureSpec.getMode(measureSpec);
- //获取尺寸
- int specSize = MeasureSpec.getSize(measureSpec);
- switch (specMode) {
- case MeasureSpec.UNSPECIFIED:
- //如果 测量模式为 UNSPECIFIED ,表示对父容器对子 view 没有限制,那么 view 的测量尺寸为
- //默认尺寸 size
- result = size;
- break;
- case MeasureSpec.AT_MOST:
- case MeasureSpec.EXACTLY:
- //如果测量模式为 AT_MOST 最大测量模式或者 EXACTLY 精准测量模式,
- //那么 View 的测量尺寸为 MeasureSpec 的 specSize
- //即父容器给定尺寸(父容器当前剩余全部空间大小)。
- result = specSize;
- break;
- }
- return result;
- }</span>
上面方法很简单,就是给 View 设置了测量高宽的测量值,而这个测量值是通过 getDefaultSize 方法获取,那么接着分析 getDefaultSize 方法:
- <span style="font-size:14px;">/**
- * @param size view 的默认尺寸,一般表示设置了android:minHeight属性
- *或者该View背景图片的大小值
- * @param measureSpec 父容器的约束条件 measureSpec
- * @return 返回 view 的测量尺寸
- */
- public static int getDefaultSize(int size, int measureSpec) {
- int result = size;
- //获取测量模式
- int specMode = MeasureSpec.getMode(measureSpec);
- //获取尺寸
- int specSize = MeasureSpec.getSize(measureSpec);
- switch (specMode) {
- case MeasureSpec.UNSPECIFIED:
- //如果 测量模式为 UNSPECIFIED ,表示对父容器对子 view 没有限制,那么 view 的测量尺寸为
- //默认尺寸 size
- result = size;
- break;
- case MeasureSpec.AT_MOST:
- case MeasureSpec.EXACTLY:
- //如果测量模式为 AT_MOST 最大测量模式或者 EXACTLY 精准测量模式,
- //那么 View 的测量尺寸为 MeasureSpec 的 specSize
- //即父容器给定尺寸(父容器当前剩余全部空间大小)。
- result = specSize;
- break;
- }
- return result;
- }</span>
这里来分析一下 UNSPECIFIED 条件下 View 的测量高宽默认值 size 是通过 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 函数获取,这两个方法原理一样,这里我们就看一下 getSuggestedMinimumHeight() 源码:
- <span style="font-size:14px;">protected int getSuggestedMinimumHeight() {
- return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
- }</span>
上面代码可以看出,如果 View 没有背景,View 的高度就是 mMinHeight,这个 mMinHeight 是由 android:minHeight 这个属性控制,可以为 0,如果有背景,就返回 mMinHeight 和背景的最小高度两者中的最大值。从 getDefaultSize 方法可以看出,View 的高/宽由 父容器传递进来的 specSize 决定,因此可以得出结论:直接继承自 View 的自定义控件需要重写 onMeasure 方法来设置 wrap_content 时候的自身大小,而设置的具体值需要根据实际情况自己去计算或者直接给定一个默认固定值,否则在布局中使用 wrap_content 时候就相当于使用 match_parent ,因为在布局中使用 wrap_content 的时候,它的 specMode 是 AT_MOST 最大测量模式,在这种模式下 View 的宽/高等于 speceSize 大小,即父容器中可使用的大小,也就是父容器当前剩余全部空间大小,这种情况,很显然,View 的宽/高就是等于父容器剩余空间的大小,填充父布局,这种效果和布局中使用 match_parent 一样,解决这个问题代码如下:
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
- int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
- int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
- int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
- // 在 MeasureSpec.AT_MOST 模式下,给定一个默认值
- //其他情况下沿用系统测量规则即可
- if (widthSpecMode == MeasureSpec.AT_MOST
- && heightSpecMode == MeasureSpec.AT_MOST) {
- setMeasuredDimension(mWith, mHeight);
- } else if (widthSpecMode == MeasureSpec.AT_MOST) {
- setMeasuredDimension(mWith, heightSpecSize);
- } else if (heightSpecMode == MeasureSpec.AT_MOST) {
- setMeasuredDimension(widthSpecSize, mHeight);
- }
- }
4. ViewGroup 的 measure 过程
上面代码中在 widthSpecMode 或 heightSpecMode 为 MeasureSpec.AT_MOST 我们就给定一个对应的 mWith 和 mHeight 默认固定值宽高,而这个默认值没有固定依据,需要我们根据自定义的 view 的具体情况去计算给定。
ViewGroup 除了完成自己的测量过程还会遍历调用所有子 View 的measure
方法,而且各个子 View 还会递归执行这个过程,我们知道 View Group 继承自 View ,是一个抽象类,因此没有重写 View onMeasure 方法,也就是没有提供具体如何测量自己的方法,但是它提供了一个 measureChildren 方法,定义了如何测量子 View 的规则,代码如下:
- /**
- * @param widthMeasureSpec 该 ViewGroup 水平方向约束条件
- * @param heightMeasureSpec 该 ViewGroup 竖直方向约束条件
- */
- protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
- final int size = mChildrenCount;
- final View[] children = mChildren;
- for (int i = 0; i < size; ++i) {
- //逐一遍历获取得到 ViewGroup 中的子 View
- final View child = children[i];
- if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
- //对获取到的 子 view 进行测量
- measureChild(child, widthMeasureSpec, heightMeasureSpec);
- }
- }
- }
- /**
- * @param child 要进行测量的子 view
- * @param parentWidthMeasureSpec ViewGroup 对要进行测量的子 view 水平方向约束条件
- * @param parentHeightMeasureSpec ViewGroup 对要进行测量的子 view 竖直方向约束条件
- */
- protected void measureChild(View child, int parentWidthMeasureSpec,
- int parentHeightMeasureSpec) {
- //第一步,获取 View 的 LayoutParams
- final LayoutParams lp = child.getLayoutParams();
- //第二步,获取 view 的 WidthMeasureSpec,其中传入的几个参数说明:
- //parentWidthMeasureSpec 父容器的 WidthMeasureSpec
- //mPaddingLeft + mPaddingRight view 本身的 Padding 值,即内边距值
- // lp.width view 本身期望的宽度 with 值
- final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
- mPaddingLeft + mPaddingRight, lp.width);
- //同上
- final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
- mPaddingTop + mPaddingBottom, lp.height);
- // 第三步,根据获取的子 veiw 的 WidthMeasureSpec 和 HeightMeasureSpec
- //调用子 view 的 measure 方法,对子 view 进行测量,具体后面的测量逻辑就是和我们前面分析
- // view 的测量过程一样了。
- child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- }
我们再看一下对子 View 进行测量的 measureChild 方法 :
上面代码中的第二步调用的方法 getChildMeasureSpec 在标题 4 MeasureSpec和LayoutParams的对应关系 中已经分析过。ViewGroup 并没有定义具体的测量过程,这是因为 ViewGroup 是一个抽象类,其不同子类具有不同的特性,导致他们的测量过程有所不同,不能有一个统一的 onMeasure 方法,所以其测量过程的 onMeasure 方法需要子类去具体实现,比如 LinearLayout 和 RelativeLayout 等,下面通过 LinearLayout 的 onMeasure 方法来分析一下 ViewGroup 的测量过程。
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- if (mOrientation == VERTICAL) {
- //垂直方向的 LinearLayout 测量方式
- measureVertical(widthMeasureSpec, heightMeasureSpec);
- } else {
- //水平方向的 LinearLayout 测量方式
- measureHorizontal(widthMeasureSpec, heightMeasureSpec);
- }
- }
- void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
- ......................
- //记录总高度
- float totalWeight = 0;
- final int count = getVirtualChildCount();
- //获取测量模式
- final int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
- final int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
- ...........
- //第1步,对 LinearLayout 中的子 view 进行第一次测量
- // See how tall everyone is. Also remember max width.
- for (int i = 0; i < count; ++i) {
- final View child = getVirtualChildAt(i);
- if (child == null) {
- mTotalLength += measureNullChild(i);
- continue;
- }
- if (child.getVisibility() == View.GONE) {
- i += getChildrenSkipCount(child, i);
- continue;
- }
- if (hasDividerBeforeChildAt(i)) {
- mTotalLength += mDividerHeight;
- }
- //获取子 view 的 LayoutParams 参数
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
- totalWeight += lp.weight;
- //第1.1步,满足该条件,第一次测量时不需要测量该子 view
- if (heightMode == View.MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
- // 满足该条件的话,不需要现在计算该子视图的高度。
- //因为 LinearLayout 的高度测量规格为 EXACTLY ,说明高度 LinearLayout 是固定的,
- //不依赖子视图的高度计算自己的高度
- //lp.height == 0 && lp.weight > 0 说明子 view 使用了权重模式,即希望使用 LinearLayout 的剩余空间
- // 测量工作会在之后进行
- //相反,如果测量规格为 AT_MOST 或者 UNSPECIFIED ,LinearLayout
- // 只能根据子视图的高度来确定自己的高度,就必须对所有的子视图进行测量。
- final int totalLength = mTotalLength;
- mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
- //标记未进行测量
- skippedMeasure = true;
- } else {
- // else 语句内部是对子 view 进行第一次测量
- int oldHeight = Integer.MIN_VALUE;
- if (lp.height == 0 && lp.weight > 0) {
- // 如果 LiniearLayout 不是 EXACTLY 模式,高度没给定,
- //说明 LiniearLayout 高度需要根据子视图来测量,
- // 而此时子 view 模式为 lp.height == 0 && lp.weight > 0 ,是希望使用 LinearLayout 的剩余空间
- // 这种情况下,无法得出子 view 高度,而为了测量子视图的高度,
- //设置子视图 LayoutParams.height 为 wrap_content。
- oldHeight = 0;
- lp.height = LayoutParams.WRAP_CONTENT;
- }
- //该方法只是调用了 ViewGroup 的 measureChildWithMargins() 对子 view 进行测量
- // measureChildWithMargins() 方法在上面 4 MeasureSpec和LayoutParams的对应关系已经分析过
- measureChildBeforeLayout(
- child, i, widthMeasureSpec, 0, heightMeasureSpec,
- totalWeight == 0 ? mTotalLength : 0);
- if (oldHeight != Integer.MIN_VALUE) {
- lp.height = oldHeight;
- }
- // 获取测量到的子 view 高度
- final int childHeight = child.getMeasuredHeight();
- final int totalLength = mTotalLength;
- //第2步, 重新计算 LinearLayout 的 mTotalLength 总高度
- mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
- lp.bottomMargin + getNextLocationOffset(child));
- if (useLargestChild) {
- largestChildHeight = Math.max(childHeight, largestChildHeight);
- }
- }
- ..........................
- //以下方法是对 LinearLayout 宽度相关的测量工作,不是我们关心的
- if (widthMode != View.MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
- .........................
- //以上方法是对 LinearLayout 宽度相关的测量工作
- if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
- mTotalLength += mDividerHeight;
- }
- //第3步,如果设置了 android:measureWithLargestChild="true"并且测量模式为 AT_MOST或者 UNSPECIFIED
- // 重新计算 mTotalLength 总高度
- if (useLargestChild &&
- (heightMode == View.MeasureSpec.AT_MOST || heightMode == View.MeasureSpec.UNSPECIFIED)) {
- mTotalLength = 0;
- for (int i = 0; i < count; ++i) {
- final View child = getVirtualChildAt(i);
- if (child == null) {
- mTotalLength += measureNullChild(i);
- continue;
- }
- if (child.getVisibility() == GONE) {
- i += getChildrenSkipCount(child, i);
- continue;
- }
- final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
- child.getLayoutParams();
- // Account for negative margins
- final int totalLength = mTotalLength;
- //每个子视图的高度为:最大子视图高度 + 该子视图的上下外边距
- mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
- lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
- }
- }
- // Add in our padding
- mTotalLength += mPaddingTop + mPaddingBottom;
- int heightSize = mTotalLength;
- // Check against our minimum height
- heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
- //第4步,根据 heightMeasureSpec 测量模式 和已经测量得到的总高度 heightSize
- //来确定得到最终 LinearLayout 高度和状态
- int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
- //分割线=================以上代码就完成了对 LinearLayout 高度和状态 的测量
- //第5步,下面代码是根据已经测量得到的 LinearLayout 高度来重新测量确定各个子 view 的大小
- //获取 LinearLayout 高度值
- heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
- //获取最终测量高度和经过测量各个子 view 得到的总高度差值
- int delta = heightSize - mTotalLength;
- //第5.1步(第5步中第1小步),如果在上面第一次测量子 view 的过程中有未进行测量的 view 那么执行下面代码
- if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
- float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
- mTotalLength = 0;
- for (int i = 0; i < count; ++i) {
- final View child = getVirtualChildAt(i);
- if (child.getVisibility() == View.GONE) {
- continue;
- }
- LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
- float childExtra = lp.weight;
- if (childExtra > 0) {
- // 计算 weight 属性分配的大小,可能为负值
- int share = (int) (childExtra * delta / weightSum);
- weightSum -= childExtra;
- delta -= share;
- final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
- mPaddingLeft + mPaddingRight +
- lp.leftMargin + lp.rightMargin, lp.width);
- // TODO: Use a field like lp.isMeasured to figure out if this
- // child has been previously measured
- if ((lp.height != 0) || (heightMode != View.MeasureSpec.EXACTLY)) {
- // 子视图在第一次测量时候已经测量过
- // 基于上次测量值再次进行新的测量
- int childHeight = child.getMeasuredHeight() + share;
- if (childHeight < 0) {
- childHeight = 0;
- }
- // 调用子 view 的 measure 方法进行测量,后面逻辑就是 view 的测量逻辑
- child.measure(childWidthMeasureSpec,
- View.MeasureSpec.makeMeasureSpec(childHeight, View.MeasureSpec.EXACTLY));
- } else {
- // 子视图第一次测量,即第一步进行测量的时候未得到测量
- //对 view 进行测量
- child.measure(childWidthMeasureSpec,
- View.MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
- View.MeasureSpec.EXACTLY));
- }
- // Child may now not fit in vertical dimension.
- childState = combineMeasuredStates(childState, child.getMeasuredState()
- & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
- }
- // 处理子视图宽度
- final int margin = lp.leftMargin + lp.rightMargin;
- ...........................
- // Add in our padding
- mTotalLength += mPaddingTop + mPaddingBottom;
- // TODO: Should we recompute the heightSpec based on the new total length?
- } else {
- //第5.2步(第5步中第2小步)执行到这里的代码,表明 view 是已经测量过的
- alternativeMaxWidth = Math.max(alternativeMaxWidth,
- weightedMaxWidth);
- // We have no limit, so make all weighted views as tall as the largest child.
- // Children will have already been measured once.
- if (useLargestChild && heightMode != View.MeasureSpec.EXACTLY) {
- for (int i = 0; i < count; i++) {
- final View child = getVirtualChildAt(i);
- if (child == null || child.getVisibility() == View.GONE) {
- continue;
- }
- final LinearLayout.LayoutParams lp =
- (LinearLayout.LayoutParams) child.getLayoutParams();
- float childExtra = lp.weight;
- //如果 view 使用了权重即 childExtra > 0,使用最大子视图高度进行重新测量
- //否则不进行测量,保持第一次测量值,那么由于 LinearLayout 的高度使用了子 view 最大高度 ,
- // 但是子视图没有进行重新测量,没有进行拉伸,可能造成空间剩余。
- if (childExtra > 0) {
- //使用最大子视图高度进行重新测量子 view
- child.measure(
- View.MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
- View.MeasureSpec.EXACTLY),
- View.MeasureSpec.makeMeasureSpec(largestChildHeight,
- View.MeasureSpec.EXACTLY));
- }
- }
- }
- }
- if (!allFillParent && widthMode != View.MeasureSpec.EXACTLY) {
- maxWidth = alternativeMaxWidth;
- }
- maxWidth += mPaddingLeft + mPaddingRight;
- // Check against our minimum width
- maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
- //第6步,最终设置 LinearLayout 的测量高宽
- setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
- heightSizeAndState);
- if (matchWidth) {
- forceUniformWidth(count, heightMeasureSpec);
- }
- }
上面代码可以看出 ViewGroup 内部测量方式分为垂直方向和水平方向,两者原理基本一样,下面看一下垂直方向的 LinearLayout 测量方式,由于这个方法代码比较长,所以贴出重点部分:
以上代码就是对 LinearLayout onMeasure 分析过程,整个过程原理已经在代码中加以注释说明,这里我们重点分析一下 resolveSizeAndState(heightSize, heightMeasureSpec, 0) 这个方法是如何实现最终确定 LinearLayout 高度值的,方法如下:
- /**
- * @param size view 想要的大小,也就是根据子 view 高度测量得到的高度值.
- * @param measureSpec 父容器的约束条件
- * @param childMeasuredState 子 view 的测量信息
- * @return Size 返回得到的测量值和状态
- */
- public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
- //获取测量模式
- final int specMode = MeasureSpec.getMode(measureSpec);
- //获取尺寸值
- final int specSize = MeasureSpec.getSize(measureSpec);
- final int result;
- //根据不同测量模式决定最终测量结果
- switch (specMode) {
- //如果是 AT_MOST 最大测量模式 ,那么总高度值为测量得到的 size 值,但是最大不能超过 specSize 规定值
- case MeasureSpec.AT_MOST:
- if (specSize < size) {
- //如果测量得到的 size 值超过 specSize 值,LinearLayout 高度就为 specSize 值
- result = specSize | MEASURED_STATE_TOO_SMALL;
- } else {
- //如果测量得到的 size 值未超过 specSize 值,LinearLayout 高度就为 size 值
- result = size;
- }
- break;
- case MeasureSpec.EXACTLY:
- //如果是 EXACTLY 精准测量模式,即 LinearLayout 值为固定值,那么 最终 LinearLayout 高度值就为 specSize 值
- result = specSize;
- break;
- case MeasureSpec.UNSPECIFIED:
- // 如果是 UNSPECIFIED 测量模式,即对子 view 没有限制 , LinearLayout 高度值就为 size
- default:
- result = size;
- }
- return result | (childMeasuredState & MEASURED_STATE_MASK);
- }
最后对整个测量过程总结一下就是分为以下几步:以上代码总结起来就是 LinearLayout 会根据测量子 View 的情况和 MeasureSpec 约束条件来决定自己最终的大小,具体来说就是如果它的布局中高度才用 具体数值,那么它的测量过程和 View 一致,即高度为 specSize 值,如果它的布局中使用 wrap_content 那么它的高度是所有子 View 高度总和,但是不能超过父容器剩余空间。
- 对 LinearLayout 中的子 View 进行第一次遍历测量,主要是通过 measureChildBeforeLayout 这个方法,这个方法内部会调用 measureChildWithMargins 方法,而在 measureChildWithMargins 方法内部会去调用 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 方法进行测量。在这次的测量过程中,如果满足了第1.1步测量条件的子 view 不需要进行测量,会在后面的第5.1步中进行测量。
- 根据测量各个子 View 的高度会得到一个初步的 LinearLayout 总高度 mTotalLength 值。
- 如果 LinearLayout 设置了 android:measureWithLargestChild=”true” 属性并且测量模式为 AT_MOST或者 UNSPECIFIED 重新计算 mTotalLength 总高度。
- 根据 LinearLayout 的 heightMeasureSpec 测量模式 和已经测量得到的总高度 mTotalLength ,来确定得到最终 LinearLayout 高度和状态 。
- 根据已经测量得到的 LinearLayout 高度来重新测量确定各个子 View 的大小。
- 最终执行 setMeasuredDimension 方法设置 LinearLayout 的测量高宽。
5.流程图
6.measure总结
- View的measure方法是final类型的,子类不可以重写,子类可以通过重写onMeasure方法来测量自己的大小,当然也可以不重写onMeasure方法使用系统默认测量大小。
- View测量结束的标志是调用了View类中的setMeasuredDimension成员方法,言外之意是,如果你需要在自定义的View中重写onMeasure方法,在你测量结束之前你必须调用setMeasuredDimension方法测量才有效。
- 在Activity生命周期onCreate和onResume方法中调用View.getWidth()和View.getMeasuredHeight()返回值为0的,是因为当前View的测量还没有开始,这里关系到Activity启动过程,文章开头说了当ActivityThread类中的performResumeActivity方法执行之后才将DecorView添加到PhoneWindow窗口上,开始测量。在Activity生命周期onCreate在中performResumeActivity还为执行,因此调用View.getMeasuredHeight()返回值为0。
- 子视图View的大小是由父容器View和子视图View布局共同决定的
7. 实际问题解决
View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,因此无法保证 Activity 执行了onCreate、onStart、onResume 时某个 View 已经测量完毕了。如果View还没有测量完毕,那么获得的宽和高都是 0。下面是四种解决该问题的方法:
1、Activity/View#onWindowsChanged 方法
onWindowFocusChanged 方法表示 View 已经初始化完毕了,宽高已经准备好了,这个时候去获取是没问题的。这个方法会被调用多次,当 Activity 继续执行或者暂停执行的时候,这个方法都会被调用,典型代码如下:
- public void onWindowFocusChanged(boolean hasWindowFocus) {
- super.onWindowFocusChanged(hasWindowFocus);
- if(hasWindowFocus){
- int width=view.getMeasuredWidth();
- int height=view.getMeasuredHeight();
- }
- }
2、View.post(runnable)
通过 post 将一个 Runnable 投递到消息队列的尾部,然后等待 Looper 调用此 runnable 的时候 View 也已经初始化好了。
- @Override
- protected void onStart() {
- super.onStart();
- view.post(new Runnable() {
- @Override
- public void run() {
- int width=view.getMeasuredWidth();
- int height=view.getMeasuredHeight();
- }
- });
- }
3、 ViewTreeObsever
使用 ViewTreeObserver 的众多回调方法可以完成这个功能,比如使用 onGlobalLayoutListener 接口,当 View 树的状态发生改变或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法将被回调。伴随着View树的变化,这个方法也会被多次调用。
- @Override
- protected void onStart() {
- super.onStart();
- ViewTreeObserver viewTreeObserver=view.getViewTreeObserver();
- viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
- int width=view.getMeasuredWidth();
- int height=view.getMeasuredHeight();
- }
- });
- }
通过手动对 View 进行 measure 来得到 View 的宽高,这个要根据 View 的 LayoutParams 来处理:4、 view.measure(int widthMeasureSpec, int heightMeasureSpec)
(1)match_parent:无法 measure 出具体的宽高,原因是根据上面我们分析 View 的measure 过程原理可知,此种 MeasureSpec 需要知道 parentSize ,即父容器剩余空间,而这个时候无法知道 parentSize 大小,所以无法测量。
(2)wrap_content: 可以采用设置最大值方法进 measure :
- int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
- int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
- view.measure(widthMeasureSpec, heightMeasureSpec);
(3)具体数值(dp/px):
例如100px,如下 measure :注意这里作者为什么使用 (1 << 30) - 1 ) 来构造 MeasureSpec 呢?笔者解释是:”通过分析 MeasureSpec 的实现可以得知 View 的尺寸是使用 30 位的二进制表示,也就是说最大是 30 个 1 即(2^30-1),也就是 (1 << 30) - 1 ),在最大化模式下,使用 View 能支持的最大值去构造 MeasureSpec 是合理的“。为什么这样就合理呢?我们前面分析在子 View 使用 wrap_content 模式的时候,其测量规则是根据自身的情况去测量尺寸,但是不能超过父容器的剩余空间的最大值,换句话说就是父容器给子 View 一个最大值,然后告诉子 View 你自己看着办,但是别超过这个尺寸就行,但是现在我们自己去测量的时候不知道父容器给定的 MeasureSpec 情况, 也就是不知道父容器给多大的限定值,需要自己去构造一个MeasureSpec ,那么这个最大值我们给定多少合适呢?所以这里干脆就给一个 View 所能支持的最大值,然子 View 根据自身情况去测量,怎么也不能超过这个值就行了。
- int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
- int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
- view.measure(widthMeasureSpec, heightMeasureSpec);
以上四种解决方法的代码和原理上面已经详细说明,大家可以根据需要进行选择使用。
四 .Layout
1.layout 过程详解
layout 的作用是 ViewGroup 来确定子元素的位置,当 ViewGroup 的位置被确定后,在 layout 中会调用 onLayout ,在 onLayout 中会遍历所有的子元素并调用子元素的 layout 方法,在子元素的 layout 方法中 onLayout 方法又会被调用,layout 方法是确定 View 本身在屏幕上显示的具体位置,即在代码中设置其成员变量 mLeft,mTop,mRight,mBottom 的值,这几个值是在屏幕上构成矩形区域的四个坐标点,就是该 View 显示的位置,不过这里的具体位置都是相对与父视图的位置而言,而 onLayout 方法则会确定所有子元素位置,ViewGroup 在 onLayout 函数中通过调用其 children 的 layout 函数来设置子视图相对与父视图中的位置,具体位置由函数 layout 的参数决定。下面我们先看 View 的layout 方法如下:
- /*
- *@param l view 左边缘相对于父布局左边缘距离
- *@param t view 上边缘相对于父布局上边缘位置
- *@param r view 右边缘相对于父布局左边缘距离
- *@param b view 下边缘相对于父布局上边缘距离
- */
- public void layout(int l, int t, int r, int b) {
- if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
- onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
- mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
- }
- //记录 view 原始位置
- int oldL = mLeft;
- int oldT = mTop;
- int oldB = mBottom;
- int oldR = mRight;
- //第1步,调用 setFrame 方法 设置新的 mLeft、mTop、mBottom、mRight 值,
- //设置 View 本身四个顶点位置
- //并返回 changed 用于判断 view 布局是否改变
- boolean changed = isLayoutModeOptical(mParent) ?
- setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
- //第二步,如果 view 位置改变那么调用 onLayout 方法设置子 view 位置
- if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
- //调用 onLayout
- onLayout(changed, l, t, r, b);
- mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
- ListenerInfo li = mListenerInfo;
- if (li != null && li.mOnLayoutChangeListeners != null) {
- ArrayList<OnLayoutChangeListener> listenersCopy =
- (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
- int numListeners = listenersCopy.size();
- for (int i = 0; i < numListeners; ++i) {
- listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
- }
- }
- }
先看一下 View 的 onLayout 方法:layout 方法大致流程:先通过上面代码第一步调用 setFrame 设置 view 本身四个顶点位置,其中 setOpticalFrame 内部也是调用 setFrame 方法来完成设置的,即为 View 的4个成员变量(mLeft,mTop,mRight,mBottom)赋值,View 的四个顶点一旦确定,那么 View 在父容器中的位置就确定了,接着进行第二步,调用 onLayout 方法,这个方法用途是父容器确定子 View 位置,和 onMeasure 方法类似, onLayout 方法的具体实现同样和具体布局有关,所以 View 和 ViewGroup 中都没有真正实现 onLayout 方法,都是一个空方法。 void onLayout(boolean changed, int left, int top, int right, int bottom) {}
那么对于我们自定义的 View 是继承自 View 的情况下,我们一般不需要重写 onLayout 方法,因为 这个方法用途是父容器确定子 View 位置,对于 View 来说是没有子 View 的,所以一般不需要重写。再看一下 ViewGroup 的 onLayout 方法:
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
相对于 view 来说,ViewGroup 中 onLayout 多了关键字abstract的修饰,也就说对于继承自 ViewGroup 的自定义 View 必须要重写 onLayout 方法,而重载 onLayout 的目的就是安排其子元素在父视图中的具体位置,为了更好的理解,接下来我们看一下 LinearLayout 的 onLayout 方法:
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- if (mOrientation == VERTICAL) {
- layoutVertical(l, t, r, b);
- } else {
- layoutHorizontal(l, t, r, b);
- }
- }
和 onMeasure 类似,这里也是分为竖直方向和水平方向的布局安排,二者原理一样,我们选择竖直方向的 layoutVertical 来进行分析,这里给出主要代码如下:
- void layoutVertical(int left, int top, int right, int bottom) {
- final int paddingLeft = mPaddingLeft;
- //记录子 View 上边缘相对于父容器上边缘距离
- int childTop;
- //记录子 View 左边缘相对于父容器左边缘距离
- int childLeft;
- //第1步,主要是根据不同的 gravity 属性来确定子元素的 child 的位置
- switch (majorGravity) {
- case Gravity.BOTTOM:
- // mTotalLength contains the padding already
- childTop = mPaddingTop + bottom - top - mTotalLength;
- break;
- // mTotalLength contains the padding already
- case Gravity.CENTER_VERTICAL:
- childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
- break;
- case Gravity.TOP:
- default:
- childTop = mPaddingTop;
- break;
- }
- ...............................
- //第2步,循环遍历子 view
- for (int i = 0; i < count; i++) {
- //获取指定位置 view
- final View child = getVirtualChildAt(i);
- if (child == null) {
- childTop += measureNullChild(i);
- } else if (child.getVisibility() != GONE) {
- //第2.1步,如果 view 可见,获取 view 的测量宽/高
- final int childWidth = child.getMeasuredWidth();
- final int childHeight = child.getMeasuredHeight();
- //获取 view 的 LayoutParams 参数
- final LinearLayout.LayoutParams lp =
- (LinearLayout.LayoutParams) child.getLayoutParams();
- .............
- if (hasDividerBeforeChildAt(i)) {
- childTop += mDividerHeight;
- }
- childTop += lp.topMargin;
- //第3步,设置子 view 位置
- setChildFrame(child, childLeft, childTop + getLocationOffset(child),
- childWidth, childHeight);
- //第4步,重新计算子 view 的 顶部 top 位置,也就是每增加一个子 view
- //下一个子 view 的 top 顶部位置就会相应的增加
- childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
- i += getChildrenSkipCount(child, i);
- }
- }
- }
简单梳理下整个流程,此方法会遍历所有子 view ,并调用 setChildFrame 方法来设定子元素位置,然后重新计算 childTop ,childTop 随着子元素的遍历而逐渐增大,这就意味着后面的子元素会被放置在当前子元素的下方,这正是我们平时使用竖直方向 LinearLayout 的特性。这里我们看一下第三步执行的 setChildFrame 方法类设置子元素位置方法代码:
- private void setChildFrame(View child, int left, int top, int width, int height) {
- child.layout(left, top, left + width, top + height);
- }
简单梳理下整个流程,此方法会遍历所有子 view ,并调用 setChildFrame 方法来设定子元素位置,然后重新计算 childTop ,childTop 随着子元素的遍历而逐渐增大,这就意味着后面的子元素会被放置在当前子元素的下方,这正是我们平时使用竖直方向 LinearLayout 的特性。这里我们看一下第三步执行的 setChildFrame 方法类设置子元素位置方法代码:
可以发现,这个方法只是调用子元素的 layout 方法而已,这样父元素在自己的 layout 方法中完成自己的定位之后,通过 onLayout 方法去调用了子元素的 layout 方法,子元素又会通过自己的 layout 方法完成自己的位置设定,这样一层一层的传递下去就完成了整个 view 数的 layout 过程。
这里我们注意到在第三步调用 setChildFrame 方法中的 传入的参数 childWidth 和 childHeight 是上面第2.1步获取的子元素的测量宽/高,而在 layout 过程中会通过 setFrame 方法设置子元素四个顶点位置,这样子元素的位置就确定了,在 setFrame 中有如下赋值语句:
- mLeft = left;
- mTop = top;
- mRight = right;
- mBottom = bottom;
说到这里就不得说一下 getWidth() 、getHeight() 和 getMeasuredWidth()、getMeasuredHeight() 这两对函数之间的区别,即 View 的测量宽/高和最终显示宽/高之间的区别。首先我们看一下 getWith() 和 getHeight() 方法的具体实现:也就是说在 LinearLayout 中其子视图显示的宽和高由 measure 过程来决定的,因此 measure 过程的意义就是为 layout 过程提供视图显示范围的参考值。为什么说是提供参考值呢?因为 layout 过程中的4个参数 left, top, iwidth, height 完全可以由视图设计者任意指定,而最终视图的布局位置和大小完全由这4个参数决定,measure 过程得到的mMeasuredWidth 和 mMeasuredHeight 提供了视图大小的测量值,只是提供一个参考一般情况下我们使用这个参考值,但我们完全可以不使用这两个值,而自己在 layout 过程中去设定一个值,可见 measure 过程并不是必须的。
说到这里就不得说一下 getWidth() 、getHeight() 和 getMeasuredWidth()、getMeasuredHeight() 这两对函数之间的区别,即 View 的测量宽/高和最终显示宽/高之间的区别。首先我们看一下 getWith() 和 getHeight() 方法的具体实现:
- public final int getWidth() {
- return mRight - mLeft;
- }
- public final int getHeight() {
- return mBottom - mTop;
- }
通过 getWith() 和 getHeight() 源码和上面 setChildFrame(View child, int left, int top, int width, int height) 方法设置子元素四个顶点位置的四个变量 mLeft、mTop、mRight、mBottom 的赋值过程来看,默认情况下 getWidth() 、getHeight() 方法返回的值正好就是 view 的测量宽/高,只不过 view 的测量宽/高形成于 view 的measure 过程,而最终宽/高形成于 view 的 layout 方法中,但是对于特殊情况,两者的值是不相等的,就是我们在 layout 过程中不按默认常规套路出牌,即不使用 measure 过程得到的 mMeasuredWidth 和 mMeasuredHeight ,而是人为的去自己根据需要设定的一个值的情况,例如以下代码,重写 view 的 layout 方法:
- public void layout(int l, int t, int r, int b) {
- //在得到的测量值基础上加100
- super.layout(int l, int t, int r+100, int b+100);
- }
通过 getWith() 和 getHeight() 源码和上面 setChildFrame(View child, int left, int top, int width, int height) 方法设置子元素四个顶点位置的四个变量 mLeft、mTop、mRight、mBottom 的赋值过程来看,默认情况下 getWidth() 、getHeight() 方法返回的值正好就是 view 的测量宽/高,只不过 view 的测量宽/高形成于 view 的measure 过程,而最终宽/高形成于 view 的 layout 方法中,但是对于特殊情况,两者的值是不相等的,就是我们在 layout 过程中不按默认常规套路出牌,即不使用 measure 过程得到的 mMeasuredWidth 和 mMeasuredHeight ,而是人为的去自己根据需要设定的一个值的情况,例如以下代码,重写 view 的 layout 方法:
上面代码会导致在任何情况下 view 的最终宽/高总会比测量宽高大100px。
2.流程图
3.layout布局总结
1.视图View的布局逻辑是由父View,也就是ViewGroup容器布局来实现的。因此,我们如果自定义View一般都无需重写onMeasure方法,但是如果自定义一个ViewGroup容器的话,就必须实现onLayout方法,因为该方法在ViewGroup是抽象的,所有ViewGroup的所有子类必须实现onLayout方法。
2.当我们的视图View在布局中使用android:visibility=”gone”属性时,是不占据屏幕空间的,因为在布局时ViewGroup会遍历每个子视图View,判断当前子视图View是否设置了 Visibility==GONE,如果设置了,当前子视图View就会添加到父容器上,因此也就不占据屏幕空间。具体可以参考2-4节。
3.必须在View布局完之后调用getHeight()和getWidth()方法获取到的View的宽高才大于0。
五.Draw
1.draw 过程详解
draw 的作用是将 view 绘制到屏幕上,view 的绘制过程准守以下几个步骤:
- 绘制背景:
background.draw(canvas)
; - 绘制自己:
onDraw()
; - 绘制 children:
dispatchDraw
; -
绘制装饰:
onDrawScrollBars
。通过源码可以看出来,部分源码如下:
- public void draw(Canvas canvas) {
- final int privateFlags = mPrivateFlags;
- final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
- (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
- mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
- /*
- * Draw traversal performs several drawing steps which must be executed
- * in the appropriate order:
- *
- * 1. Draw the background
- * 2. If necessary, save the canvas' layers to prepare for fading
- * 3. Draw view's content
- * 4. Draw children
- * 5. If necessary, draw the fading edges and restore layers
- * 6. Draw decorations (scrollbars for instance)
- */
- // Step 1, draw the background, if needed
- //绘制背景
- int saveCount;
- if (!dirtyOpaque) {
- drawBackground(canvas);
- }
- // skip step 2 & 5 if possible (common case)
- final int viewFlags = mViewFlags;
- boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
- boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
- if (!verticalEdges && !horizontalEdges) {
- // Step 3, draw the content
- //调用 onDraw 方法,绘制自己本身内容,这个方法是个空方法,没有具体实现,
- //因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现,
- //如果要自定义 view ,需要重载该方法完成绘制工作
- if (!dirtyOpaque) onDraw(canvas);
- // Step 4, draw the children
- //绘制子视图
- //View 中的 dispatchDraw()方法也是一个空方法,因为 view 本身没有子视图,所以不需要,
- //而 ViewGroup 的 dispatchDraw() 方法中就会有具体的绘制代码,来实现子视图的绘制工作
- dispatchDraw(canvas);
- // Overlay is part of the content and draws beneath Foreground
- if (mOverlay != null && !mOverlay.isEmpty()) {
- mOverlay.getOverlayView().dispatchDraw(canvas);
- }
- // Step 6, draw decorations (foreground, scrollbars)
- //绘制装饰
- //对视图的滚动条进行绘制,其实任何一个视图都是有滚动条的,只是一般情况下都没有让它显示出来,
- //而例如像 ListView 等控件是进行了显示而已。
- onDrawForeground(canvas);
- // we're done...
- return;
- }
通过上面代码可以发现,View 绘制过程的传递是通过 dispatchDraw() 方法完成,这个方法会遍历调用所有子视图的 draw ()方法,这样事件就一层一层的传递下去了。其中 View 中有一个特殊方法 setWillNotDraw ,源码如下:
- /**
- * If this view doesn't do any drawing on its own, set this flag to
- * allow further optimizations. By default, this flag is not set on
- * View, but could be set on some View subclasses such as ViewGroup.
- *
- * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
- * you should clear this flag.
- *
- * @param willNotDraw whether or not this View draw on its own
- */
- public void setWillNotDraw(boolean willNotDraw) {
- setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
- }
看注释部分大概意思是,如果一个 View 不需要绘制任何内容,那么设置这个标记位为 true 后,系统会进行相应的优化,默认情况下 View 没有启动这个默认标记位,但 viewGroup 默认启用这个标记位,这个标记位对实际开发的意义是:当我们的自定义的控件继承自 viewGroup 并且本身不具备绘制功能的时候,就可以开启这个标记位,从而便于系统进行后续的优化工作,当我们明确知道 viewGrop 需要通过 onDraw 来绘制本身内容时,需要我们去关闭 WILL_NOT_DRAW 这个标记位。
2.流程图
视图View的绘制可以分为如下6个步骤:
- 绘制当前视图的背景。
- 保存当前画布的堆栈状态,并且在在当前画布上创建额外的图层,以便接下来可以用来绘制当前视图在滑动时的边框渐变效果。
- 绘制当前视图的内容。
- 绘制当前视图的子视图的内容。
- 绘制当前视图在滑动时的边框渐变效果。
- 绘制当前视图的滚动条。
3.绘制Draw总结:
1.View绘制的画布参数canvas是由surface对象获得,言外之意,View视图绘制最终会绘制到Surface对象去。关于Surface内容参考3-1节。
2.由3-2小节我们了解到,父类View绘制主要是绘制背景,边框渐变效果,进度条,View具体的内容绘制调用了onDraw方法,通过该方法把View内容的绘制逻辑留给子类去实现。因此,我们在自定义View的时候都一般都需要重写父类的onDraw方法来实现View内容绘制。
3.不管任何情况,每一个View视图都会绘制scrollBars滚动条,且绘制滚动条的逻辑是在父类View中实现,子类无需自己实现滚动条的绘制。其实TextView也是有滚动条的,可以通过代码让其显示滚动条和内容滚动效果。你只需在TextView布局设置android:scrollbars=”vertical”属性,且在代码中进行如下设置
textView
.setMovementMethod(ScrollingMovementMethod
.getInstance())
;
这样既可让你的TextView内容可以滑动,且有滚动条。
4.ViewGroup绘制的过程会对每个子视图View设置布局容器动画效果,如果你在ViewGroup容器布局里面设置了如下属性的话:
android:animateLayoutChanges=
"true"
六.requestLayout
当我们动态移动一个View的位置,或者View的大小、形状发生了变化的时候,我们可以在view中调用这个方法,即:
view.requestLayout();
那么该方法的作用是什么呢?
从方法名字可以知道,“请求布局”,那就是说,如果调用了这个方法,那么对于一个子View来说,应该会重新进行布局流程。但是,真实情况略有不同,如果子View调用了这个方法,其实会从View树重新进行一次测量、布局、绘制这三个流程,最终就会显示子View的最终情况。那么,这个方法是怎么实现的呢?我们从源码角度进行解析。
首先,我们看View#requestLayout方法:
- /**
- * Call this when something has changed whichhas invalidated the
- * layout of this view. This will schedule alayout pass of the view
- * tree. This should not be called while theview hierarchy is currently in a layout
- * pass ({@link #isInLayout()}. If layout ishappening, the request may be honored at the
- * end of the current layout pass (and thenlayout will run again) or after the current
- * frame is drawn and the next layout occurs.
- *
- * <p>Subclasses which override thismethod should call the superclass method to
- * handle possible request-during-layout errorscorrectly.</p>
- */
- //从源码注释可以看出,如果当前View在请求布局的时候,View树正在进行布局流程的话,
- //该请求会延迟到布局流程完成后或者绘制流程完成且下一次布局发现的时候再执行。
- @CallSuper
- publicvoid requestLayout() {
- if (mMeasureCache != null)mMeasureCache.clear();
- if (mAttachInfo != null &&mAttachInfo.mViewRequestingLayout == null) {
- // Only trigger request-during-layout logicif this is the view requesting it,
- // not the views in its parent hierarchy
- ViewRootImpl viewRoot =getViewRootImpl();
- if (viewRoot != null && viewRoot.isInLayout()) {
- if (!viewRoot.requestLayoutDuringLayout(this)) {
- return;
- }
- }
- mAttachInfo.mViewRequestingLayout = this;
- }
- //为当前view设置标记位 PFLAG_FORCE_LAYOUT
- mPrivateFlags |= PFLAG_FORCE_LAYOUT;
- mPrivateFlags |= PFLAG_INVALIDATED;
- if (mParent != null &&!mParent.isLayoutRequested()) {
- //向父容器请求布局
- mParent.requestLayout();
- }
- if (mAttachInfo != null &&mAttachInfo.mViewRequestingLayout == this) {
- mAttachInfo.mViewRequestingLayout = null;
- }
- }
在requestLayout方法中,首先先判断当前View树是否正在布局流程,接着为当前子View设置标记位,该标记位的作用就是标记了当前的View是需要进行重新布局的,接着调用mParent.requestLayout方法,这个十分重要,因为这里是向父容器请求布局,即调用父容器的requestLayout方法,为父容器添加PFLAG_FORCE_LAYOUT标记位,而父容器又会调用它的父容器的requestLayout方法,即requestLayout事件层层向上传递,直到DecorView,即根View,而根View又会传递给ViewRootImpl,也即是说子View的requestLayout事件,最终会被ViewRootImpl接收并得到处理。纵观这个向上传递的流程,其实是采用了责任链模式,即不断向上传递该事件,直到找到能处理该事件的上级,在这里,只有ViewRootImpl能够处理requestLayout事件。
在ViewRootImpl中,重写了requestLayout方法,我们看看这个方法,ViewRootImpl#requestLayout:
- @Override
- public void requestLayout() {
- if (!mHandlingLayoutInLayoutRequest) {
- checkThread();
- mLayoutRequested = true;
- scheduleTraversals();
- }
- }
在这里,调用了scheduleTraversals方法,这个方法是一个异步方法,最终会调用到ViewRootImpl#performTraversals方法,这也是View工作流程的核心方法,在这个方法内部,分别调用measure、layout、draw方法来进行View的三大工作流程,对于三大工作流程,前几篇文章已经详细讲述了,这里再做一点补充说明。
先看View#measure方法:
- public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
- ...
- if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
- widthMeasureSpec !=mOldWidthMeasureSpec ||
- heightMeasureSpec !=mOldHeightMeasureSpec) {
- ...
- if (cacheIndex < 0 || sIgnoreMeasureCache) {
- // measure ourselves, this should set themeasured dimension flag back
- onMeasure(widthMeasureSpec,heightMeasureSpec);
- mPrivateFlags3 &=~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
- }
- ...
- mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
- }
- }
首先是判断一下标记位,如果当前View的标记位为PFLAG_FORCE_LAYOUT,那么就会进行测量流程,调用onMeasure,对该View进行测量,接着最后为标记位设置为PFLAG_LAYOUT_REQUIRED,这个标记位的作用就是在View的layout流程中,如果当前View设置了该标记位,则会进行布局流程。具体可以看如下View#layout源码:
- public void layout(int l, int t, int r, int b) {
- ...
- //判断标记位是否为PFLAG_LAYOUT_REQUIRED,如果有,则对该View进行布局
- if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) ==PFLAG_LAYOUT_REQUIRED) {
- onLayout(changed, l, t, r, b);
- //onLayout方法完成后,清除PFLAG_LAYOUT_REQUIRED标记位
- mPrivateFlags &=~PFLAG_LAYOUT_REQUIRED;
- ListenerInfo li = mListenerInfo;
- if (li != null &&li.mOnLayoutChangeListeners != null) {
- ArrayList<OnLayoutChangeListener> listenersCopy =
- (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
- int numListeners = listenersCopy.size();
- for (int i = 0; i < numListeners; ++i) {
- listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
- }
- }
- }
- //最后清除PFLAG_FORCE_LAYOUT标记位
- mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
- mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
- }
那么到目前为止,requestLayout的流程便完成了。
小结:子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。
七.invalidate
该方法的调用会引起View树的重绘,常用于内部调用(比如 setVisiblity())或者需要刷新界面的时候,需要在主线程(即UI线程)中调用该方法。那么我们来分析一下它的实现。
首先,一个子View调用该方法,那么我们直接看View#invalidate方法:
- public void invalidate() {
- invalidate(true);
- }
- void invalidate(boolean invalidateCache) {
- invalidateInternal(0, 0, mRight -mLeft, mBottom - mTop, invalidateCache, true);
- }
- void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
- boolean fullInvalidate) {
- if (mGhostView != null) {
- mGhostView.invalidate(true);
- return;
- }
- //这里判断该子View是否可见或者是否处于动画中
- if (skipInvalidate()) {
- return;
- }
- //根据View的标记位来判断该子View是否需要重绘,假如View没有任何变化,那么就不需要重绘
- if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN |PFLAG_HAS_BOUNDS)
- || (invalidateCache &&(mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
- || (mPrivateFlags &PFLAG_INVALIDATED) != PFLAG_INVALIDATED
- || (fullInvalidate &&isOpaque() != mLastIsOpaque)) {
- if (fullInvalidate) {
- mLastIsOpaque = isOpaque();
- mPrivateFlags &= ~PFLAG_DRAWN;
- }
- //设置PFLAG_DIRTY标记位
- mPrivateFlags |= PFLAG_DIRTY;
- if (invalidateCache) {
- mPrivateFlags |= PFLAG_INVALIDATED;
- mPrivateFlags &=~PFLAG_DRAWING_CACHE_VALID;
- }
- // Propagate the damage rectangle to theparent view.
- //把需要重绘的区域传递给父容器
- final AttachInfo ai = mAttachInfo;
- final ViewParent p = mParent;
- if (p != null && ai!= null && l < r && t <b) {
- final Rect damage = ai.mTmpInvalRect;
- damage.set(l, t, r, b);
- //调用父容器的方法,向上传递事件
- p.invalidateChild(this, damage);
- }
- ...
- }
- }
可以看出,invalidate有多个重载方法,但最终都会调用invalidateInternal方法,在这个方法内部,进行了一系列的判断,判断View是否需要重绘,接着为该View设置标记位,然后把需要重绘的区域传递给父容器,即调用父容器的invalidateChild方法。
接着我们看ViewGroup#invalidateChild:
- /**
- * Don't call or override this method. It isused for the implementation of
- * the view hierarchy.
- */
- publicfinalvoid invalidateChild(View child, final Rect dirty) {
- //设置 parent 等于自身
- ViewParent parent = this;
- final AttachInfo attachInfo = mAttachInfo;
- if (attachInfo != null) {
- // If the child is drawing an animation, wewant to copy this flag onto
- // ourselves and the parent to make surethe invalidate request goes
- // through
- finalboolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION)
- == PFLAG_DRAW_ANIMATION;
- // Check whether the child that requeststhe invalidate is fully opaque
- // Views being animated or transformed arenot considered opaque because we may
- // be invalidating their old position andneed the parent to paint behind them.
- Matrix childMatrix = child.getMatrix();
- finalboolean isOpaque = child.isOpaque() && !drawAnimation &&
- child.getAnimation() == null && childMatrix.isIdentity();
- // Mark the child as dirty, using theappropriate flag
- // Make sure we do not set both flags atthe same time
- int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE: PFLAG_DIRTY;
- if (child.mLayerType != LAYER_TYPE_NONE) {
- mPrivateFlags |= PFLAG_INVALIDATED;
- mPrivateFlags &=~PFLAG_DRAWING_CACHE_VALID;
- }
- //储存子View的mLeft和mTop值
- finalint[] location = attachInfo.mInvalidateChildLocation;
- location[CHILD_LEFT_INDEX] =child.mLeft;
- location[CHILD_TOP_INDEX] = child.mTop;
- ...
- do {
- View view = null;
- if (parent instanceofView) {
- view = (View) parent;
- }
- if (drawAnimation) {
- if (view != null) {
- view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
- } elseif (parent instanceof ViewRootImpl) {
- ((ViewRootImpl)parent).mIsAnimating = true;
- }
- }
- // If the parent is dirty opaque or notdirty, mark it dirty with the opaque
- // flag coming from the child thatinitiated the invalidate
- if (view != null) {
- if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
- view.getSolidColor() ==0) {
- opaqueFlag = PFLAG_DIRTY;
- }
- if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) !=PFLAG_DIRTY) {
- //对当前View的标记位进行设置
- view.mPrivateFlags =(view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
- }
- }
- //调用ViewGrup的invalidateChildInParent,如果已经达到最顶层view,则调用ViewRootImpl
- //的invalidateChildInParent。
- parent =parent.invalidateChildInParent(location, dirty);
- if (view != null) {
- // Account fortransform on current parent
- Matrix m = view.getMatrix();
- if (!m.isIdentity()) {
- RectF boundingRect =attachInfo.mTmpTransformRect;
- boundingRect.set(dirty);
- m.mapRect(boundingRect);
- dirty.set((int) (boundingRect.left - 0.5f),
- (int) (boundingRect.top - 0.5f),
- (int) (boundingRect.right + 0.5f),
- (int) (boundingRect.bottom + 0.5f));
- }
- }
- } while (parent != null);
- }
- }
可以看到,在该方法内部,先设置当前视图的标记位,接着有一个do…while…循环,该循环的作用主要是不断向上回溯父容器,求得父容器和子View需要重绘的区域的并集(dirty)。当父容器不是ViewRootImpl的时候,调用的是ViewGroup的invalidateChildInParent方法,我们来看看这个方法,ViewGroup#invalidateChildInParent:
- public ViewParent invalidateChildInParent(finalint[] location, final Rect dirty) {
- if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
- (mPrivateFlags &PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
- if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE |FLAG_ANIMATION_DONE)) !=
- FLAG_OPTIMIZE_INVALIDATE) {
- //将dirty中的坐标转化为父容器中的坐标,考虑mScrollX和mScrollY的影响
- dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
- location[CHILD_TOP_INDEX] -mScrollY);
- if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
- //求并集,结果是把子视图的dirty区域转化为父容器的dirty区域
- dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
- }
- finalint left = mLeft;
- finalint top = mTop;
- if ((mGroupFlags & FLAG_CLIP_CHILDREN) ==FLAG_CLIP_CHILDREN) {
- if (!dirty.intersect(0, 0, mRight - left,mBottom - top)) {
- dirty.setEmpty();
- }
- }
- mPrivateFlags &=~PFLAG_DRAWING_CACHE_VALID;
- //记录当前视图的mLeft和mTop值,在下一次循环中会把当前值再向父容器的坐标转化
- location[CHILD_LEFT_INDEX] = left;
- location[CHILD_TOP_INDEX] = top;
- if (mLayerType != LAYER_TYPE_NONE) {
- mPrivateFlags |=PFLAG_INVALIDATED;
- }
- //返回当前视图的父容器
- return mParent;
- }
- ...
- }
- returnnull;
- }
可以看出,这个方法做的工作主要有:调用offset方法,把当前dirty区域的坐标转化为父容器中的坐标,接着调用union方法,把子dirty区域与父容器的区域求并集,换句话说,dirty区域变成父容器区域。最后返回当前视图的父容器,以便进行下一次循环。
回到上面所说的do…while…循环,由于不断向上调用父容器的方法,到最后会调用到ViewRootImpl的invalidateChildInParent方法,我们来看看它的源码,ViewRootImpl#invalidateChildInParent:
- @Override
- public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
- checkThread();
- if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty);
- if (dirty == null) {
- invalidate();
- returnnull;
- } elseif (dirty.isEmpty() &&!mIsAnimating) {
- returnnull;
- }
- if (mCurScrollY != 0 || mTranslator!= null) {
- mTempRect.set(dirty);
- dirty = mTempRect;
- if (mCurScrollY != 0) {
- dirty.offset(0, -mCurScrollY);
- }
- if (mTranslator != null) {
- mTranslator.translateRectInAppWindowToScreen(dirty);
- }
- if (mAttachInfo.mScalingRequired) {
- dirty.inset(-1, -1);
- }
- }
- final Rect localDirty = mDirty;
- if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
- mAttachInfo.mSetIgnoreDirtyState = true;
- mAttachInfo.mIgnoreDirtyState = true;
- }
- // Add the new dirty rect to the current one
- localDirty.union(dirty.left, dirty.top,dirty.right, dirty.bottom);
- // Intersect with the bounds of the window to skip
- // updates that lie outside of the visible region
- finalfloat appScale =mAttachInfo.mApplicationScale;
- finalboolean intersected = localDirty.intersect(0, 0,
- (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
- if (!intersected) {
- localDirty.setEmpty();
- }
- if (!mWillDrawSoon && (intersected || mIsAnimating)) {
- scheduleTraversals();
- }
- returnnull;
- }
可以看出,该方法所做的工作与上面的差不多,都进行了offset和union对坐标的调整,然后把dirty区域的信息保存在mDirty中,最后调用了scheduleTraversals方法,触发View的工作流程,由于没有添加measure和layout的标记位,因此measure、layout流程不会执行,而是直接从draw流程开始。
好了,现在总结一下invalidate方法,当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。
postInvalidate
这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,invalidate则是在UI线程中调用。
接下来我们分析postInvalidate方法的原理。
首先看View#postInvalidate:
- publicvoid postInvalidate() {
- postInvalidateDelayed(0);
- }
- publicvoid postInvalidateDelayed(long delayMilliseconds) {
- // We try only with the AttachInfo because there's nopoint in invalidating
- // if we are not attached to our window
- final AttachInfo attachInfo = mAttachInfo;
- if (attachInfo != null) {
- attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
- }
- }
可以看出,参数message传递过来的正是View视图的实例,然后直接调用了invalidate方法,然后继续invalidate流程。
到目前为止,对于常用的刷新视图的方法已经分析完毕。最后以一幅流程图来说明requestLayout、invalidate的区别:
View的生命周期
一般来说,如果View确定自身不再适合当前区域,比如说它的LayoutParams发生了改变,需要父布局对其进行重新测量、布局、绘制这三个流程,往往使用requestLayout。而invalidate则是刷新当前View,使当前View进行重绘,不会进行测量、布局流程,因此如果View只需要重绘而不需要测量,布局的时候,使用invalidate方法往往比requestLayout方法更高效。最后,感谢你们的阅读,希望这篇文章给你们带来帮助。
八、自定义 View
lsfdjljkldf1. 自定义 View 分类
-
继承自 View 重写 ondraw 方法
这种方法主要用于实现一些不规则的效果,即想要达到的 View 效果无法使用已有的 View 通过布局组合的方式来实现,所以需要我们自己去绘制去画一个出来,即重写 onDraw 方法,采用这种方式需要注意处理自定义的 View 支持 wrap_content ,并且 padding 也需要自己处理。
-
继承自 ViewGroup 实现特殊的 Layout 容器
主要实现除了 LinearLayout 、 RelativeLayout 等系统已有的 View 容器之外的特殊 View 容器,需要处理 ViewGroup 的测量 onMeasure 和布局 onLayout 这两个方法,并同时处理子元素的测量和布局。
-
继承自 Android 系统本身已有的特定 View (如 TextView)
这种方法是要是为拓展某个已有 View 的功能,在已有的 View 的基础上添加一些功能,方便我们重复使用,这种方法不需要我们进行特殊的处理。
-
继承自 Android 系统本身已有的特定的 ViewGroup (如 LinearLayout)
这种方法主要是为了实现将几个 View 组合在一起形成一个特定的组合模块,来方便我们后续进行使用,例如我们想要一个特定的 TitleBar ,我们可以可以将几个 TextView 和 Button 放在一个 LinearLayout 布局中组合成一个自定义的控件,采用这种方式不需要进行特殊的处理。
2. 自定义 View 须知
自定义 View 过程中需要注意一些事项,如果这些问题处理不好,可能会影响 View 的正常使用和性能。
-
让 View 支持 wrap_content
在自定义 View 时,如果是直接继承自 View 或者 View Group ,并且不在 onMeasure 中对 wrap_content 做特殊处理,那么在我们使用这个自定义的 View 的 wrap_content 属性时,就无法达到预期效果,而是和使用 match_parent 属性效果一样。
-
如果有必要,让自定义的 View 支持 padding 属性
在自定义 View 时,如果是直接继承自 View ,不在 onDraw 方法中处理 padding ,那么该自定义的 View padding属性将失效;如果是直接继承自 ViewGrop 需要在 onMeasure 和 onLayout 中考虑 padding 和 margin 对其造成的影响,否则将导致自定义的控件 padding 和子元素的 margin 属性失效。
-
尽量不要在 View 中使用 Handler ,没必要
View 本身内部提供了一些列的 post 方法,完全可以替代 Handler 作用。
-
View 中如果有线程或者动画需要在特定生命周期进行停止
当包含此 View 的 Activity 退出或者当前 View 被 remove 掉时,View 的 onDetachedFromWindow() 方法会被调用,所以如果有需要停止的线程或者动画可以在这个方法中执行,和此方法相对应的是 onAttachedToWindow() 方法,当包含该 View 的 Activity 启动的时候,该方法就会被调用。同时当 View 变得不可见时,我们需要及时停止线程和动画,否则可能造成内存泄露。
-
View 带有滑动嵌套情形时,需要处理好滑动冲突
如果有滑动冲突需要合适的进行处理。如果要处理好滑动处理可以看一下View 事件的分发机制
3 自定义 View 示例
1. 继承自 View 重写 onDraw 方法
这种方法一般为了实现一些不规则的效果,需要我们自己去绘制去画一个出来 View 出来,即重写 onDraw 方法,采用这种方式需要考虑 View 四周的空白即处理 padding 值,而 margin 值是受父容器控制所以不需要进行处理,并且需要注意处理自定义的 View 支持 wrap_content ,即重写 onMeasure 方法,如果不进行处理那么当在 xml 文件中使用 wrap_content 属性的时候,就相当于 match_parent 属性,这里为了更详细的说明问题,我们一起来实现一个简单的自定义 View ,只简单的画一个圆出来,这里给出关键地方的代码,先看看 onMeasure 部分代码如下:
- /**
- * 这里进行重写 onMeasure 方法,让自定义的 View 支持 Wrap_content 模式
- * 当使用该自定义的 View 时候,如果使用了 Wrap_content 属性后
- * 该 View 的宽和高都为 200dp ,这个尺寸在实际应用中需要根据具体需要和情况进行计算
- * 这里只是为了解释这个原理,任意给定了一个值恰巧是 200dp 而已
- * @param widthMeasureSpec 父容器给定的宽度约束条件
- * @param heightMeasureSpec 父容器给定的高度约束条件
- */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- // 在 xml 文件中使用 wrap_content 属性时,该 View 的默认宽/高值为 200dp
- int width=200;
- int height=200;
- //获取测量值和模式
- int widthMode=MeasureSpec.getMode(widthMeasureSpec);
- int heightMode=MeasureSpec.getMode(heightMeasureSpec);
- int withSize=MeasureSpec.getSize(widthMeasureSpec);
- int heightSize=MeasureSpec.getSize(heightMeasureSpec);
- if(widthMode==MeasureSpec.AT_MOST&&heightMode==MeasureSpec.AT_MOST){
- //宽和高都为 wap_content 模式,进行设定默认值
- setMeasuredDimension(width,height);
- }else if(widthMode==MeasureSpec.AT_MOST){
- //如果只有宽为 wrap_content 模式
- setMeasuredDimension(width,heightSize);
- }else if(heightMode==MeasureSpec.AT_MOST){
- //如果只有高为 wrap_content 模式
- setMeasuredDimension(withSize,height);
- }
- }
以上代码就解决了让 自定义的 View 支持 wrap_content 的问题。下面在看看在 onDraw 方法中进行绘制的时候处理 padding 值的问题,代码如下:
- /**
- * 这里进行绘制 View 的内容,这里要注意需要处理 padding 值,
- * 让自定义的 View 支持 padding 属性,如果不处理,
- *那么在 xml 文件中使用该自定义的 View 的 padding
- * 属性时候,将会失效
- * @param canvas 画布
- */
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- //获取 view 最终宽和高
- int width=getWidth();
- int height=getHeight();
- //获取 padding 值
- int paddingLeft=getPaddingLeft();
- int paddingRight=getPaddingRight();
- int paddingTop=getPaddingTop();
- int paddingBottom=getPaddingBottom();
- //计算去掉 padding 的宽和高
- int withFinal=width-paddingLeft-paddingRight;
- int heightFinal=height-paddingTop-paddingBottom;
- //计算半径
- int radius=Math.min(withFinal/2,heightFinal/2);
- //绘制视图内容
- //确定x轴和y轴圆中心点位置,主要受 paddingLeft 和 withFinal/2 影响
- //即受左上方侧偏移量和圆半径有关,与 RightPadding 无关
- canvas.drawCircle(paddingLeft+withFinal/2,paddingTop+heightFinal/2,radius,paint);
- }
以上代码解决了让自定义的 View 支持 padding 属性。
2. 继承自 ViewGroup 实现特殊的 Layout 容器
这种自定义的 ViewGroup 需要处理 onMeasure 测量和 onLayout 布局两个过程,同时需要处理子元素的测量和布局过程。采用这种方法实现一个规范的自定义 View 是相当复杂的,通过前面分析的 LinearLayout 代码就可以发现,因为要考虑如何摆放子视图以及各种细节的处理,Android 开发艺术探索书中给出了一个相对规范(不完全规范)的自定义的 HorizontalScrollViewEx 视图容器,实现了一个类似 ViewPaper 的控件,内部子视图可以水平方向滑动,并且子视图的内部子元素可以实现竖直方向滑动,很显然这个控件解决了水平方向和竖直方向滑动冲突的问题,该部分知识可以看一下View 的事件分发机制(Android 开发艺术探索读书笔记) 这篇文章从源码角度分析了事件分发机制,理解了就可以解决滑动冲突的问题。下面一起看一下关键部分代码,先看 onMeasure 部分代码如下:
- /**
- * 重写 onMeasure 方法,处理自定义 View 支持 wrap_content 模式
- * @param widthMeasureSpec 父容器给定宽度约束条件
- * @param heightMeasureSpec 父容器给定高度约束条件
- */
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int measuredWidth = 0;
- int measuredHeight = 0;
- //获取子视图个数
- final int childCount = getChildCount();
- //测量子视图
- measureChildren(widthMeasureSpec, heightMeasureSpec);
- //获取父容器给定测量模式和测量值
- int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
- int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
- int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
- int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
- if (childCount == 0) {
- //如果没有子视图直接设定 View 的宽/高为0
- setMeasuredDimension(0, 0);
- } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
- //如果视图宽/高都采用 wrap_content 模式
- final View childView = getChildAt(0);
- //宽度为第一个视图宽度乘以所有子视图个数
- measuredWidth = childView.getMeasuredWidth() * childCount;
- // 高度为第一个视图宽度
- measuredHeight = childView.getMeasuredHeight();
- //设置 自定义视图宽/高值
- setMeasuredDimension(measuredWidth, measuredHeight);
- } else if (heightSpecMode == MeasureSpec.AT_MOST) {
- //如果只有视图高采用 wrap_content 模式
- final View childView = getChildAt(0);
- //设置视图高度为第一个视图高度
- measuredHeight = childView.getMeasuredHeight();
- setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
- } else if (widthSpecMode == MeasureSpec.AT_MOST) {
- //如果视图宽度使用 wrap_content 模式,设置宽度为第一个视图宽度乘以所有子视图个数
- final View childView = getChildAt(0);
- measuredWidth = childView.getMeasuredWidth() * childCount;
- setMeasuredDimension(measuredWidth, heightSpaceSize);
- }
- }
说明,这里为了方便处理有几处不规范的地方如下:以上代码实现了让自定义的 View 支持 wrap_content 属性。
- 假设了所有子视图的高度和宽度都相等,而实际应用中这是不可能的,所以计算起来会更复杂。
- 没有子元素的时候不应该直接设置宽/高为 0,而是应该根据 LayoutParams 的宽/高来做相应的处理,因为当使用 padding 属性的时候,虽然没有子视图,但 padding 值也会占据一定空间,你可以设置 LinearLayout 子视图个数为 0,然后给定一个 padding 值去试试。
- 在测量 HorizontalScrollViewEx 的高/宽的时候没有考虑它的 padding 值和子视图的 margin 值,因为自己的 padding 值和子视图的 margin 值都是占据空间的。
下面再看一下 onLayout 代码如下:
- /**
- * 重写 onLayout 方法,实现摆放子视图功能
- */
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- //记录子视图左边距位置
- int childLeft = 0;
- //获取子视图个数
- final int childCount = getChildCount();
- //记录子视图个数
- mChildrenSize = childCount;
- //遍历子视图
- for (int i = 0; i < childCount; i++) {
- //获取子视图
- final View childView = getChildAt(i);
- if (childView.getVisibility() != View.GONE) {
- //如果子视图可见,获取子视图测量宽度
- final int childWidth = childView.getMeasuredWidth();
- //记录子视图宽度
- mChildWidth = childWidth;
- //设置摆放子视图位置,每次子视图放置在上一个子视图右边依次排放
- childView.layout(childLeft, 0, childLeft + childWidth,
- childView.getMeasuredHeight());
- childLeft += childWidth;
- }
- }
- }
说明,以上代码不规范之处:以上代码实现了摆放子视图的功能,从代码可以看出放置子视图是从左至右依次摆放。
在摆放子视图的过程中,没有考虑自身的 padding 和子视图的 margin 值。
3. 自定义 View 的总结
到这里,关于 View 的基础知识基本学习完毕,笔者写到这里也完全不能写出了一个牛逼的自定义控件(一个基友曾经这样问我:你学完了这些知识,还不徒手撸出一个牛逼的自定义 View 啊——–阿风,我的回答当然不能。因为自定义 View 是一个综合的知识体系,需要灵活的运用各种知识和经验,这里我们只是学习了一下基础理论知识,知其原理,懂其思路,如果我们想自定义 View,首先要掌握基本功,比如 View 的弹性滑动,滑动冲突,绘制原理等,这些都是自定义 View 所必须知识点,再复杂的自定义 View 也是离不开这些知识点,尤其是那些看起来很炫酷的自定义 View,往往对这些技术点要求更高,只有熟悉掌握这些基础知识点以后,在面对新的自定义 View 时,才能够根据需求情况选择合适的实现思路,实现大体方法就是 4.1 节中介绍的四种分类,另外还需要学习一下 Canvas 这个类的用法才能画出想要的 View 。