最近年末了,打算把本身的Android知识都整理一下。java
Android技能树系列:android
Android基础知识git
Android技能树 — 动画小结github
Android技能树 — View事件体系小结canvas
Android技能树 — Android存储路径及IO操做小结数组
数据结构基础知识
算法基础知识
此次是相对View作个小结,主要是View的工做原理,绘制流程等。为何要总结这块,由于平时自定义View的状况多多少少都会遇到,若是能深入了解这块知识,对自定义View的掌握才能更透彻。有些人可能会说那我确定不会的,我也不用看这个总结文章了,不要紧,我此次写的很简单,基本你们都能理解。看完后,你们应该都会本身写效果不复杂的自定义View和自定义ViewGroup。
PS: 非广告。我自己View的相关知识也是之前从其余地方学到的。我比较推荐这块内容看(Android开发艺术探索 和 扔物线的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);
//咱们通常会本身写的代码
........
........
.......
}
复制代码
咱们能够看到,主要分为二块:
咱们根据不一样的状况一步步来看这些代码的做用。
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.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怎么使用呢: 主要分为二大块:
其中几何变化又分为二维变换和三维变换:
咱们知道Paint是画笔,咱们能够设置颜色,画笔粗细等。
继续推荐扔物线大佬的相关文章(基础我就不写了):
HenCoder Android 开发进阶: 自定义 View 1-2 Paint 详解
有错误的地方,请你们轻点喷,我胆子很小的。。。。