Android View 绘制流程之三:draw绘制
系列文章:
Android View 绘制流程之一:measure测量
Android View 绘制流程之二:layout布局
Android View 绘制流程之三:draw绘制
Android View 绘制流程之四:绘制流程触发机制
draw()方法是View系统测绘流程的最后一步,就是绘制,当view测量完大小、确定完位置后,就需要在其位置处绘制出其内容等视觉上的东西;View的draw方法有固定的流程,一般ViewGroup需要在dispatchDraw方法中完成对子view的绘制,View需要在onDraw方法里完成对自己内容的绘制。
一.draw方法的整体流程
ViewRootImpl里调用performDraw()方法,该方法里最终会调用根view的draw方法开始整个view的绘制
View的draw(Canvas canvas)方法主要流程已经规定好:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);//如果view的DIRTY标志是OPAQUE,说明其child的该区域不透明,就不用绘制这块的背景和内容了
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;//标志DRAWN,已经绘制
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
//有透明的需要绘制背景
drawBackground(canvas);
}
// 通常情况是可以省略2、5两步
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, 绘制自身内容
if (!dirtyOpaque) onDraw(canvas);
// Step 4, ViewGroup需要重写该方法实现子view的绘制
dispatchDraw(canvas);
// ...
// Step 6, 绘制一些前景,比如前景图、ScrollBar等
onDrawForeground(canvas);
// we're done...
return;
}
...
//正常的全部流程
// Step 2, 用canvas.saveLayers的方法保存渐变边的图层
...
// Step 3, 4
// Step 5, 绘制图层
...
canvas.restoreToCount(saveCount);//恢复canvas状态
// Step 6
...
}
流程很清晰,大部分view就是先绘制必要的背景,然后调用onDraw绘制自身内容,绘制完后如果是ViewGroup,还会调用dispatchDraw绘制子view,最后绘制前景一些UI即可;对于一些特殊view,只是多加了绘制渐变框的一步(使用保存图层、绘制图层、恢复画布的做法)
下面就来看看这几个流程的具体实现,这里有一点需要注意,整个view调用draw时的参数,是一个Canvas对象,是ViewRootImpl里创建的SurfaceView的画布,对于整个屏幕来说,canvas的起始点就是0,0,但是对于每个View来说,为了使view在使用canvas绘制时的处理方便,每次在调用view的draw方法前,都会用平移的方法将canvas的其实点移动到view的起始点,这样对于view自己来说,canvas总是从"原点"开始的,所以只需照常绘制即可;那么这点是怎么实现的,下面的各个流程会解释到:
二.drawBackground实现
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();//设置drawable边界
// 硬件加速绘制
...
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
//如果有滚动偏移量,则要将canvas原点移动,然后交由drawable绘制,还要再将原点移动回来
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
由代码可知,如果有view有偏移量,比如向上滚动了10px,则mScrollY=10(即新的内容原点就是原来的y=10处),那么传递给view的canvas的起始点就是(0,-10),而background是针对于view的,不是view内部内容的,不应该跟随着内容的滚动而滚动,所以canvas需要移动(0,10)到(0,0),再去绘制背景drawable,之后再复原到(0,-10),用于view内容的绘制;且无论是否硬件加速绘制,都会有相同的处理
三.onDraw
onDraw(canvas)方法是view绘制自己内容的方法,也是我们自定义view最常用的方法,我们可以使用canvas绘制任意元素,而不需要考虑滚动,因为传递过来的canvas对于view来说就是从(0,0)开始的,下面拿ImageView来举例:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
mDrawable.draw(canvas);//直接绘制在canvas上即可
} else {
int saveCount = canvas.getSaveCount();//保存画布状态
canvas.save();
...
canvas.translate(mPaddingLeft, mPaddingTop);//平移
if (mDrawMatrix != null) {
//矩阵变换
canvas.concat(mDrawMatrix);
}
mDrawable.draw(canvas);//绘制在canvas上
canvas.restoreToCount(saveCount);//恢复画布状态
}
}
有代码可知,onDraw里基本就是使用canvas绘制一些元素即可,其canvas自身也提供了许多绘制元素方法,这里不在赘述。
四.dispatchDraw的实现
dispatchDraw(canvas)方法是ViewGroup需要重写,进行子view绘制的方法,上面已经说过view绘制流程,那么可得知该方法是在自身onDraw方法调用后执行,也就是说view先绘制自身内容,再绘制子view而ViewGroup其实已经实现了dispatchDraw方法,而大部分ViewGroup绘制子view的流程都是如此,所以大部分的ViewGroup子类直接调用该方法即可,无需再处理,下面就来看看这个方法:
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
//如果ViewGroup有布局动画
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
final boolean buildCache = !isHardwareAccelerated();
for (int i = 0; i < childrenCount; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
final LayoutParams params = child.getLayoutParams();
attachLayoutAnimationParameters(child,