一 概述
我们知道 Android 视图的绘制流程分为:Measure,Layout 和 Draw,之前我们已经分析了:Measure 和 Layout,他们最主要的任务是:确定 View/ViewGroup 可绘制的矩形区域。接下来我们分析 Draw 是如何在这给定的区域内绘制想要的图形。
二 为什么要自定义View
Android 提供了关于 View 最基础的两个类:
ViewGroup 与 View
然而 ViewGroup 并没有约定其内部的子 View 是如何布局的,是叠加在一起呢?还是横向摆放、纵向摆放等。同样的 View 也没有约定其展示的内容是啥样,是矩形、圆形、三角形、一张图片、一段文字抑或是不规则的形状?这些都要我们自己去实现吗?
不尽然,值得高兴的是 Android 已经考虑到上述需求了,为了开发方便已经预制了一些常用的 ViewGroup、View,比如:
继承自 ViewGroup 的子类
- FrameLayout --> 里面的子 View 是层叠摆放的
- LinearLayout -->里边的子 View 是可以纵向/横向排列的
- RelativeLayout -->里边的子 View 可以相对布局
- RecyclerView -->里边的子 View 以列表形式展示
等等…
继承自 View 的子类
- TextView --> 用于绘制一段文本
- ImageView --> 用于绘制一张图片
- EditText -->用于绘制输入框
- Button --> 用户绘制按钮
等等…
虽然以上衍生的 View/ViewGroup 子类已经大大为我们提供了便利,但也仅仅是通用场景下的通用控件,我们想实现一些较为复杂的效果,比如波浪形状进度条、会发光的球体等,这些系统控件就无能为力了,也没必要去预制千奇百怪的控件。想要达到此效果,我们需要自定义 View/ViewGroup。
通常来说自定义 View/ViewGroup 有以下几种:
- 如果你觉得系统提供的 ViewGroup 子类基本符合你需求,但你想将一些功能封装到一个组件里,那么就直接继承 FrameLayout、LinearLayout 等。这样一来,继承了他们的特性,也将自己的逻辑封装了。
- 如果你觉得系统提供的 View 子类基本符合你的需求,但你想将一些功能封装到一个控件里,比如显示 Emoji,那么直接继承自 TextView (AppCompatTextView 兼容)。
- 如果你看不起系统预制的 ViewGroup 子类,直接继承自 ViewGroup,那么你需要重写 onMeasure()、onLayout() 等方法。
- 如果不想用系统预制的 View 子类,直接继承自 View,那么你需要自己绘制内容,重写 onDraw() 方法。
第三种情况一般不怎么用,除非布局比较特殊。其它情况是我们常用的手段,对于我们通常来说的 “自定义View” 一般指的是第四种情况。
三 View Draw过程
来看看 View 默认的 onDraw() 方法:
View.java
protected void onDraw(Canvas canvas) {
}
发现是个空实现,因此继承自 View 的类必须重写 onDraw() 方法才能实现绘制。该方法传入参数为:Canvas 类型,Canvas 翻译过来一般叫做画布,在重写的 onDraw() 里拿到 Canvas 对象后,有了画布我们还需要一支笔,这只笔即为 Paint,翻译过来一般称作画笔。两者结合,就可以愉快的作画 (绘制) 了.
你可能发现了,在 Demo 里调用
canvas.drawColor(Color.RED);
并没有传入 Paint 啊,是不是 Paint 不是必须的?实际上调用该方法后,底层会自动生成 Paint 对象。
SkCanvas.cpp
void SkCanvas::drawColor(SkColor c, SkBlendMode mode) {
SkPaint paint;
paint.setColor(c);
paint.setBlendMode(mode);
this->drawPaint(paint);
}
可以看到,底层初始化了 Paint,并且给其设置的颜色为在 Java 层设置的颜色。
onDraw() 比较简单,开局一个 Canvas,效果全靠画。试想,这个 Canvas 怎么来的呢,换句话说是谁调用了 onDraw()。发挥一下联想功能,在 Measure、Layout 过程有提到过两者套路很像:
- measure()、layout() 一般不需要重写
- onMeasure()、onLayout() 需要重写
- measure() 里调用了 onMeasure()
- layout() 里调用了 onLayout()
那么 Draw 过程是否也是如此套路呢?看见了 onDraw(),那么 draw() 还远吗?没错,他们是一样的套路。我们来看 draw() 方法,其中的主要的步骤如下:
- Draw the background
- If necessary, save the canvas’ layers to prepare for fading
- Draw view’s content
- Draw children
- If necessary, draw the fading edges and restore layers
- Draw decorations (scrollbars for instance)
View.java
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
//打上已绘制标记
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
int saveCount;
//第一步 绘制背景
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;
//条件分支A
if (!verticalEdges && !horizontalEdges) {
//第三步 调用onDraw(),绘制View内容 (1)
onDraw(canvas);
//第四步 分发Draw,绘制子布局(2)
dispatchDraw(canvas);
//绘制自动填充的高亮(默认不会绘制)
drawAutofilledHighlight(canvas);
//mOverlay 绘制在内容之上,在前景色之下 (3)
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
//第六步,绘制装饰,如前景、滚动条等 (4)
onDrawForeground(canvas);
//第七步,绘制默认高亮,在touch mode模式基本不生效
drawDefaultFocusHighlight(canvas);
//调试用的,可以忽略
if (debugDraw()) {
debugDrawFocus(canvas);
}
//绘制完成,直接返回
return;
}
......
//条件分支B
//下面还有一大堆源码,主要就是做了一件事:绘制边缘渐变
//大部分情况下都不会走到这
//绘制步骤大体分为 七 个步骤,而上面只列出了1、3、4、6、7,剩下的步骤在此完成
//如果设置了边缘渐变,那么绘制步骤就会比不设置时多两个步骤,多出来的步骤是:2、5
//用文字简单概述一下
//1--->绘制背景
//2--->canvas.getSaveCount(); 记录canvas状态,为绘制边缘渐变做准备(canvas坐标要改变,因此先保存)
//3--->绘制内容
//4--->分发Draw,绘制子布局
//5--->绘制边缘渐变
//6--->绘制装饰
//7--->绘制默认高亮
}
可以看出,draw() 主要分为两个部分:
- 条件分支 A–> 大部分情况下都会走该分支
- 条件分支 B—> 极小部分情况会走该分支
- B 分支比 A 分支多了个2个步骤,目的是为了绘制边缘渐变效果
不管是 A 分支还是 B 分支,都进行了好几步的绘制。通常来说,单一一个 View 的层次分为:
- 背景–>内容–>前景
后面绘制的可能会遮挡前边绘制的。对于一个 ViewGroup 来说,层次分为:
- 背景–>内容–>子布局的层次–>前景
来看看 A 分支标注的重点步骤:
onDraw(canvas)
前面分析过,对于单一的 View,onDraw() 是空实现,需要由我们自定义绘制。而对于 ViewGroup,也并没有具体实现,如果在自定义 ViewGroup 里重写 onDraw(),它会执行吗?默认是不会执行的,相关分析请移步:
Android ViewGroup onDraw为什么没调用
dispatchDraw(canvas)
protected void dispatchDraw(Canvas canvas) {
}
发现是个空实现,再看看 ViewGroup.java 里的实现:
protected void dispatchDraw(Canvas canvas) {
......
//遍历子布局,发起 Draw 过程
......
}
也即是说,对于单一 View,因为没有子布局,因此没必要再分发 Draw,而对于 ViewGroup 来说,需要触发其子布局发起 Draw 过程(此过程后续分析),可以类比事件分发过程 View、ViewGroup 的处理。
OverLay
顾名思义就是"盖在某个东西上面",此处是在绘制内容之后,绘制前景之前。怎么用呢?
View viewGroup = findViewById(R.id.myviewgroup);
//给 overLay 指定一个 Drawable
Drawable drawable = ContextCompat.getDrawable(this, R.drawable.shapeme);
//设置 Drawable 的尺寸
drawable.setBounds(0, 0, 400, 58);
//为 overLay 添加 Drawable
viewGroup.getOverlay().add(drawable);
以上是给一个 ViewGroup 设置 overLay,效果如下:
黑色部分为 ViewGroup 背景,红色矩形+黄色圆圈为子布局,黄色矩形即为为 ViewGroup 添加的 overLay,可以看出 overLay 绘制在内容之上。
onDrawForeground()
绘制前景,使用方法如下:
View viewGroup = findViewById(R.id.myviewgroup);
Drawable drawable = ContextCompat.getDrawable(this, R.drawable.shapeme);
drawable.setBounds(0, 0, 400, 58);
viewGroup.setForeground(drawable);
你可能发现了,这和设置 overLay 差不多的嘛,实际还是有差别的。在 onDrawForeground() 里会重新调整 Drawable 的尺寸,该尺寸与 View 大小一致,之前给 Drawable 设置的尺寸会失效。运行效果如下:
可以看出,ViewGroup 都被前景盖住了。
以上是分支 A 的主要流程。
四 ViewGroup Draw过程
对于 View 里的 onDraw()、draw(),ViewGroup 里并没有重写。而对于 dispatchDraw(),在 View 里是空实现。在 ViewGroup 里发起对子布局的绘制。
ViewGroup dispatchDraw()
@Override
protected void dispatchDraw(Canvas canvas) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
int flags = mGroupFlags;
//动画相关
......
int clipSaveCount = 0;
//设置了 padding 后,绘制的子布局不能超过 padding (1)
final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
if (clipToPadding) {
//因此需要对canvas坐标进行变换,先保存其状态
clipSaveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop,
mScrollX + mRight - mLeft - mPaddingRight,
mScrollY + mBottom - mTop - mPaddingBottom);
}
//重置相关标记
mPrivateFlags &= ~PFLAG_DRAW_ANIMATION;
mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
......
for (int i = 0; i < childrenCount; i++) {
......
//遍历子布局,发起子布局绘制
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime); (2)
}
}
......
}
来看看标记的2点:
设置 padding
目的是为了让子布局留出一定的空隙出来,因此当设置了 padding 后,子布局的 canvas 需要根据 padding 进行裁减。判断标记为:
(flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK
protected static final int CLIP_TO_PADDING_MASK = FLAG_CLIP_TO_PADDING | FLAG_PADDING_NOT_NULL;
FLAG_CLIP_TO_PADDING 默认设置为 true,FLAG_PADDING_NOT_NULL 只要有 padding 不为 0,该标记就会打上,也就是说:只要设置了padding 不为 0,子布局显示区域需要裁减。那么能不能不让子布局裁减显示区域呢?答案是可以的。
考虑到一种场景:使用 RecyclerView 的时候,我们需要设置 paddingTop = 20px,效果是:RecyclerView Item 展示时离顶部有 20px,但是滚动的时候永远滚不到顶部,看起来不是那么友好。这就是上述的裁减起作用了,需要将此动作禁止。通过设置:
setClipToPadding(false)
当然也可以在xml里设置:
android:clipToPadding="false"
drawChild()
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
从方法名上看是调用子布局进行绘制。child.draw() 里分两种情况:
- 硬件加速绘制
- 软件绘制
这两者具体作用与区别会在下篇文章分析,不管是硬件加速绘制还是软件绘制,最终都会调用 View.draw() 方法,该方法上面已经分析过。注意,draw(x1,x2,x3) 与 draw (xx) 并不一样,参数不一样,两个不同的方法,不要搞混了。
五 View/ViewGroup 常用方法分析
用图表示:
一般来说,我们通常会自定义 View,并且重写其 onDraw() 方法,有没有绘制内容的 ViewGroup 需求呢?是有的,举个例子,大家可以去看看 RecyclerView ItemDecoration 的绘制,其中运用到了 ViewGroup draw(xx)、ViewGroup onDraw(xx) 、View onDraw(xx) 绘制的先后顺序来实现分割线,分组头部悬停等功能。