对于Android View绘制的一些思考
Posted 行板如歌
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对于Android View绘制的一些思考相关的知识,希望对你有一定的参考价值。
AT_MOST
表示最大是多大.
UNSPECIFIED
不确定是多大, 你想多大就多大,我尽量满足你.
EXACTLY
就这么大 已经指定了大小
MATCH_PARENT 为什么说MATCH_PARENT 因为 MATCH_PARENT 就是间接说 我要占据父控件剩下的那部分了。
这就相当于指定了确定的宽或高。
一个view 的绘制需要三个阶段 measure -> layout -> draw
view的测量阶段
measure-> onMeasure() 我们就看单纯一个view的测量 测量自身的大小
它需要有两个参数
widthMeasureSpec
heightMeasureSpec
这两个参数 是由他们所在的父容器传给他们的,父控件是要传递多大呢? 这是一个给view自己测量时候的
参考值,肯定不能乱给。我的问题是 父容器会传多少?为什么会传递这么大?先记住这个问题(1)
在这里面我需要调用下面的方法,来设置测量的值,调用这个方法,就表示该view 在测量这一阶段完成了。
setMeasuredDimension(width, height)
注意在基类 View onMeasure() 方法调用是这样的:
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
这里面 View 是一个通俗的实现,就是默认值,意思要么使用父控件指定的( xxxMeasureSpec),要么自己测量(getSuggestedMininumxxx)。
然后,我们就看了一下getDefaultSize的源码,看一下是如何选择选择,这个源码很简单。
/** * Utility to return a default size. Uses the supplied size if the * MeasureSpec imposed no constraints. Will get larger if allowed * by the MeasureSpec. * * @param size Default size for this view * @param measureSpec Constraints imposed by the parent * @return The size this view should be. */ 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: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
从getDefaultSize() 的实现。 我们可以得到这么个结论。
除了unspecified 模式之外 其他模式(exactly, at_most)都用了父容器传递过来的值。也就是父容器指定的值。原来
父容器给的参考值这么重要。
话又说回来 什么叫父指定的值,为什么要指定值,自己测量自己不就行了么?那么多事.这个问题也很好理解:
同志们都知道 view(比如 常用的TextView等) 一般情况下 在xml 文件中定义中view 必然是有父布局的
并且view的宽高也是必须指定(wrap_content、
match_parent 、固定值) 不指定的话编译都过不了。而传递的参考值就是在这里设定的值。于是,对于问题1
我基本是想通了。so 现在想一下,我们是如何动态创建一个view,并将其放到一个布局里面的,代码如下
LinearLayout ll = (LinearLayout)findViewById(R.id.ll); TextView text_view = new TextView(this); ll.add(text_view); TextView text_view = new TextView(this); text_view.setText("计划好了再娶吧"); text_view.setBackgroundColor(Color.RED); ll.addView(text_view);
此时你看到了所显示的。不要激动!
注意:对于LinearLayout 来说如果是VERTICAL 那么view默认生成的 LayoutParams 宽度是match_parent.
这里为什么要说下动态生成与添加view,在这里我就是想说这个LayoutParams,我们可以为我们的
view 设置LayoutParams 也可以用系统默认的,系统默认的就是上层父控件默认的,这里不设置就是用我们LinearLayout 默认给子view生成的。
不管怎么实现,我们的父控件 会拿到了我们的LayoutParams 里面设置的各种参数。
然后 呢 ?--requestLayout 继续三个过程,并且是在父控件(我们的LinearLayout)的3个过程measure -> layout -> draw
于是我们就可以 开始 measure ,但是父控件调用measure 必然是为了测量自身,这是必须明白的。 measure->onMeasure
真正的测量在onMeasure中开始了。在这里面会测量每一个子view 的大小。
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)
必须得看一下源码(因为不是太长)
/** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding * and margins. The child must have MarginLayoutParams The heavy lifting is * done in getChildMeasureSpec. * * @param child The child to measure * @param parentWidthMeasureSpec The width requirements for this view * @param widthUsed Extra space that has been used up by the parent * horizontally (possibly by other children of the parent) * @param parentHeightMeasureSpec The height requirements for this view * @param heightUsed Extra space that has been used up by the parent * vertically (possibly by other children of the parent) */ 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 的MarginLayoutParams
2、确定子view 长宽的参考值,调用getChildMeasureSpec(...)
3、调用子view 的 measure 并传入参考值 child.measure(childWidthMeasureSpec, childHeightMeasureSpec) 进入子view测量阶段
看一下getChildMeasureSpec(...)的源码(稍微有点长,但是人的好奇心促使我们继续前行,因为我们必须知道给子view的参考值是怎么来的,注释好好看,我都加粗了):
/** * Does the hard part of measureChildren: figuring out the MeasureSpec to * pass to a particular child. This method figures out the right MeasureSpec * for one dimension (height or width) of one child view. * * The goal is to combine information from our MeasureSpec with the * LayoutParams of the child to get the best possible results. For example, * if the this view knows its size (because its MeasureSpec has a mode of * EXACTLY), and the child has indicated in its LayoutParams that it wants * to be the same size as the parent, the parent should ask the child to * layout given an exact size. * * @param spec The requirements for this view * @param padding The padding of this view for the current dimension and * margins, if applicable * @param childDimension How big the child wants to be in the current * dimension * @return a MeasureSpec integer for the child */ 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) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { 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; } 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); }
public static int getChildMeasureSpec(int spec, int padding, int childDimension)
spec: 父控件的测量值,主要是为得到 父控件相关的规格(mode)
padding : 父控件中已经被使用的部分
childDimension 我们通过LayoutParams 设定的值(match_parent、wrap_content 或者固定值) 注意仅仅是尺寸
这里并没有包括相关的mode值,因我就是为了得到子view 的mode值
1、计算剩余空间 int size = Math.max(0, specSize - padding);
2、拿到父控件的 specMode 与 specSize
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
对于 接下来代码,就是通过父控件的specMode 和 子view 自身的LayoutParams值来确定 子view大小的参考值和规格
但是源码中的几句英文注释,对于我们来说理解分析at_most exactly unspecified 不同有很大关系。
我理解是这样的:
每一种mode都有 一个size 与之对应 mode 就是说明了size这个参数表示的意义
at_most at_most this size
exactly exactly this size
unspecified unspecified this size
现在我们测量完成之后 每一个 子view 的大小,得到所需要参考值(mode + size)。
然后测量完所有子view之后,我们就可以调用 child.getMeasuredWidth() 方法得到每一个子view宽度。
通过计算各个子view 的宽度,和自身的padding 我们就得到了我们父容器的测量值。于是我们就可以大胆的
setMeasuredDimension 来设置自己(LinearLayout)的测量的高度。但这仅仅是测量大小,还有layout 呢!
layout 是如何开始的肯定是从layout(),在这里面如果发现位置发生了变化,那么就会onLayout()方法
并且会通知所有注册 OnLayoutChangeListener 的类,又是观察模式。来到了onLayout() 发现是空的,
当然是空的。这是给有子view 的来用的啊。于是看下ViewGroup 中源码的实现,不过还是抽象方法。因为
这需要具体的布局来自己实现。于是选择LinearLayout来看onLayout的具体实现。分析留到下一次~
题外话:
比如 对于一个TextView TextView.measure(0, 0); 会知道TextView 的长宽。
TextView 在测量的时候(测量很复杂,因为是自身的测量),是一定会参考一下父控件传过来的值,并且
只有mode为exactly 或者 at_most 时候才会有所参考,否则就是,按照自己实际情况测量的结果。因为
传递时俩0,那么mode 就是unspecified 因此就会自己测量自己的大小。也就是最小宽度和高度。最小高度
和最小宽度。可以在xml中 android:minHeight 与 android:minWidth 来设置。
不设置就用背景的宽度和高度。
以上是关于对于Android View绘制的一些思考的主要内容,如果未能解决你的问题,请参考以下文章