本文不再讨论自定义View,推荐阅读扔物线的HenCoder的自定义View系列文章。
View的流程简述
View的绘制流程是从ViewRoot的performTraversals方法开始的,主要是:measure、layout、draw。大概流程如下图:
View的绘制流程.jpgDecorView.png
- measure方法确定View的测量宽、高。
- layout方法确定View的最终宽高和四个顶点的位置。
- draw方法将View绘制到屏幕上。
- ViewRoot的实现类是ViewRootImpl,是连接WindowManager和DecorView的纽带。
- 从ViewRoot的performTraversals开始,经过performMeasure()、performLayout()、performDraw()完成顶级View(DecorView)的绘制。其中performMeasure()会调用measure和onMeasure,在onMeasure中对所有子元素调用measure和onMeasure,如此循环则完成measure过程,layout、draw同理。
- 顶级View,即DecorView是一个FrameLayout。内部有一个竖直方向的LinearLayout,包含了titleBar和contentView。如下图:
- 我们的setContentView就是将自己的View放进了content里面。获取自己的View可以这样做:
ViewGroup viewGroup = findViewById(android.R.id.content);
View view = viewGroup.getChildAt(0);
View的具体工作流程
1. measure过程
作用:确定View的测量宽高
- MeasureSpec:一个32位的int值。高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小)。
SpecMode分为UNSPECIFIED、AT_MOST、EXACTLY。
- UNSPECIFIED 父容器不对View做任何限制,常用于系统内部。
- EXACTLY 父容器指定View的大小为SpecSize指定的值。对应match_parent和具体数值。
- AT_MOST 父容器指定最大尺寸SpecSize。对应wrap_content。
- 最终View的MeasureSpec由View的LayoutParams和父容器的MeasureSpec决定,一旦确定后,在onMeasure就可以确定View的测量宽高。
引用一张网上的图片帮助理解:
MeasureSpec.png
其中parentLeftSize表示父容器剩下的可用大小,childSize为子View的指定大小。
1.1 View的measure过程
在View的measure方法中会去调用View的onMeasure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension方法会设置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;
}
可以看出,如果直接继承View的自定义控件没有重写onMeasure,那么参数match_parent和wrap_content将为同一结果。处理这个问题的常用代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int withSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int withSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//处理warp_content为自己指定默认值,其余为本身测量值
if (withSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (withSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(withSpecSize, mHeight);
}
}
1.2 ViewGroup的measure过程
ViewGroup是一个抽象类,其中没有重写View的onMeasure方法但是提供了measureChildren的方法来测量子类,具体的测量过程需要子类去实现。
在实际开发中,需要在Activity启动时获取某个控件的宽高,如果在onCreate、onStart中获取这个View的宽高,有可能不是正确的数值。在View测量完成前,获取到的数值都是0。
解决方法有4种:
- Activity/View#onWindowFocusChanged
当Activity获得焦点时,View已经初始化完毕了。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
- view.post(runnable)
通过post将runnable投递到消息队列尾部,等待Looper调用此runnable时View也已经初始化好了。
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
- ViewTreeObserver
这是View树的回调接口,当View树状态改变或者View可见性改变时,onGlobalLayout会被回调。 - view.measure(widthMeasureSpec, heightMeasureSpec)
手动去测量view的宽高。
2. layout过程
作用:确定View的四个顶点的位置和最终宽高。
layout方法首先确定ViewGroup的位置,然后在onLayout中对子View遍历来确定子View的位置。
具体流程:
ViewGroup的layout过程.jpg
3. draw过程
作用:将View绘制到屏幕上。
View的过程如下几步:
- 绘制背景:drawBackground()
- 绘制自己:onDraw()
- 绘制children:dispatchDraw()
- 绘制装饰:onDrawForeground()
通过查看View的源码可以清晰看到:
public void draw(Canvas canvas) {
...
/*
* 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)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...
}
网友评论