四. view
树的绘制流程
4.1 view
树的绘制流程是谁负责的?
view
树的绘制流程是通过viewroot
去负责绘制的,viewroot
这个类的命名有点坑,最初看到这个名字,翻译过来是view的根节点,
但是事实完全不是这样,viewroot
其实不是View的根节点,它连view
节点都算不上,它的主要作用是View树的管理者,负责将decorview
和phonewindow
“组合”起来,
而View树的根节点严格意义上来说只有decorview
;每个decorview
都有一个viewroot
与之关联,这种关联关系是由windowmanager
去进行管理的;
4.2 view
的添加
记住每添加一次view
都会刷新一次ui,这里面可能出现卡顿问题,之前在做周年庆活动的时候往里flowlayout
添加item
,measure
,layout
, draw
方法会被执行多次。
4.3 measure
话说自定义ui执行的方法中:measure
、layout
和draw
,我们第一个接触方法就是measure
了,今天来就说说measure
执行过程。 那么问题来了:
1.view
为什么要有view
过程 ?
- 因为在
android
中view
有自适应尺寸的机制,在用自适应尺寸来定义view
大小的时候,view
的真实尺寸还不能确定,这时候就需要根据view
的宽高匹配规则,经过计算,得到具体的像素值,view
过程就是干这件事
2.measure
过程都干了点什么事 ?
由于上面提到的自适应尺寸的机制,所以在用自适应尺寸来定义view
大小的时候,view
的真实尺寸还不能确定。但是view
尺寸最终需要映射到屏幕上的像素大小,所以measure
过程就是干这件事,自适应各种尺寸值,
经过计算,得到具体的像素值。measure
过程会遍历整棵view
树,然后依次测量每个view
真实的尺寸。具体是每个viewgroup
会向它内部的每个子view
发送measure
命令,然后由具体子view
的onmeasure()
来测量自己的尺寸。
最后测量的结果保存在view
的mmeasuredwidth
和mmeasuredheight
中,保存的数据单位是像素。
3.对于自适应的尺寸机制,如何合理的测量一棵view
树 ?
系统在遍历完布局文件后,针对布局文件,在内存中生成对应的view
树结构,这个时候,整棵view
树种的所有view
对象,都还没有具体的尺寸,因为measure
过程最终是要确定每个view
打的准确尺寸,也就是准确的像素值。
但是刚开始的时候,view
中layout_width
和layout_height
两个属性的值,都只是自适应的尺寸,也就是match_parent
和wrap_content
,这两个值在系统中为负数,所以系统不会把它们当成具体的尺寸值。
所以当一个view
需要把它内部的match_parent
或者wrap_content
转换成具体的像素值的时候,他需要知道两个信息。
- a. 针对于match_parent,父布局当前具体像素值是多少,因为match_parent就是子View想要和父布局一样大。
- b. 针对wrap_content,子View需要根据当前自己内部的content,算出一个合理的能包裹所有内容的最小值。但是如果这个最小值比当前父布局还大,那不行,父布局会告诉你,我只有这么大,你也不应该超过这个尺寸。
- 也就是说,在measure过程中,ViewGroup会根据自己当前的状况,结合子View的尺寸数据,进行一个综合评定,然后把相关信息告诉子View,然后子View在onMeasure自己的时候,
一边需要考虑到自己的content大小,一边还要考虑的父布局的限制信息,然后综合评定,测量出一个最优的结果
- 4.那么
viewgroup
是如何向子view
传递限制信息的 ?
谈到传递限制信息,那就是measurespec
类了,该类贯穿于整个measure
过程,用来传递父布局对子view
尺寸测量的约束信息。简单来说,该类就保存两类数据。
- 1.子
view
当前所在父布局的具体尺寸。 - 2.父布局对子
view
的限制类型。 那么限制类型又分为三种类型:
- 1.UNSPECIFIED,不限定。
- 子
view
想要多大,我就可以给你多大,你放心大胆的measure
吧,不用管其他的。也不用管我传递给你的尺寸值。(其实Android高版本中推荐,只要是这个模式,尺寸设置为0)
- 2.EXACTLY,精确的。
- 根据我当前的状况,结合你指定的尺寸参数来考虑,你就应该是这个尺寸,具体大小在
MeasureSpec
的尺寸属性中,自己去查看吧,你也不要管你的content
有多大了,就用这个尺寸吧。
- 3.AT_MOST,最多的。
- 根据我当前的情况,结合你指定的尺寸参数来考虑,在不超过我给你限定的尺寸的前提下,你测量一个恰好能包裹你内容的尺寸就可以了。
3.scrollview
嵌套listview
问题 ?
只存在滑动卡顿这一问题,可以采用如下两种简单方式快速解决,利用RecyclerView
内部方法
rV.setHasFixedSize(true); rV.setNestedScrollingEnabled(false);
4.4 layout
- 1.系统为什么要有
layout
过程?
view
框架在经过第一步的measure
过程后,成功计算了每一个view
的尺寸。但是要成功的把view
绘制到屏幕上,只有view
的尺寸还不行,还需要准确的知道该view
应该被绘制到什么位置。除此之外,对一个viewgroup
而言,还需要根据自己特定的layout
规则,来正确的计算出子view
的绘制位置,已达到正确的layout
目的。这也就是layout
过程的职责。- 该位置是
view
相对于父布局坐标系的相对位置,而不是以屏幕坐标系为准的绝对位置。这样更容易保持树型结构的递归性和内部自治性。而view
的位置,可以无限大,超出当前viewgroup
的可视范围,这也是通过改变view
位置而实现滑动效果的原理。
2.layout
过程都干了点什么事?!
- 由于
view
是以树结构进行存储,所以典型的数据操作就是递归操作,所以,view
框架中,采用了内部自治的layout
过程。 - 每个叶子节点根据父节点传递过来的位置信息,设置自己的位置数据,每个非叶子节点,除了负责根据父节点传递过来的位置信息,设置自己的位置数据外(如果有父节点的话),还需要根据自己内部的
layout
规则(比如垂直排布等),计算出每一个子节点的位置信息,然后向子节点传递layout
过程。 - 对于
viewgroup
,除了根据自己的parent
传递的位置信息,来设置自己的位置之外,还需要根据自己的layout
规则,为每一个子view
计算出准确的位置(相对于子view
的父布局的位置)。
对于view
,根据自己的parent
传递的位置信息,来设置自己的位置。view
对象的位置信息,在内部是以4个成员变量的保存的,分别是mLeft
、mRight
、mTop
、mBottom
。他们的含义如图所示
4.5 draw
- 1.系统为什么要有
draw
过程?
- View框架在经过了
measure
过程和layout
过程之后,就已经确定了每一个view
的尺寸和位置。那么接下来,也是一个重要的过程,就是draw
过程,draw
过程是用来绘制view
的过程。它的作用就是使用graphic
框架提供的各种绘制功能,绘制出当前view
想要的样子。
2.draw
过程都干了点什么事?
View
框架中,draw
过程主要是绘制View
的外观。ViewGroup
除了负责绘制自己之外,还需要负责绘制所有的子View
。而不含子View
的View
对象,就负责绘制自己就可以了。draw
过程的主要流程如下:
- 1.绘制
backgroud(drawBackground)
- 2.如果需要的话,保存
canvas
的layer
,来准备fading
(不是必要的步骤) - 3.绘制
view
的content
(onDraw
方法) - 4.绘制
children
(dispatchDraw
方法) - 5.如果需要的话,绘制
fading edges
,然后还原layer
(不是必要的步骤) - 6.绘制装饰器、比如
scrollBar
(onDrawForeground
)
五. layoutparams
5.1 marginlayoutparams
MarginLayoutParams
是和外间距有关的。事实也确实如此,和LayoutParams
相比,MarginLayoutParams
只是增加了对上下左右外间距的支持。
实际上大部分LayoutParams
的实现类都是继承自MarginLayoutParams
,因为基本所有的父容器都是支持子View
设置外间距的
- 属性优先级问题
MarginLayoutParams
主要就是增加了上下左右4种外间距。在构造方法中,先是获取了margin属性;如果该值不合法,就获取horizontalMargin
;
如果该值不合法,再去获取leftMargin
和rightMargin
属性(verticalMargin
、topMargin
和bottomMargin
同理)。我们可以据此总结出这几种属性的优先级
margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin
- 属性覆盖问题
优先级更高的属性会覆盖掉优先级较低的属性。此外,还要注意一下这几种属性上的注释
Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
5.2 layoutparams
与view
如何建立联系
- 在XML中定义View
- 在Java代码中直接生成View对应的实例对象
5.3 addview
/** * 重载方法1:添加一个子View * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams */ public void addView(View child) { addView(child, -1); } /** * 重载方法2:在指定位置添加一个子View * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams * @param index View将在ViewGroup中被添加的位置(-1代表添加到末尾) */ public void addView(View child, int index) { if (child == null) { throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); } LayoutParams params = child.getLayoutParams(); if (params == null) { params = generateDefaultLayoutParams();// 生成当前ViewGroup默认的LayoutParams if (params == null) { throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null"); } } addView(child, index, params); } /** * 重载方法3:添加一个子View * 使用当前ViewGroup默认的LayoutParams,并以传入参数作为LayoutParams的width和height */ public void addView(View child, int width, int height) { final LayoutParams params = generateDefaultLayoutParams(); // 生成当前ViewGroup默认的LayoutParams params.width = width; params.height = height; addView(child, -1, params); } /** * 重载方法4:添加一个子View,并使用传入的LayoutParams */ @Override public void addView(View child, LayoutParams params) { addView(child, -1, params); } /** * 重载方法4:在指定位置添加一个子View,并使用传入的LayoutParams */ public void addView(View child, int index, LayoutParams params) { if (child == null) { throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup"); } // addViewInner() will call child.requestLayout() when setting the new LayoutParams // therefore, we call requestLayout() on ourselves before, so that the child's request // will be blocked at our level requestLayout(); invalidate(true); addViewInner(child, index, params, false); } private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) { ..... if (mTransition != null) { mTransition.addChild(this, child); } if (!checkLayoutParams(params)) { // ① 检查传入的LayoutParams是否合法 params = generateLayoutParams(params); // 如果传入的LayoutParams不合法,将进行转化操作 } if (preventRequestLayout) { // ② 是否需要阻止重新执行布局流程 child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout->onDraw) } else { child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout->onDraw) } if (index < 0) { index = mChildrenCount; } addInArray(child, index); // tell our children if (preventRequestLayout) { child.assignParent(this); } else { child.mParent = this; } ..... }
5.4 自定义layoutparams
- 1. 创建自定义属性
<resources> <declare-styleable name="xxxViewGroup_Layout"> <!-- 自定义的属性 --> <attr name="layout_simple_attr" format="integer"/> <!-- 使用系统预置的属性 --> <attr name="android:layout_gravity"/> </declare-styleable> </resources>
- 2. 继承MarginLayout
public static class LayoutParams extends ViewGroup.MarginLayoutParams { public int simpleAttr; public int gravity; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); // 解析布局属性 TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout); simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0); gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1); typedArray.recycle();//释放资源 } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(MarginLayoutParams source) { super(source); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } }
- 3. 重写ViewGroup中几个与LayoutParams相关的方法
// 检查LayoutParams是否合法 @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof SimpleViewGroup.LayoutParams; } // 生成默认的LayoutParams @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } // 对传入的LayoutParams进行转化 @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new SimpleViewGroup.LayoutParams(p); } // 对传入的LayoutParams进行转化 @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new SimpleViewGroup.LayoutParams(getContext(), attrs); }
5.5 layoutParams
常见的子类
在为View
设置LayoutParams
的时候需要根据它的父容器选择对应的LayoutParams
,否则结果可能与预期不一致,这里简单罗列一些常见的LayoutParams
子类:
ViewGroup.MarginLayoutParams
FrameLayout.LayoutParams
LinearLayout.LayoutParams
RelativeLayout.LayoutParams
RecyclerView.LayoutParams
GridLayoutManager.LayoutParams
StaggeredGridLayoutManager.LayoutParams
ViewPager.LayoutParams
WindowManager.LayoutParams
六. measurespec
6.1 measurespec
定义
测量规格,封装了父容器对 view 的布局上的限制,内部提供了宽高的信息( SpecMode 、 SpecSize ),SpecSize是指在某种SpecMode下的参考尺寸,其中SpecMode 有如下三种:
- UNSPECIFIED
父控件不对你有任何限制,你想要多大给你多大,想上天就上天。这种情况一般用于系统内部,表示一种测量状态。(这个模式主要用于系统内部多次Measure的情形,并不是真的说你想要多大最后就真有多大) - EXACTLY
父控件已经知道你所需的精确大小,你的最终大小应该就是这么大。 - AT_MOST
你的大小不能大于父控件给你指定的size,但具体是多少,得看你自己的实现。
6.2 measurespec
的意义
通过将 SpecMode 和 SpecSize 打包成一个 int 值可以避免过多的对象内存分配,为了方便操作,其提供了打包 / 解包方法
6.3 measurespec
值的确定
MeasureSpec值到底是如何计算得来的呢?
measurespe子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里
/** * * 目标是将父控件的测量规格和child view的布局参数LayoutParams相结合,得到一个 * 最可能符合条件的child view的测量规格。 * @param spec 父控件的测量规格 * @param padding 父控件里已经占用的大小 * @param childDimension child view布局LayoutParams里的尺寸 * @return child view 的测量规格 */ 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) { // 当父控件的测量模式 是 精确模式,也就是有精确的尺寸了 case MeasureSpec.EXACTLY: //如果child的布局参数有固定值,比如"layout_width" = "100dp" //那么显然child的测量规格也可以确定下来了,测量大小就是100dp,测量模式也是EXACTLY if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } //如果child的布局参数是"match_parent",也就是想要占满父控件 //而此时父控件是精确模式,也就是能确定自己的尺寸了,那child也能确定自己大小了 else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.EXACTLY; } //如果child的布局参数是"wrap_content",也就是想要根据自己的逻辑决定自己大小, //比如TextView根据设置的字符串大小来决定自己的大小 //那就自己决定呗,不过你的大小肯定不能大于父控件的大小嘛 //所以测量模式就是AT_MOST,测量大小就是父控件的size else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 当父控件的测量模式 是 最大模式,也就是说父控件自己还不知道自己的尺寸,但是大小不能超过size case MeasureSpec.AT_MOST: //同样的,既然child能确定自己大小,尽管父控件自己还不知道自己大小,也优先满足孩子的需求 if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } //child想要和父控件一样大,但父控件自己也不确定自己大小,所以child也无法确定自己大小 //但同样的,child的尺寸上限也是父控件的尺寸上限size else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } //child想要根据自己逻辑决定大小,那就自己决定呗 else if (childDimension == LayoutParams.WRAP_CONTENT) { 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 = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
针对下表,这里再做一下具体的说明
- 对于应用层
View
,其MeasureSpec
由父容器的MeasureSpec
和自身的LayoutParams
来共同决定 对于不同的父容器和view
本身不同的LayoutParams
,view
就可以有多种MeasureSpec
。
- 当
view
采用固定宽高的时候,不管父容器的MeasureSpec
是什么
view
的MeasureSpec
都是精确模式并且其大小遵循Layoutparams
中的大小;
- 当
view
的宽高是match_parent
时,这个时候如果父容器的模式是精准模式,
- 那么
view
也是精准模式并且其大小是父容器的剩余空间,如果父容器是最大模式, - 那么view也是最大模式并且其大小不会超过父容器的剩余空间;
- 当
view
的宽高是wrap_content
时,不管父容器的模式是精准还是最大化
- view的模式总是最大化并且大小不能超过父容器的剩余空间。
Unspecified
模式,这个模式主要用于系统内部多次measure
的情况下
- 一般来说,我们不需要关注此模式 (这里注意自定义
View
放到ScrollView
的情况 需要处理) 。
七. 自定义UI
原因
android
系统内置view
无法实现我们的需求- 处于性能考虑