本系列总结主要参考GcsSloop自定义View系列以及《Android开发艺术探索》中的相应章节内容,仅作为个人笔记使用。
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是ViewRoot来完成。当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。View的绘制流程从ViewRoot的performaTraversals方法开始,经过measure、layout、draw三个过程将一个View绘制出来。performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw流程。
DecorView作为顶级VIew,一般它内部包含一个竖直方法的LinearLayout,这个LinearLayout里面有上下两个部分,上面是标题栏,下面是内容栏。Activity中setContentView所设置的布局文件是被加到内容栏中。用如下方式得到content,通过content.getChildAt(0)得到我们设置的View。
ViewGroup content = (ViewGroup) findViewById(android.R.id.content);
自定义View绘制流程函数调用链
View的工作流程主要是指measure、layout、draw这三大流程,其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,draw负责将View绘制到屏幕上。
1.View的measure过程
View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,子类不能重写此方法,在View的measure方法中会调用View的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
setMeasureDimension会设置View宽/高的测量值,只需看getDefaultSize方法:
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,即View测量后的大小。
UNSPECIFIED这种情况,一般用于系统内部的测量过程,View的大小为getSuggestedMinimumWidth和getSuggestedMinimumHeight这两个方法的返回值。
2.ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个方法。ViewGroup是一个抽象类,没有重写onMeasure方法,但是提供了一个measureChildren方法。
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); } } }
ViewGroup在measure时,会对每一个子元素进行measure,measureChild方法的实现:
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); }
measureChild的思想就是取出子元素的LayoutParams,然后通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。ViewGroup其测量过程的onMeasure方法需要各个子类去具体实现,ViewGroup子类有不同的布局特性,测量细节也各不相同。
Tips
如在Activity已启动的时候做一任务来获取某个View的宽/高,在onCreate、onStart、onResume中均无法正确得到某个View的宽/高,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,如果View还没有测量完毕,那么获取的宽/高就是0.
解放方法:
1) 在onWindowFocusChanged方法中获取。View已经初始化完毕,宽/高已准备好,这时候获取宽/高是没问题的。当Activity的窗口得到焦点和失去焦点均会调用onWindowFocusChanged。
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的众多回调可以完成这个功能。