《Android开发艺术探索》读书笔记 (4) 第4章 View的工作原理
本节和《Android群英传》中的第3章Android控件架构与自定义控件详解有关系,建议先阅读该章的总结
除了掌握View的三大流程外,View常见的回调方法也 需要熟练掌握的,比如构造方法,onTttach、onVisibilityChanged、onDetach等,自定义View的类型,有直接继承自View和ViewGroup,有的则选择继承现有的系统控件
4.1 初始ViewRoot和DecorView
(1)ViewRoot
对应ViewRootImpl
类,它是连接WindowManager
和DecorView
的纽带,View的三大流程均通过ViewRoot来完成。
(2)ActivityThread
中,Activity创建完成后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并建立两者的关联。
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);
(3)View的绘制流程从ViewRoot的
performTraversals
方法开始,经过
measure
、
layout
和
draw
三大流程。
(4)preformTranversals会依次调用performMeasure、performLayout和preformDraw三个方法,这三个方法分别完成顶级View的measure、layout、和draw这三大流程、
performMeasure
方法中会调用
measure
方法,在
measure
方法中又会调用
onMeasure
方法,在onMeasure方法中会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素了,这样就完成了一次measure过程,layout和draw的过程类似。 (书中175页画出详细的图示)
(5)measure过程决定了view的宽高,在几乎所有的情况下这个宽高都等同于view最终的宽高。layout过程决定了view的四个顶点的坐标和view实际的宽高,通过getWidth
和getHeight
方法可以得到最终的宽高。draw过程决定了view的显示。
(6)DecorView其实是一个FrameLayout,其中包含了一个竖直方向的LinearLayout,上面是标题栏,下面是内容栏(id为android.R.id.content
)
4.2 理解MeasureSpec
(1)MeasureSpec
和LayoutParams
的对应关系
在view测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高,这里面的宽/高是测量宽/高,不一定等于View的最终宽/高。
http://blog.csdn.net/lmj623565791/article/details/24252901
headerView.measure(0, 0); // 系统会帮我们测量出headerView的高度
headerViewHeight = headerView.getMeasuredHeight();
(2)普通view的MeasureSpec的创建规则 (书中182页列出详细的表格)
当view采用固定宽高时,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精确模式,并且大小是LayoutParams中的大小。
当view的宽高是match_parent
时,如果父容器的模式是精确模式,那么view也是精确模式,并且大小是父容器的剩余空间;如果父容器是最大模式,那么view也是最大模式,并且大小是不会超过父容器的剩余空间。
当view的宽高是wrap_content
时,不管父容器的模式是精确模式还是最大模式,view的模式总是最大模式,并且大小不超过父容器的剩余空间。
4.3 view的工作流程
如果只是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成了自己的测量过程外,还需要遍历去调用所有子元素的measure方法
1、view测量measure过程
View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法中会去调用View的onMeasure方法,因此只需要看onMeasure的实现即可,我们得出如下结论:直接继承View的自定义控件需要重写onMeasure方法,并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent,如果是使用wrap_content,那么它的specMode是AT_MOST模式,这里面的代码有写wrap_content的时候怎么测量大小http://blog.csdn.net/lmj623565791/article/details/24252901
2、ViewGroup的measure过程
Viewgroup在measure时,会对每一个子元素进行measure,measureChile这个方法,需要注意的是,在某些极端情况,系统可能需要多次measure才能确定最终的测量宽/高,在这种情形下,在onMeasure方法中拿到的测量宽/高很可能不准确的,一个比较好的习惯是在onLayout方法中去获取View的测量的宽/高或者最终宽/高。
如果我们要在activity的onCreate中获取View的宽/高,如果直接获取就可能是0,下面给出四种方法来解决这个问题:
(1)view的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate
、onStart
、onResume
时某个view已经测量完毕了。如果view还没有测量完毕,那么获得的宽高就都是0。下面是四种解决该问题的方法:
1.Activity/View # onWindowFocusChanged
方法
onWindowFocusChanged
方法表示view已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没问题的。这个方法会被调用多次,当Activity继续执行或者暂停执行的时候,这个方法都会被调用,典型的代码如下;
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if(hasWindowFocus)
{
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
2.
view.post(runnable)
通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view也已经初始化好了
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
3.
ViewTreeObserver
使用
ViewTreeObserver
的众多回调方法可以完成这个功能,比如使用
onGlobalLayoutListener
接口,当view树的状态发生改变或者view树内部的view的可见性发生改变时,
onGlobalLayout
方法将被回调。
伴随着view树的状态改变,这个方法也会被多次调用。
4.view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动对view进行measure来得到view的宽高,这个要根据view的LayoutParams来处理:
match_parent
:无法measure出具体的宽高,原因很简单,根据View的measure过程,构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小,所以理论上不可能测量出View的大小;
wrap_content
:如下measure,设置最大值
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST); |
精确值:例如100px
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); |
4.3.2 layout过程
Layout的作用是ViewGroup用来确定子元素的位置
(3)draw过程大概有下面几步:
1.绘制背景:background.draw(canvas)
;
2.绘制自己:onDraw()
;
3.绘制children:dispatchDraw
;
4.绘制装饰:onDrawScrollBars
。
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递下去,这里面有一个setWillNotDraw方法,这个方法的意义是,当我们的自定义控件继承于Viewgroup并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化,当然,当明确知道一个ViewGroup需要通过onDraw来绘制内容时,我们需要显示地关闭WILL_NOT_DRAW这个标记位。
4.4 自定义view
4.4.1 自定义view的分类
1、继承View重写onDraw方法
这种方法主要用于实现一些不规则的效果,即重写onDraw方法,采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。
2、继承ViewGroup派生特殊的layout
这种方法主要用于实现自定义的布局,当某种效果看起来像几种View组合在一起的时候,采用这种方式,需要处理ViewGroup的测量,布局这两个过程,并同时处理子元素的测量和布局的过程。
3、继承特定的View(比如TextView)
这种方式比较常见,一般是用于扩展某种已有的View的功能,不需要自己支持wrap_content和padding等
4、继承特定的ViewGroup(比如LinearLayout)
这种方式不需要自己处理ViewGroup的测量和布局的两个过程
4.4.2 自定义View须知
1、让View支持wrap_content
2、如果有必要,让你的View支持padding
这是因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用的,另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的marign失效。
3、尽量不要在View中使用Handler,没必要
这是因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你很明确地要使用Handler来发送消息。
4、View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow
在onDetachedFromWindow是一个很好的时机,这个和onAttachedToWindow对应,否则会造成内存泄露。
5、View带的滑动嵌套情形时,需要处理好滑动冲突
接下来是原书中的自定义view的示例,推荐阅读源码。