初识ViewRoot和DecorView
ViewRoot对应ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot类完成的,在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
View的绘制流程是从ViewRoot的performTraversals方法开始。
理解MeasureSpec
MeasureSpec(测量规格)是一个32位int值,高2位代表SpecMode,低30位代表SpecSize。
SpecMode有以下三类,每一个类都表示特殊的含义。
UNSPECIFIED
父容器不对 View有任何限制,要多大就给多大,这种情况一般用于系统内部,表示一种测量的状态。
EXACTLY
父容器已经检测出View的所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。 它对应于LayoutParams中的match_parent和具体数值这两种模式。
AT_MOST
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定;对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同确定。
View的工作流程
1.Measure流程
View 的measure过程
View.measure–>View.onMeasure
protected void onMeasure(int widthMeasureSpec , int heightMeasureSpec){
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
}
(具体分析得多看书上源码分析,一定要看啊!)
*注意:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于match_parent.可以结合specMode 和specSize作相应分析。
解决办法如下:*
protectd void onMeasure(int widthMeasureSpec,int heightMeasureSpec{
super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int widthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode=MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight);
}else if (widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightSpecSize);
}else if (heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,mHeight);
}
}
在上面代码中,我们只需要给View指定一个默认的内部宽/高(mWidth和mHeight),并在wrap_content时设置此宽/高即可,对于非wrap_content情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。 如果查看TextView,ImageView等的源码就可以知道,针对wrap_content情形,它们的onMeasure方法均做了特殊处理。
ViewGroup的measure过程
ViewGroup.measureChildren–>ViewGroup.measureChild
–>View.measure
onMeasure是一个抽象方法,各种ViewGroup测量细节和逻辑在onMeasure中具体体现,大家可以参考某些ViewGroup源码。
获取某个View的宽/高的几种方法
(1)Activity/View.onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没问题的。注意这个回调由于焦点的得到和失去会被频繁调用。
public void onWindowFocusChanged(boolean hasFocus){
super.onWindowFocusChanged(hasFocus);
if (hasFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
(2)view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后的等待Looper调用此runnable的时候,View也已经初始化好了。
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的宽/高一个很好的时机。需要注意的是,伴随着View树的状态改变等,onGlobalLayou会被调用多次。
protected void onStart(){
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
@SuppressWarning("deprecation")
@Override
public void onGlobalLayout(){
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
(4)view.measure(int widthMeasureSpec,int heightMeasureSpec)
通过手动进行View的measure来提前获取宽高,根据View的LayoutParams来分:
match_parent(直接放弃 ,因为构造此MeasureSpec需要根据parentSize(父容器剩余空间))
具体的数值(dp/px)
比如宽/高都是100px,如下measure
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
wrap_content
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30)-1,MeasureSpec.AT_MOST);
int heightMeasureSpec= MeasureSpec.makeMeasureSpec((1 << 30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);
2.Layout过程
具体过程参照书本上代码分析
getMeasuredWidth/getMeasuredHeight
getWidth/getHeight
注意:在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于View的measure过程,而最终的宽/高形成于View的layout过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。
3.Draw过程
dispatchDraw –>draw –>onDraw
(1)绘制背景 (background.draw(canvas))
(2)绘制自己 (onDraw)
(3)绘制children (dispatchDraw)
(4)绘制装饰 (onDrawScrollBars)
注意:setWillNotDraw这个方法的作用是:如果一个View不需要绘制任何内容,那么设置整个标记位为true以后,系统会进行相应的优化。默认情况下,View没有启用整个优化标记位,但是ViewGroup会默认启动这个优化标记位。所以,当我梦自定义控件继承于ViewGroup并且不具备绘制的功能时 ,可以开启这个标记位 从而便于系统进行的后续的优化。
自定义View
注意几点:
1.让View 支持wrap_content,找好默认的宽高。
2.如果有必要,让你的View 支持padding
3.尽量不要在view中使用handler,没必要,因为View内部本身有一系列的post方法
4.View 中如果有线程或者动画,需要及时停止,View.onDetachedFromWindow()是个很好的时机。
5.View带有滑动嵌套的情形,需要处理好滑动冲突。