最近年底了,打算把自己的Android知识都整理一下。
Android技能树系列:
Android基础知识
Android技能树 — 动画小结
Android技能树 — View小结
Android技能树 — Activity小结
Android技能树 — View事件体系小结
Android技能树 — Android存储路径及IO操作小结
Android技能树 — 多进程相关小结
Android技能树 — Drawable小结
数据结构基础知识
Android技能树 — 数组,链表,散列表基础小结
Android技能树 — 树基础知识小结(一)
算法基础知识
Android技能树 — 排序算法基础小结
这次是相对View做个小结,主要是View的工作原理,绘制流程等。为什么要总结这块,因为平时自定义View的情况多多少少都会遇到,如果能深刻了解这块知识,对自定义View的掌握才能更透彻。有些人可能会说那我肯定不会的,我也不用看这个总结文章了,没关系,我这次写的很简单,基本大家都能理解。看完后,大家应该都会自己写效果不复杂的自定义View和自定义ViewGroup。
PS: 非广告。我本身View的相关知识也是以前从其他地方学到的。我比较推荐这块内容看(Android开发艺术探索 和 扔物线的View相关内容。所以文中有些的知识点也会引用这二块地方。)
如下图所示:我主要是整理了这些相关知识:
View小结
我们可以看大分类:
我们知道一个View要绘制好,是要有三步的(我估计百分之99.9的人都知道这三步): measure测量,layout确定位置,然后draw画出来。所以我这次也是主要这三步来说明的。而大家可能看到这里有一个额外的ViewRoot的知识点,主要是给前面的三步做个补充知识。
ps:不看其实问题也不大,不想了解的直接看本文的主要的measure,layout,draw三步曲。
ViewRoot
字面意思是不是让你感觉是整个ViewTree的根节点。错!ViewRoot不是View,它的实现类是ViewRootImpl
,它是DecorView
和WindowManager
之间的纽带。所以ViewRoot
更恰当来说是DecorView
的“管理者”。
(PS:下次面试官问你ViewRoot是啥,你可别说是ViewTree的根节点。哈哈。)
所以这时候既然开始整个界面要绘制了。明显就是ViewRoot开始发起调用方法,毕竟“管理者”么。所以View的绘制流程是从ViewRoot
的performTraversals
方法开始的。所以performTraversals
方法依次调用performMeasure
,performLayout
和performDraw
三个方法。因为这三个方法及后面的方法调用都差不多,我们以performMeasure
为例,performMeasure
会调用measure
方法,而measure
方法又会调用onMeasure
方法(PS:是不是就发现了为啥我们平时都是重写onMeasure
方法了。),然后又会在onMeasure
方法里面去调用所有子View的measure
过程。
我们可以看到思维脑图中有提到顶级View就是DecorView
,那DecorView
是什么呢? DecorView
是一个FrameLayout,里面包含了一个竖向的LinearLayout
,一般来说这个LinearLayout是有上下二部分(这里具体跟Android SDK和主题有关):
是不是看到了熟悉的Content这个名字,没错。我们在Activity里面设置布局setContentView
就是把我们的布局加到这个id为android.R.id.content
的FrameLayout
里面。
我们现在正式进入View整个绘制流程:
大家可以看到,为了方便大家理解,我写了二个现实生活场景故事对比。
我们可以看到,我们的气球放到柜子里面,决定气球大小的因素有二个:柜子给它的限制,还有它自身的因素(质量好坏,好的能吹的很大)。而我们的View也是一样的,首先我们用MeasureSpec来决定我们的View大小,那我们的MeasureSpec和气球一样,也受到二个因素的影响:
总结起来就是一句话:在测量过程中,系统会将View的LayoutParams根据父容器ViewGroup所施加的规则下,转换得出相对应的MeasureSpec,然后根据这个MeasureSpec来测量出View的高/宽。
可能大家会问什么是MeasureSpec,别急,我们马上就来介绍
其实直接看脑图,应该就能看得懂吧,主要是这么几个知识点:
没错,通过对比,我们可以发现规律原来很简单。因为我们脑子里面可以用这个气球的对比故事更好的理解。
我做一个总结表格:(要理解上面的分析过程,而不是背下这个表格,背下来没啥意思)
通过上面我们已经知道MeasureSpec是用来确定View的测量的,也已经能根据不同的情况来获得相应的MeasureSpec了。那我们的到底应该在哪里去创建MeasureSpec呢?然后给子View去约束呢?
其实奥秘就在我们平时重写的onMeasure()
方法中:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
复制代码
我们是不是看到了onMeasure
方法里面传入了(int widthMeasureSpec, int heightMeasureSpec)
,没错,这里传入的二个参数,就是当前你重写这个方法的所在的View(子View或者ViewGroup)的进行过一系列的操作最后获得的MeasureSpec。
那我们拿到这二个参数后,View还是不知道我们到底给它的宽和高是多少。应该肯定最后是我们调用类型:view.setMeasureWidth(XX),view.setMeasureHeight(XX)
这样,它才能被设置测量的宽和高。没错,setMeasuredDimension(int measuredWidth, int measuredHeight)
方法就是我们用来设置view的测量宽和高。
当然你可能会问,那我如果直接调用这个方法来设置view的宽和高,那我感觉我不用MeasureSpec
都没关系啊。比如下面的代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//没有使用相应的MeasureSpec
setMeasuredDimension(100,100);
}
复制代码
没错,我们可以不是通过正规的测量过程来决定测量的宽和高,我们就是任性的直接定了宽高是100。但是这样就不符合规则流程了。而且做出来的东西也不会特别好。比如这时候,你在xml中对你的view设置match_parent
,wrap_content
,200dp
就会都无效,因为代码最后都是用了100。
我们前面提过,自定义View是要重写onMeasure()方法的,我们再仔细分析下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//我们一般会自己写的代码
........
........
.......
}
复制代码
我们可以看到,主要分为二块:
我们根据不同的情况一步步来看这些代码的作用。
直接继承View.java
super.onMeasure() 分析1 :比如我们的自定义View直接继承了View.java:
public class DemoView extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
}
}
复制代码
我们可以查看super.onMeasure
方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
复制代码
我们看到果然调用了setMeasureDimension方法来进行宽高的设置了。
PS:接下来的源码这个分析可以不看,直接看结论。嘿嘿。嘿嘿。我知道很多人都不想看。
我们可以看到主要是三个方法(我们这里就看width的测量):
1和2的方法先不看,我们起码知道了。我们最终确定一个View的测量大小,是通过setMeasuredDimension来设置的(其实我感觉我说的废话,看这个方法的名字就很明确了)。
我们再回头来看1中的方法:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
复制代码
如果我们的View没有设置background,则返回的最小值为mMinWidth(啥是mMinWidth?????就是我们在xml设置的android:minWidth
的值)。如果我们设置了background,则获取mBackground.getMinimumWidth()
(其实这个方法就是返回Drawable的原始宽度)。最后返回max(mMinWidth, mBackground.getMinimumWidth())
二者中的最大值。
我们再来看2中的方法:
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;
}
复制代码
其实上面我们的MeasureSpec的创建规则会的话,其实应该就能看的懂。如果是specMode是UNSPECIFIED
,则返回我们1中的方法getSuggestedMinimumWidth
获取到的值,如果是AT_MOST
和EXACTLY
,则直接返回specSize。(View源码这里的宽度的创建规则和我们前面讲的测量的规则区别就在于,当specMode是UNSPECIFIED
的时候,返回的是getSuggestedMinimumWidth
的值,而我们是返回了0。)
结论1:如果写的自定义View是直接继承View的,而且写了super.measure(),则会默认给这个View设置了一个测量宽和高(这个宽高是多少?如果没有设置背景,则是xml里面设置的android:minWidth/minHeight(这个属性默认值是0),如果有背景,则取背景Drawable的原始高宽值和android:minWidth/minHeight二者中的较大者。)
继承现有控件
super.onMeasure() 分析2 :比如我们的自定义View继承了现有的控件,比如ImageView.java:
public class Image2View extends ImageView {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
复制代码
这时候我们的super.onMeasure()
方法调用的就是ImageView
里面的onMeasure
方法了:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//ImageView 的一大堆计算宽高的代码。
......
......
......
//当然最终肯定要把算好的宽高告诉View
setMeasuredDimension(widthSize, heightSize);
}
复制代码
我们发现如果我们的View直接继承ImageView,ImageView已经运行了一大堆已经写好的代码测出了相应的宽高。我们可以在它基础上更改即可。
比如我们的Image2View是一个自定义的正方形的ImageView,:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这里已经帮我们测好了ImageView的规则下的宽高,并且通过了setMeasuredDimension方法赋值进去了。
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//我们这里通过getMeasuredWidth/Height放来获取已经赋值过的测量的宽和高
//然后在ImageView帮我们测量好的宽高中,取小的值作为正方形的边。
//然后重新调用setMeasuredDimension赋值进去覆盖ImageView的赋值。
//我们从头到位都没有进行复杂测量的操作,全靠ImageView。哈哈
int width = getMeasuredWidth();
int height = getMeasuredHeight();
if (width < height) {
setMeasuredDimension(width, width);
} else {
setMeasuredDimension(height, height);
}
}
复制代码
结论2:如果写的自定义View是继承现有控件的,而且写了super.measure(),则会默认使用那个现有控件的测量宽高,你可以在这个已经测量好的宽高上做修改,当然也可以全部重新测过再改掉。
自己写的代码与super.measure的前后位置
super.onMeasure() 分析3:我们写的自己的代码与super.measure的前后位置关系
我们可以看到,不管你是继承View还是现有的控件(比如ImageView),super.onMeasure()
中都默认会按照自己的逻辑测量一个宽和高,然后调用setMeasuredDimension()
方法赋值进去。
setMeasuredDimension()
赋值。public class Image2View extends ImageView {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这里的super.onMeasure()方法里面,已经是调用了ImageView的onMeasure()方法。
//所以已经进行了测量了。并且在这个方法最后调用了setMeasuredDimension(widthSize, heightSize);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//所以你不写任何东西,这个测量结果都已经确定过了,因为已经执行过了setMeasuredDimension。
//但比如你想要在ImageView的基础上,让这个ImageView变成一个正方形的ImageView。
//因为测出来的宽高可能不同,是一个矩形。我们就需要手动的再去设置一次宽和高。
int width = getMeasuredWidth();//获取ImageView源码里面已经测量好的宽度
int height = getMaxHeight();//获取ImageView源码里面已经测量好的高度
if (width < height) {
setMeasuredDimension(width, width);
} else {
setMeasuredDimension(height, height);
}
}
}
复制代码
我们发现,我们是在已经我们继承的现有的控件帮我们测量好宽高后,可以再次在这个已经测量好的宽高的基础上进行更改。我们并没有用到我们前面学到的MeasureSpec的知识,因为super.onMeasure()中已经帮我们把MeasureSpec处理好了。
public class CircleView extends View {
public CircleView(Context context) {
super(context);
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//View测量宽高的三步曲
//1.设置默认值,wrap_content的情况下的值。
//因为wrap_content只是说不超过某个最大值,如果不设置默认值,效果与Match_parent一样了。
int defaultWidthSize = 200;
int defaultHeightSize = 200;
//2.调用resolveSize()方法,把MeasureSpec和我们的默认值放进去
//这个方法返回一个最终根据你传入的默认值及MeasureSpec共同作用后的最终结果
defaultWidthSize = resolveSize(defaultWidthSize, widthMeasureSpec);
defaultHeightSize = resolveSize(defaultHeightSize, heightMeasureSpec);
//调用setMeasuredDimension方法赋值宽和高
setMeasuredDimension(defaultWidthSize, defaultHeightSize);
}
}
复制代码
是不是超级超级超级简单。大家可能就会问,那个resolveSize()
方法是什么,怎么这么神奇。
PS:下面的resolveSize()源码分析不看也没啥关系,反正会用就行了。哈哈,不影响使用。
我们可以来看下它的源码:
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
//1.拿到specMode 和 specSize
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
//2.根据不同的specMode来进行判断最终值是什么
switch (specMode) {
case MeasureSpec.AT_MOST:
/*
2.1如果specMode是AT_MOST模式,我们本来应该直接是specSize
但是如果我们的默认值比我们的specSize大就很尴尬了。气球默认的大小都装不进柜子了。这时候我们View的大小要设置成specSize,如果默认大小比我们的specSize小就没关系,直接为默认值。
*/
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
/*
2.2如果是EXACTLY,直接就是specSize
*/
case MeasureSpec.EXACTLY:
result = specSize;
break;
/*
2.3如果是UNSPECIFIED模式,则直接就是我们设的默认值
*/
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
复制代码
在讲ViewGroup的测量前面,我要提问个问题,大家应该知道了某个View的MeasureSpec在是在onMeasure()方法的参数里面传进来的。我们是直接拿来用了。那又是那里调用了onMeasure()方法帮忙把这二个参数带进来的呢。这二个参数又是哪里生成的呢?
答案就是这个子View的父容器给它的。父容器在他自己的onMeasure()方法里面会根据自己的onMeasure()传进来的MeasureSpec,及这个子View的自身的LayoutParams情况,生成相应的childMeasureSpec,然后调用子View的measure()传递进去的(前面提过,measure()方法会调用onMeasure()方法。)
比如我们写一个圆形排布的ViewGroup(LinearLayout是一排的排布)。
public class CircleLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//1.父容器的onMeasure()传进来的二个参数widthMeasureSpec和 heightMeasureSpec
//2.还差子View的LayoutParams,获取子View的LayoutParams
//3.通过二者产生新的MeasureSpec然后给子View。
//4.而产生的新的ChildMeasureSpec的规则就是我们前面表格总结过的规则。
/*
PS:下面这段是我写的代码,并不是正确的,因为父容器可能包含多个子View,
所以到某个子View的时候,给它的specSize应该是父容器的剩余空间,
所以传入的父容器的可用空间本来是不停的减少的,外加还有margin,padding值也要减去。
我就是主要意思下,让大家懂得原理。
*/
//先判断初始时候父容器的大小,因为父容器也是个View,所以也是三步曲。
//设置默认值(可以是0,因为父容器一般默认不会占有空间)
int defaultWidthSize = 500;
int defaultHeightSize = 500;
//resolveSize处理获取宽和高
int resultWidthSize = resolveSize(defaultWidthSize, widthMeasureSpec);
int resultHeightSize = resolveSize(defaultHeightSize, heightMeasureSpec);
//比如我们这里以width为例子:
//我们前面提过了,最终给子View的MeasureSpec是由父View的MeasureSpec与子View的LayoutParam共同确定。
//先获取父View的MeasureSpec的mode和size
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
//根据不同的SpecMode及子View的LayoutParams来产生新的ChildMeasureSpec。
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
LayoutParams params = view.getLayoutParams();
int childWidthSpec, childHeightSpec;
//先根据父View的MeasureSpec来进行大分类:
switch (specMode) {
case MeasureSpec.EXACTLY:
//说明是固定值,比如100dp等
if (params.width >= 0) {
resultSize = params.width;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = specSize;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = specSize;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
.....
.....
break;
case MeasureSpec.UNSPECIFIED:
.....
.....
break;
}
childWidthSpec = MeasureSpec.makeMeasureSpec(resultWidthSize, MeasureSpec.EXACTLY);
getChildAt(i).measure(childWidthSpec, childHeightSpec);
}
/*
可能有人说,生成新的规则我都懂,但是每次都要写上面一大段的代码,
我不想写自定义ViewGroup了。我还是放弃吧,别急,大家也发现上面的规则的确是固定的。
那有没有类似我们在上面设置自己宽高时候的类似resolveSize的方法呢。
如果没有特定的需求,的确我们不需要写上面一大段。
有二种方法。
*/
//方法1:可以通过调用measureChildren()一下子把所有的子View测量好
measureChildren(widthMeasureSpec, heightMeasureSpec);
//方法2:通过measureChild()一个个来测量。
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
measureChild(view , widthMeasureSpec,heightMeasureSpec);
}
//设置父容器的大小
setMeasuredDimension(XXXX,XXXX)
}
}
复制代码
没错,最后我们可以用measureChildren(widthMeasureSpec, heightMeasureSpec);
和measureChild(view , widthMeasureSpec,heightMeasureSpec);
方法来,我们也知道它的内部肯定也是根据相应的规则,生成对应的childMeasureSpec,然后调用child的measure方法。
我们可以看下源码(PS:不想看还是没关系,可以跳过):
//measureChildren其实只是帮我们遍历了所有的View,帮我们把可见的View分别调用measureChild方法来处理。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
//而measureChild方法里面就是获取子View的LayoutParams和传进来的MeasureSpec,
//把这二者通过getChildMeasureSpec方法获得一个新的childMeasureSpec,然后传给child.measure方法。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码
如果具体想看getChildMeasureSpec
做了什么,可有再去看下源码,但是他们生成的规则跟我们前面讲的还是一样的。我这里不多说了。
这个就十分简单了。直接看脑图即可。
这块比较简单,我也不多说了。(别吐槽我,这文章太多了。写太多没人会耐心看完。)
我们都知道View的大小和位置都确定好了,肯定就差绘画了。
我们都知道是通过draw()方法来绘制的。
而draw()方法具体做了什么呢,我们可以看源码这个方法的工作过程的介绍:
draw()源码里面的介绍:
/*
* 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)
*/
复制代码
分别是先绘制背景,然后绘制自己的内容,然后绘制子View的内容,最后画装饰和前景。
推荐大家看扔物线大佬的文章,讲的很清楚,我就不花大篇幅写基础了。
HenCoder Android 自定义 View 1-5: 绘制顺序
我们知道不管是onDraw(Canvas canvas)
,dispatchDraw(Canvas canvas)
,onDrawForeground(Canvas canvas)
等都是参数是Canvas(画布)。所以我们知道了是用Canvas来绘画。
这里也是推荐扔物线大佬的相关文章,讲的很细,我也不再大篇幅的写各种基础使用知识。
HenCoder Android 开发进阶: 自定义 View 1-1 绘制基础
HenCoder Android 开发进阶:自定义 View 1-4 Canvas 对绘制的辅助
Canvas怎么使用呢: 主要分为二大块:
这块很简单,直接用Canvas来画颜色,画矩形,画圆形,画直线等各种图形。虽然简单,但毕竟这才是基本的绘制,用的最多。
其中几何变化又分为二维变换和三维变换:
二维变换
三维变换
我们知道Paint是画笔,我们可以设置颜色,画笔粗细等。
继续推荐扔物线大佬的相关文章(基础我就不写了):
HenCoder Android 开发进阶: 自定义 View 1-2 Paint 详解
有错误的地方,请大家轻点喷,我胆子很小的。。。。