android 自定义View之ViewRoot到三大方法
写博客就好像吃甜品,明知道发胖还忍不住,胖并快乐着。
我写东西基本就是在各种总结别人的文章和自己的理解,所以可能不是很专业。
在事件分发篇,我们扯到了window和DecorView ,这次我们引入一个ViewRoot。
其实这块还是很有意思的,后面我们讲handle这块还能和这里有关。
ViewRoot
ViewRoot 或者说对应的 ViewRootImlp 类,它其实是连接 WindowManger 和 DecorView 的桥梁。View 的三大流程都是通过 ViewRoot 来完成的。
Activity创建完毕后,会讲DecorView添加到window,同时创建了ViewRootImlp,将它和DecorView绑定。
View的绘制是从ViewRootImlp的performTraversals开始的,经过measure.layout.draw三方法最终将一个View绘制出来。measure测量View宽高。layout确定View位置,draw负责绘制。
从上图我们能看到,performTraversals调用的时候,调用了performMeasure.performLayout.performDraw 三方法,然后调用最顶级View的Measure,Layout,Draw,顶级View的Measure调用onMersure对子view进行测量,子view对自己子view进行一样的操作。Layout,Draw也是一样,遍历进行处理,不过performDraw的传递是在draw方法种用了dispatchDeaw完成的,不过没啥区别。
这里用我们自己的话总结,其实就是ViewRootImlp自己的测量,布局,绘制的方法掉用顶级view的测量,布局,绘制的方法,顶级view又对自己下面子view进行了递归的测量,布局和绘制。ViewRootImlp三方法跑完,view就有了自己的宽高和位置,才能绘制完毕显示到页面上。
到现在为止 我们大体上了解了,Activity有一个window,收到事件以后,window持有DecorView,DecorView在被添加到window到时候,会产生一个ViewRootImlp,ViewRootImlp在调用performTraversals(遍历表演)的时候会调用顶级view的测量,布局和绘制,然后顶级view会挨个调用自己子布局的测量布局和绘制,这样一个页面就展现出来了。
DecorView
DecorView 继承自 FrameLayout,是一个 ViewGroup。在整个 ViewTree 中, DecorView 是整个 ViewTree 的顶层 View。View 的所有事件,都先经过 DecorView,然后再传递给 View。
DecorView 在一般情况下,内部会包含一个竖直的 LInearLayout,里面有上下两部分,上面是标题栏,下面是内容,内容布局有一个默认的 id: content。
我们在 Activity 中 通过 setContentView() 方法设置的布局,其实就是添加到了内容部分里。如果我们想获取到内容布局的话,可以通过如下方法获取:
ViewGroup content = (ViewGroup) findViewById(android.R.id.content)
想获取到我们设置的 View 的话,可以通过如下方式获取: View childAt = content.getChildAt(0);
顶级View调用三大方法其实也是调用了View自带的onMeasure,onLayout和onDraw。
MeasureSpec
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
其实我一一直好奇MeasureSpec到底是个啥?
MeasureSpec其实是一个32位数int值, 高二位指的是SpeceMode,低30位指的是SpeceSize。
SpeceMode就是测量模式,这玩意就是我们自定义view的时候,想知道是很准却的多少dp?还是我们写的match_parent之类的。android 将SpeceMode和SpeceSize和在了一起。
android 里面SpeceMode有三种模式
- UNSPECIFIED(未指定):父元素不对子元素施加任何束缚,子元素可以得到任意想要的大小;
- EXACTLY(完全):父元素决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身的大小;
- AT_MOST(最多):子元素至最多达到指定大小的值。
MeasureSpec和LayoutParamas有什么关系呢
对于顶级的DecorView,它的MeasureSpec是由窗口尺寸和自身的LayoutParamas决定大小。
普通View是由父容器的MeasureSpec和自身的LayoutParamas决定。
这块其实有点晕,可以这么理解,MeasureSpec决定了控件的大小,DecorView是由窗口决定的和自己大小决定,别人限制不了,但是普通View哪怕写了match也得看父容器给不给你这么大。
DecorView
普通View
当普通View是精确模式的时候,不管父的MeasureSpec是啥,View就是那么大。
当普通View是match模式时候,父容器是精确模式,那么view大小就是剩下的大小。
如果父容器也是match模式,那么view也是最大模式,但是最大不能超过父容器剩余空间。
当view是warp模式的时候,不管父容器是什么模式,view总是最大模式,但是不能超过父容器剩下的空间。
UNSPECIFIED 这个模式一般用于在Measure过程中一般不需要理睬。
总结一波就是精确模式,我多大就是多大。 match模式,最大不能超过父容器剩余空间,模式和父容器一样。warp模式,最大化,不能超过父容器。
onMeasure
该方法是一个 finall 方法,所以不能被重写。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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;
}
从上述代码上可以看到,关于我们关心的 AT_MOST 和 EXACTLY 测量模式,其实 getDefaultSize() 方法返回的就是 MeasureSpec 的 specSize。
到这里也就理解了,为什么当我们在布局中写 wrap_content,如果不重写 onMeasure() 方法,则默认大小是父控件的可用大小了。 当我们在布局中写 wrap_content 时,那么测量模式就是: AT_MOST,在该模式下,它的宽高等于 specSize。而 specSize 由 ViewGroup 传递过来时就是 parentSize,也就是父控件的可用大小。 当我们在布局中写 match_parent 时,那么不用多说,宽高当然也是 parentSize。这时候,我们只需对 AT_MOST 测量模式进行处理。
其实我们再自定义一个view的时候,其实如果父布局没有固定宽高时候,我们希望我们的控件wrap_content模式可以有个固定宽高,不然我们的view永远是最大模式。
如果父控件传递给的MeasureSpec的mode是MeasureSpec.UNSPECIFIED,就说明,父控件对自己没有任何限制,那么尺寸就选择自己需要的尺寸size
如果父控件传递给的MeasureSpec的mode是MeasureSpec.EXACTLY,就说明父控件有明确的要求,希望自己能用measureSpec中的尺寸,这时就推荐使用MeasureSpec.getSize(measureSpec)
如果父控件传递给的MeasureSpec的mode是MeasureSpec.AT_MOST,就说明父控件希望自己不要超出MeasureSpec.getSize(measureSpec),如果超出了,就选择MeasureSpec.getSize(measureSpec),否则用自己想要的尺寸就行了。
public class TestView extends View {
private static final String TAG = "TestView";
public TestView(Context context) {
super(context);
}
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 300;//默认值300
int height=300;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
switch (widthMode) {
case MeasureSpec.AT_MOST://当得不到精确值的时候就300
width = 300;
break;
case MeasureSpec.EXACTLY://精确模式采用系统返回的数值
width = MeasureSpec.getSize(widthMeasureSpec);
break;
case MeasureSpec.UNSPECIFIED:
break;
}
switch (heightMode) {
case MeasureSpec.AT_MOST:
height = 300;
break;
case MeasureSpec.EXACTLY:
height = MeasureSpec.getSize(heightMeasureSpec);
break;
case MeasureSpec.UNSPECIFIED:
break;
}
//设置
setMeasuredDimension(width, height);
}
}
setMeasuredDimension(width, height);
都为wrap_content的时候,父布局和我们的View一样大
父布局固定的时候,我们按照自己默认大小。
父布局固定的时候,我们match
父布局固定,我们固定的时候
父布局和我们都match的时候
在onMeasure方法里我们主要去处理自定义View的宽高。
ViewGropu的measure
对于ViewGropu来说,完成除了自己的measure过程外,会遍历子元素的measure过程,ViewGropu是一个抽象类,没重写View的onMeasure方法,但是它提供了一个meausreChildren的方法。
android艺术探索
取出子元素的LayoutParams,通过getChildeMeasureSpec创建子元素的MeasureSpce,传递给View去测量。
View的具体宽高在页面的onCreat,onMeasure,onStart方法都是不能准确获取的,因为View的measure不能保证和生命周期同步。
想测量准确,第一个方法onWindowFocusChanged,页面获得焦点失去焦点的时候都会被调用
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
int width = text.getWidth();
}
第二个方法是View.post,发送一个runnable到消息队列尾部。
text.post(new Runnable() {
@Override
public void run() {
int width = text.getWidth();
}
});
第三个是ViewTreeObserver,但是可能会被调用多次。
ViewTreeObserver viewTreeObserver = text.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int width = text.getWidth();
}
});
第四种是View.measure,比较麻烦不建议使用。
onLayout
layout可以说是三大方法里面最简单的一个了,layout会确定View自己的位置,onLayout会确定子元素的位置。
onLayout
上面代码我们可以看出来,测量完毕我们获得上下左右四个定点位置,如果不是march,那么就设置进去上下左右四个点位置,然后掉onLayout方法,让父布局知道自己位置。
draw
View的draw可以说是最简单,也是最复杂的一个,简单是因为遵循如下几步
- 画背景 backGround.draw(canvas)
- 画自己(onDraw)
- 画children(dispatchDraw)
- 画装饰 (onDrawScrollBars)
dispatchDraw会调用遍历子元素的onDraw方法。
ViewGroup 有个方法 setWillNotDraw,这个方法标示 你确定不需要绘制任何内容可以设置为
true,系统会进行优化,但是需要绘制的时候,你必须手动关闭它。当然该标示默认为false的。
我们讲draw方法最简单也最麻烦,是因为,很多很好看的控件都需要很多绘制,所以我们接下来会讲绘制的几大常用类,以及自定义View的一些技巧方法。这篇就到这里。