学习View的时候看到的,记录下
作者 朱才:http://cnblogs.com/zhucai/
朱才 微博:http://weibo.com/zhucai
2017/8/17 14:26:19
动画基础
本质
每帧绘制不同的内容。
基本过程
开始动画后,调用View
的invalidate
触发重绘。重绘后检查动画是否停止,若未停止则继续调用invalidate
触发下一帧(下一次重绘),直到动画结束。
重绘时View
的draw
方法会被调用,根据动画的进行绘制不同的内容,如某个被绘制元素的大小变化、角度旋转、透明度变化等,这样即会产生动画。
动画的推进过程一般都会有一个变化量,这个变量会被用到draw
方法内元素的绘制。一般的变量都是时间,也可以是手指移动、传感器等任何其他的变量。
Android中的动画支持
Animation
:早期实现的让View整体做动画的类。能让View
做Matrix
(移动、缩放、旋转、3D旋转)和Alpha
(透明)的动画。
Animator
:有硬件加速后为做动画实现的类。能方便的让View整体做动画;也可以只产生随时间变化的变量,用来在onDraw
里做绘图级的动画。比Animation
灵活很多。
AnimationDrawable
:图片逐帧动画。主要用来播放提前制作好的动画。
在哪个级别做动画
让整个View做动画(比如整个View平移、旋转等)很简单方便,一般调用几行代码就行。我把它称作View级的动画。
在View的draw/onDraw里通过Canvas来绘制时做动画更灵活,更精细,能力更强大。我把它称作绘图级的动画。(View级的动画本质上也是这么做的,只是Android系统帮我们做了大部分工作)
绘图级的动画
这篇文章主要讲绘图级的动画。
下面来一段绘图级动画的典型实现:
class MyView extends View {
void startAnimator() {
ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f);
animator.start();
invalidate();
}
protected void onDraw(Canvas canvas) {
if (animator.isRunning()) {
float ratio = (Float)animator.getAnimatedValue();
canvas.rotate(ratio*360);
canvas.drawBitmap(bitmap, 0, 0, null);
invalidate();
}
...
}
}
有了不断变化的ratio变量,绘图级动画就可以大展身手了。
绘图级动画的强大能力来自绘图API的强大能力,下面主要讲绘图API。
绘图API
Matrix
Canvas.[translate,scale,rotate,skew]方法
Matrix.set/pre/post[translate,scale,rotate,skew]方法
平移、缩放、旋转、斜切。
从使用API的角度来看,我们通过调用Canvas.translate
等方法,可以使后续在此Canvas
上绘制操作的绘制区域变化,如translate(5,0)
,则后续所有绘制操作的绘制区域都会向右移动5个像素。
原理:Canvas
里有一个Matrix
,Canvas
上的这几个调用都会最终调用到Matrix.pre*
。这个Matrix
保存整个变换过程。当有Canvas.draw
时,要绘制的点都会经过Matrix.mapPoints
方法做一个映射。于是产生我们期望的变换效果。(事实上映射的时候只需要映射关键点,其他的是插值来的)
关于Matrix的更多信息
set/pre/post
的区别:set
是设置,冲掉以前的数据。pre
是前乘,post
是后乘,根本上讲就是生效顺序不同。具体表现效果可在网上搜索资料。setPolyToPoly:与mapPoints方法相反,mapPoints是通过矩阵把原始点映射为目标点。
setPolyToPoly
是输入原始点和映射后的目标点,计算出这个矩阵。
Camera
:有透视效果的3D
旋转。Camera
是一个生成Matrix
的工具类。可用来生成有透视效果的3D旋转。
Canvas.draw*方法
Canvas.draw-Point/s
Canvas.draw-Line/s
Canvas.draw-Rect,RoundRect,Circle,Oval,Arc,Path
Canvas.draw-Text
Canvas.draw-Bitmap,BitmapMesh
Canvas.draw-Color,Paint
这些方法都表示绘制一个区域。绘制的区域中究竟填充什么颜色,由Paint决定。
Color,Paint,Bitmap,BitmapMesh这几个则除了指定绘制区域外,还指定了填充内容。
Path功能比较强大,可自行组织成任何形状,还可以用贝塞尔曲线。
这些方法基本上都很好理解,从名字上即可看出其功能。这里重点提一下drawBitmapMesh
。
drawBitmapMesh
是输入一个网格模型,绘制出的图片内容将依据这个网格来扭曲。可以想像成把图片画在一块有弹性的布上,当我们把布的某些区域扯动的时候,会形成画面扭曲效果。
示例:假设有个30x30大小的图片,我们建立这样的网格输入:
0,0, 15,0, 30,0,
0,15, 15,15, 30,15,
0,30, 15,30, 30,30
则图片会原样输出,没有任何扭曲。
如果我们建立这样的网格输入:
0,0, 15,12, 30,0,
0,15, 15,15, 30,15,
0,30, 15,30, 30,30
则原本[15,0]的点会被绘制到[15,12]的位置上去。图片绘制出来后,上面部分会缺一块,形成用手把图片从上边中间位置往下拉的扭曲效果。但很锐利,上面缺的一块是个三角形而不会是半圆型,通常我们希望的是半圆型,这就需要我们把这个网格建得密一些。
Alpha通道
每个Color
里可以有四个通道ARGB
,其中RGB
是红绿蓝,A
即Alpha
通道,它通常的作用是用来作为此颜色的透明度。
因为我们的显示屏是没法透明的,因此最终显示在屏幕上的颜色里可以认为没有Alpha
通道。Alpha
通道主要在两个图像混合的时候生效。
默认情况下,当一个颜色绘制到Canvas
上时的混合模式是这样计算的:(RGB
通道) 最终颜色 = 绘制的颜色 + (1 - 绘制颜色的透明度) × Canvas上的原有颜色。
注意:
1.这里我们一般把每个通道的取值从0到255映射到0到1的浮点数表示。
2.这里等式右边的“绘制的颜色”、“Canvas
上的原有颜色”都是经过预乘了自己的Alpha通道的值。如绘制颜色:0x88ffffff
,那么参与运算时的每个颜色通道的值不是1.0,而是(1.0 * 0.53125 = 0.53125)。
使用这种方式的混合,就会造成后绘制的内容以半透明的方式叠在上面的视觉效果。
其实还可以有不同的混合模式供我们选择,用Paint.setXfermode
,指定不同的PorterDuff.Mode
。
下表是各个PorterDuff
模式的混合计算公式:(D指原本在Canvas
上的内容dst
,S指绘制输入的内容src
,a指alpha
通道,c指RGB
各个通道)
ADD Saturate(S + D)
CLEAR [0, 0]
DARKEN [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)]
DST [Da, Dc]
DST_ATOP [Sa, Sa * Dc + Sc * (1 - Da)]
DST_IN [Sa * Da, Sa * Dc]
DST_OUT [Da * (1 - Sa), Dc * (1 - Sa)]
DST_OVER [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc]
LIGHTEN [Sa + Da - Sa*Da, Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)]
MULTIPLY [Sa * Da, Sc * Dc]
SCREEN [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc]
SRC [Sa, Sc]
SRC_ATOP [Da, Sc * Da + (1 - Sa) * Dc]
SRC_IN [Sa * Da, Sc * Da]
SRC_OUT [Sa * (1 - Da), Sc * (1 - Da)]
SRC_OVER [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc]
XOR [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc]
可以发现,我们之前的默认混合模式其实就是SRC_OVER。
通过选择其他的PorterDuff模式,我们可以达到一些特殊的效果:
使用DST_OVER的话,相当于后绘制的内容作为背景在底下。
使用DST_IN/DST_OUT的话,可以裁剪Canvas里的内容,或用一张带alpha的图片mask指定哪些区域显示/不显示。
通过选择SRC_ATOP可以只在Canvas上有内容(不透明)的地方绘制。
用一张示例图来查看使用不同模式时的混合效果(src表示输入的图,dst表示原Canvas上的内容):
填充颜色
之前说过Canvas.draw*
指定了绘制的区域。而区域里的填充颜色是由Paint
来指定的。
Paint.setColor
指定纯色。
Paint.setShader
可指定:BitmapShader
, LinearGradient
, RadialGradient
, SweepGradient
, ComposeShader
。
BitmapShader
:图片填充。
LinearGradient
, RadialGradient
, SweepGradient
:渐变填充。
ComposeShader
:叠加前面的某两种。可选择PorterDuff混合模式。
如果既调用了setColor,又调用了setShader,则setShader生效。如果同时用setColor或setAlpha设置了透明度,则透明度也会生效。(会和Shader的透明度叠加)
如果使用drawBitmap输入一个只有alpha的图片(可用Bitmap.extractAlpha
方法获得),则会以alpha
图片为mask
,绘制出shader/color
的颜色。
ColorFilter
通过ColorFilter
可以对一次绘制的所有像素做一个通用处理。
Paint.setColorFilter
: LightingColorFilter
, PorterDuffColorFilter
, ColorMatrixColorFilter
。
这可以整体上改变这一次draw的内容,比如让颜色更暗、更亮等。
这里重点介绍下ColorMatrixColorFilter
。
ColorMatrix
是4x5矩阵,定义其每个元素如下:
{ a, b, c, d, e,
f, g, h, i, j,
k, l, m, n, o,
p, q, r, s, t }
则ColorMatrix的最终运算方式如下:
R' = a*R + b*G + c*B + d*A + e;
G' = f*R + g*G + h*B + i*A + j;
B' = k*R + l*G + m*B + n*A + o;
A' = p*R + q*G + r*B + s*A + t;
绘图API架构
整个绘制流水线大概如下:(我们能定义的部分用蓝色表示)
考虑动画实现的时候一般从两个角度来思考:
宏观角度:有几个变化量,分别是什么。动画从开始到结束的流程。
微观角度:从某一帧上去想,在变化量为某个数值时的图像,该怎么绘制。
把这两者分开去想,就会比较清晰。
PPT里有示例,可以参照DEMO来熟悉:
PPT中的内容:
Canvas.draw*
Point/s
Line/s
Rect, RoundRect, Circle, Oval, Arc, Path
Text
Bitmap, BitmapMesh
Color, Paint
填充色:setColor
or setShader
(设置Shader
后,Color
的alpha
仍会生效,RGB
无效)
BitmapShader
, LinearGradient
, RadialGradient
, SweepGradient
, ComposeShader
BitmapShader
比直接drawBitmap多哪些能力:
Tile
控制:重复、镜面重复。- 绘制各种形状下的图片,
drawBitmap
只限于矩形。 - 配合
ComposeShader
,可和其他Shader
一起用PorterDuff
模式混合。(比如做渐变倒影)
案例1:圆形进度条
方法一:多图法。
制作足够多的图片文件,比如100张,序号为1的为1%的进度图。
当要显示百分之几的时候就读取并绘制哪张图。
伪代码:
void drawFrame(Canvas canvas, float ratio) {
根据ratio读取对应图片;
canvas.draw读取出的图片;
}方法二:逐像素改图法。
基于方法一,但只制作100%时的圆环图片。
当要显示百分之几的时候,在内存里修改这个图的拷贝,使其只包含需要的部分。然后绘制它。
关键部分伪代码:
int[] pixels;
bitmap.getPixels(pixels…);
for循环 (遍历每个像素点) {
根据像素点位置决定像素是否保留,不该显示的则:pixels[i]=0;
}
drawBitmap(pixels,…);备注:可以不用int[]而改用Bitmap,用setPixel修改每一个像素,但这样setPixel方法调用会成为性能瓶颈。
方法三:PorterDuff层层绘制法。
制作100%时的图片:圆环图片。
当要显示百分之几的时候,先在一个离屏缓冲(可以是临时的Bitmap)里绘制纯色的百分比,再用Paint.setXfermode(PorterDuff.SRC_IN)的方式绘制圆环图片。然后绘制这个离屏缓冲。
关键部分伪代码:
创建离屏缓冲:canvas.saveLayer(…);
绘制纯色的百分比扇形:canvas.drawArc(ratio*360,…);
设置模式:paint.setXfermode(SRC_IN);
绘制圆环图片:canvas.drawBitmap(圆环图片,paint);
将离屏缓冲绘制到canvas上:canvas.restore();备注:
- 绘图顺序反过来是不行的。
- 如果此时Canvas上对应区域是空的,则不需要用离屏缓冲.
方法四:Shader一次绘制法。
制作100%时的图片:圆环图片。创建一个BitmapShader对应此图片。
当要显示百分之几的时候,直接用drawArc绘制,将上面的BitmapShader作为参数。
关键部分伪代码:
BitmapShader bitmapShader = new BitmapShader(圆形图片…);
paint.setShader(bitmapShader);canvas.drawArc(…, paint);
案例2:散落
分析
对逐个pixel进行操作是唯一选择。
实现方式
开始动画前将Bitmap内容取到int[] pixels中。
创建一个相同大小的数组存放每个pixel的额外信息,如这个点每次位移多少。
初始化时用随机数或根据动画需要生成出额外信息。
开始动画后每帧draw之前先调用一个updatePixels方法来更新pixels的信息,这里会根据额外信 息对每个点做移动。然后drawBitmap(pixels,…)。
性能优化
这里的updatePixels会是显著的性能瓶颈,因此所有优化点都在它身上。
C++实现updatePixels方法。
用双缓冲模式。updatePixels操作在后台线程计算。这样计算和draw就是并行的。
还可利用多核,分块计算updatePixels。
动画时图片分辨率降低。比如长宽各除以2,用1个点代表之前的4个点。这样计算量就是之前的1/4了。
案例2:圆形进度2 (带拖影效果的圆形进度条)
分析
和上一个比,这个的每一帧需要两个变化量,一个总体进度,一个拖影长度。
“多图法”在这里搞不定。
“逐像素改图法”仍然可用。
“PorterDuff层层绘制”需要结合Shader才行。
“Shader一次绘制”也要使用PorterDuff模式,在这里仍然是最佳的。(ComposeShader)
Shader一次画法
设计师将红色拖影设计成一张圆环状的图片,图片里的红色都是一样的,不需要渐变。
用ComposeShader来绘制红色拖影,包含一个SweepGradient和一个BitmapShader,用PorterDuff模式DST_IN。
在SweepGradient上需要设置localMatrix,通过matrix旋转来跟随动画的旋转(否则需要不停创建SweepGradient)。
将ComposeShader以drawArc的方式绘制出来。拖影的长度通过drawArc的参数控制。
[悲剧的是,我在米3尝试做这个的DEMO的时候发现调用drawArc来绘制ComposeShader有bug,于是放弃。]
案例3:桌面图标按下状态
分析
由于图标有第三方的,不全是我们自己能控制的,因此“多图法”不可用。
“逐像素改图法”仍可用。
“PorterDuff层层绘制”可以用。
“Shader一次绘制”也可以用。
这里还有一种新方法:“ColorFilter法”。
ColorFilter法
三种ColorFilter在这里都可以达到效果:LightingColorFilter, PorterDuffColorFilter,ColorMatrixColorFilter。
LightingColorFilter(0xff555555, 0x00000000)
PorterDuffColorFilter(0xAA000000, Mode.SRC_ATOP)
ColorMatrixColorFilter(new float[]{
0.3f, 0f, 0f, 0f, 0f,
0f, 0.3f, 0f, 0f, 0f,
0f, 0f, 0.3f, 0f, 0f,
0f, 0f, 0f, 1f, 0f,
})
案例4:变黑白
分析
由于彩色变黑白这一点比较特殊,因此这里的关键点是怎么得到黑白图片。
逐个像素更改仍然是可以的。
这里衍生出一个方法:动画前用上面方式生成一张完全黑白的图。然后同时叠加显示两张图,彩色的图alpha从1到0,黑白的图从0到1。这样达到慢慢过渡的效果。很多类似的过渡效果都可以这样。
还有个方法,之前提到ColorMatrix可以更改饱和度,因此可以在这里使用。先找到将颜色变纯黑白的ColorMatrix,然后在每帧动画的时候以插值的方式更改ColorMatrix即可。
案例5:倒影
分析
逐个像素更改可以。
Drawable层层绘制可以。
ComposeShader一次绘制也可以:ComposeShader:LinearGradient+BitmapShader(SRC_IN)
[备注:scale(1, -1)会使绘制是倒过来的。]
案例6:阿拉灯神灯
分析
这个典型的画面扭曲,适合用drawBitmapMesh。
提前构建好drawBitmapMesh需要的参数buffer,每次draw的时候根据进度更新这个buffer即可。
具体的构建方式请看DEMO源码。
自己:
说实话最后那点没看懂