Android View的绘制流程知识点总结
前言
本文属于《Android开发艺术探索》(以下简称“艺术探索”)第四章——View的工作原理读书笔记。
同样,还是博客园那位博主的文章:Android View绘制13问13答。他的 Andoird N问N答 系列文章总结的非常好。同时,在第三章读书笔记中提到的CSDN博主废墟的树,这篇从ViewRootImpl类分析View绘制的流程,把绘制的三大流程讲的非常清楚。感谢前辈们的分享。
1.绘制流程从哪里开始
以Activity为例,下图是相关方法的调用流程:
可以看到,最终performTraversals()方法触发了View的绘制。该方法内部,依次调用了performMeasure(),performLayout(),performDraw(),将View的measure,layout,draw过程,从顶层View分发了下去。
上面体现了Activity中View的绘制过程是如何被触发的,其实通过阅读《艺术探索》第8章——理解Window和WindowManager,可以知道,Dialog,PopupWindow中View的绘制过程也是一样的,只是触发的方式不同。例如Dialog中,是调用dialog.show()时,触发了WindowManagerImpl的addView()(上图步骤2),后面的流程就一样了。
2.绘制流程第一步——measure
什么是MeasureSpec
MeasureSpec是一个specSize和specMode信息的32位int值,其中高两位表示specMode,低30位表示specSize。specMode指测量模式,specSize指在某种测量模式下的规格大小。specMode包括:
- UNSPECIFIED
- EXACTLY
- AT_MOST
View需要MeasureSpec信息,来确定自己能显示多大。顶层View的MeasureSpec由ViewRootImpl#getRootMeasureSpec()方法获得,子View的MeasureSpec,由父容器和其自身的LayoutParams共同决定,通过ViewGroup提供的getChildMeasureSpec()方法获得。
getRootMeasureSpec()源码如下:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
getChildMeasureSpec()方法较长,《艺术探索》中的一图以表格形式呈现了该方法:
View的measure过程
父容器调用子View的measure方法把上一步获得的MeasureSpec信息传递过去,子View的measure方法调用View#onMeasure(),onMeasure调用setMeasuredDimension()设置自身大小。该过程如图:
getSuggestedMinimumHeight(),getSuggestedMinimumWidth():
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
作用一目了然。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的宽高属性为wrap_content时,其父View通过getChildMeasureSpec方法确定的其测量模式为AT_MOST。再由getDefaultSize()可知,其最终的宽高会被设置为specSize,即父View所剩空间的大小,这也就是为什么自定义View不对AT_MOST模式做处理,其wrap_content和match_parent效果一样。
ViewGroup的measure过程
ViewGroup的measure()和onMeasure()方法是从View继承过来的,没有做任何重写(measure方法是final限定,不能重写)。其onMeasure()方法由各个子类各自重写,实现自己的需求。ViewGroup的子类在onMeasure()中做的事其实都差不多:
- 遍历子View
- 调用measureChild*让子View确定自己的大小
- 根据所有子View的大小确定自己的大小
2步骤中*是个通配符,意思是measureChild一类的方法,如measureChildHorizontal,measureChildWithMargins。这些方法内部会调用getChildMeasureSpec确定子View的测量模式,会调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec)触发子View的测量。
3.绘制流程第二步——layout
View源码中,layout方法中会先调用setFrame给自身的left,top,right,bottom属性赋值,至此自己在父View中的位置就确定了。然后会调用onLayout方法,该方法在View中是一个空实现,具体的实现由其子View(一般指ViewGroup)实现,如LinearLayout。
4.绘制流程第三步——draw
绘制流程经过ViewRootImpl中的performMeasure(),performLayout(),现在到了perfromDraw()。performDraw经过如下图所示的方法调用,触发了顶层View的draw方法:
drawSoftware()方法内部调用了mView.draw(canvas),mView就是“1.绘制流程从哪里开始”第4步setView()所设置的根View。View的draw方法源码中的注释具有参考价值,如下:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* 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);
// 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);
// we're done...
return;
}
}
可以看到一般情况下View的draw流程分为四步:
- 绘制背景
- 绘制自身内容(onDraw)
- 遍历子View,调用其draw方法把绘制过程分发下去(dispatchDraw)
- 绘制装饰(onDrawForeground)
其中步骤二在自定义View的时候经常需要去实现,以绘制自己想要的效果。步骤三dispatchDraw在View中是个空实现。ViewGroup实现了dispatchDraw(),其中调用了ViewGroup#drawChild方法,而drawChild()仅仅是调用了child.draw():
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
自定义View
自定义View的几种方式
1.继承自View,重写onDraw方法
场景:显示的内容需要高度定制,如图表
2.继承自ViewGroup
场景:流式布局等,对于内容布局由特殊要求的。通过重写onLayout方法达到目的。
3.继承自特定的View(如TextView,ImageView)
场景:需要扩展系统控件功能,如圆角ImageView
4.继承自特定的ViewGroup(如LinearLayout)
场景:不需要自定义布局方式,只需要将常用的View组合起来,比如app设置界面,常常每一个item都是左一个图标,中间一行文字,右边一个箭头,这是就可以继承自LinearLayout,默认水平方向布局,暴露出setIcon,setText等自定义的方法设置图标,文字即可。
自定义View注意事项
- 让View支持wrap_conent
- 让View支持padding
- 尽量避免使用Handler,一般都可以用View自带的post方法代替
- 在onDeatchFromWindow时,停止View的动画或线程(如果有的话)
- 如果存在嵌套滑动,处理好滑动冲突
总结
没有总结。。以上只是看完《艺术探索》用自己的理解写读书笔记加强记忆而已。各路大神总结的很清楚,需要时查阅他们博客就行了,比如要点提炼|开发艺术之View。