Android 自定义View之Draw过程(上)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 自定义View之Draw过程(上)相关的知识,希望对你有一定的参考价值。
参考技术ADraw 过程系列文章
android 展示之三部曲:
前边我们已经分析了:
这俩最主要的任务是: 确定View/ViewGroup可绘制的矩形区域。
接下来将会分析,如何在这给定的区域内绘制想要的图形。
通过本篇文章,你将了解到:
Android 提供了关于View最基础的两个类:
然而ViewGroup 并没有约定其内部的子View是如何布局的,是叠加在一起呢?还是横向摆放、纵向摆放等。同样的View 也没有约定其展示的内容是啥样,是矩形、圆形、三角形、一张图片、一段文字抑或是不规则的形状?这些都要我们自己去实现吗?
不尽然,值得高兴的是Android已经考虑到上述需求了,为了开发方便已经预制了一些常用的ViewGroup、View。
如:
继承自ViewGroup的子类
继承自View的子类
虽然以上衍生的View/ViewGroup子类已经大大为我们提供了便利,但也仅仅是通用场景下的通用控件,我们想实现一些较为复杂的效果,比如波浪形状进度条、会发光的球体等,这些系统控件就无能为力了,也没必要去预制千奇百怪的控件。想要达到此效果,我们需要自定义View/ViewGroup。
通常来说自定义View/ViewGroup有以下几种:
3 一般不怎么用,除非布局比较特殊。1、2、4 是我们常用的手段,对于我们常说的"自定义View" 一般指的是 4。
接下来我们来看看 4是怎么实现的。
在xml里引用MyView
效果如下:
黑色部分为其父布局背景。
红色矩形+黄色圆形即是MyView绘制的内容。
以上是最简单的自定义View的实现,我们提取重点归纳如下:
由上述Demo可知,我们只需要在重写的onDraw(xx)方法里绘制想要的图形即可。
来看看View 默认的onDraw(xx)方法:
发现是个空实现,因此继承自View的类必须重写onDraw(xx)方法才能实现绘制。该方法传入参数为:Canvas类型。
Canvas翻译过来一般叫做画布,在重写的onDraw(xx)里拿到Canvas对象后,有了画布我们还需要一支笔,这只笔即为Paint,翻译过来一般称作画笔。两者结合,就可以愉快的作画(绘制)了。
你可能发现了,在Demo里调用
并没有传入Paint啊,是不是Paint不是必须的?实际上调用该方法后,底层会自动生成Paint对象。
可以看到,底层初始化了Paint,并且给其设置的颜色为在Java层设置的颜色。
onDraw(xx)比较简单,开局一个Canvas,效果全靠画。
试想,这个Canvas怎么来的呢,换句话说是谁调用了onDraw(xx)。发挥一下联想功能,在Measure、Layout 过程有提到过两者套路很像:
那么Draw过程是否也是如此套路呢?看见了onDraw(xx),那么draw(xx)还远吗?
没错,还真有draw(xx)方法:
可以看出,draw(xx)主要分为两个部分:
不管是A分支还是B分支,都进行了好几步的绘制。
通常来说,单一一个View的层次分为:
后面绘制的可能会遮挡前边绘制的。
对于一个ViewGroup来说,层次分为:
来看看A分支标注的4个点:
(1)
onDraw(canvas)
前面分析过,对于单一的View,onDraw(xx)是空实现,需要由我们自定义绘制。
而对于ViewGroup,也并没有具体实现,如果在自定义ViewGroup里重写onDraw(xx),它会执行吗?默认是不会执行的,相关分析请移步:
Android ViewGroup onDraw为什么没调用
(2)
dispatchDraw(canvas),来看看在View.java里的实现:
发现是个空实现,再看看ViewGroup.java里的实现:
也即是说,对于单一View,因为没有子布局,因此没必要再分发Draw,而对于ViewGroup来说,需要触发其子布局发起Draw过程(此过程后续分析),可以类比事件分发过程View、ViewGroup的处理。感兴趣的请移步:
Android 输入事件一撸到底之View接盘侠(3)
(3)
OverLay,顾名思义就是"盖在某个东西上面",此处是在绘制内容之后,绘制前景之前。怎么用呢?
以上是给一个ViewGroup设置overLay,效果如下:
你可能发现了,这和设置overLay差不多的嘛,实际还是有差别的。在onDrawForeground(xx)里会重新调整Drawable的尺寸,该尺寸与View大小一致,之前给Drawable设置的尺寸会失效。运行效果如下:
可以看出,ViewGroup都被前景盖住了。
再来看看B分支的重点:边缘渐变效果
先来看看TextView 边缘渐变效果:
加上这俩参数。
实际上系统自带的一些控件也使用了该效果,如NumberPicker、YearPickerView
以上是NumberPicker 的效果,可以看出是垂直方向渐变的。
对于View.java 里的onDraw(xx)、draw(xx),ViewGroup.java里并没有重写。
而对于dispatchDraw(xx),在View.java里是空实现。在ViewGroup.java里发起对子布局的绘制。
来看看标记的2点:
(1)
设置padding的目的是为了让子布局留出一定的空隙出来,因此当设置了padding后,子布局的canvas需要根据padding进行裁减。判断标记为:
FLAG_CLIP_TO_PADDING 默认设置为true
FLAG_PADDING_NOT_NULL 只要有padding不为0,该标记就会打上。
也就是说:只要设置了padding 不为0,子布局显示区域需要裁减。
能不能不让子布局裁减显示区域呢?
答案是可以的。
考虑到一种场景:使用RecyclerView的时候,我们需要设置paddingTop = 20px,效果是:RecyclerView Item展示时离顶部有20px,但是滚动的时候永远滚不到顶部,看起来不是那么友好。这就是上述的裁减起作用了,需要将此动作禁止。通过设置:
当然也可以在xml里设置:
(2)
drawChild(xx)
从方法名上看是调用子布局进行绘制。
child.draw(x1,x2,x3)里分两种情况:
这两者具体作用与区别会在下篇文章分析,不管是硬件加速绘制还是软件加速绘制,最终都会调用View.draw(xx)方法,该方法上面已经分析过。
注意,draw(x1,x2,x3)与draw(xx)并不一样,不要搞混了。
用图表示:
View/ViewGroup Draw过程的联系:
一般来说,我们通常会自定义View,并且重写其onDraw(xx)方法,有没有绘制内容的ViewGroup需求呢?
是有的,举个例子,大家可以去看看RecyclerView ItemDecoration 的绘制,其中运用到了ViewGroup draw(xx)、ViewGroup onDraw(xx) 、View onDraw(xx)绘制的先后顺序来实现分割线,分组头部悬停等功能的。
本篇文章基于 Android 10.0
Android系列View的绘制之draw过程
本文将讲述View绘制的最后一个过程——draw过程,继measure和layout过程之后,View已经确认了自身的大小和位置,draw过程将完成View内容的绘制,到此,View的绘制才真正完成。
draw第一步:从DecorView开始
和measure与layout过程一样,draw过程也是由ViewRootImpl对象开始执行的。在ViewRootImpl类的performTraversals
方法中,经由performDraw
、draw
、drawSoftware
一直到调用DecorView的draw
方法,draw过程开始遍历整个View树。
draw方法的六个步骤
View的draw
方法和measure
与layout
方法一样,在自定义View时不用重写draw
方法,而是要重写onDraw
方法,这一点在draw
方法的注释中也给出了说明:
When implementing a view, implement onDraw(android.graphics.Canvas) instead of overriding this method.
下面看下draw
方法的源码:
/* View.draw */
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
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);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
从draw
方法中一开始的注释可以看到,draw
方法是分6步完成的:
1. 绘制背景;
2. 保存画布当前状态为绘制fading edges做准备;
3. 绘制内容;
4. 绘制子View;
5. 绘制dading edges并还原画布状态;
6. 绘制装饰(如滑动条)。
其中,第2和第5步是可选的,上面的代码除去了第2和第5步,因为它们并不常用并且会拖慢绘制过程。第3步是View绘制自身的内容,而第4步是父View绘制子View的过程,draw过程也是在这里传递给子View的。
draw第二步:从父View向子View传递
经过上一节的分析,我们已经知道View是通过onDraw
方法绘制自身的,并且是通过dispatchDraw
方法绘制子View的。
我们看View的onDraw
方法,它是一个空方法,这在我们的意料之中,毕竟不同类型的View在屏幕上展现的内容都不一样,也就是说每个View(不是ViewGroup)都会重写onDraw
方法。
我们再来看dispatchDraw
方法,它在View中也是一个空方法,不过ViewGroup在继承View时重写了此方法,在ViewGroup的dispatchDraw
方法中有这样一段代码:
/* ViewGroup.dispatchDraw */
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);
这里在for循环中遍历了每一个子View,并通过drawChild
方法绘制子View的内容:
/* ViewGroup.drawChild */
protected boolean drawChild(Canvas canvas, View child, long drawingTime)
return child.draw(canvas, this, drawingTime);
到此,draw过程就从父View传递到了子View,并重复此过程直到到达View树的叶子节点。
draw第三步:在View处终止
当draw过程传递到View树的叶子节点时,由于已经没有需要绘制的子View,因此draw过程到这里也就完成了。对应的,View的dispatchDraw
方法理所当然是一个空方法。
到这里,View绘制的三个过程都已经介绍完了,如果你还对measure或者layout过程不甚了解,可以阅读另外两篇文章:【Android系列】View的绘制之measure过程和【Android系列】View的绘制之layout过程。
按照惯例,我们也给出draw过程的流程图:
现在,我们再回顾一下View绘制的整个过程:
View的绘制是从ViewRootImpl的performTraversals
方法开始的,并经过measure、layout和draw三个过程才能最终将一个View绘制出来。其中measure过程对View的宽高进行测量,layout过程会确定View在父容器中的放置位置,draw过程负责将View绘制到屏幕上。
坚持就是胜利ヾ(◍°∇°◍)ノ゙
以上是关于Android 自定义View之Draw过程(上)的主要内容,如果未能解决你的问题,请参考以下文章
Android应用层View绘制流程之measure,layout,draw三步曲