12、View动画之自定义View动画:Canvas动画、PathMeasure动画、SVG动画
说实话,讲完前面那些补间动画和属性动画,很多同学会觉得——嗯,动画也就这样了。但真正到了复杂UI场景,你会发现系统提供的那些API根本不够用。
举个例子,我之前做过一个车载导航项目,需要模拟车辆沿复杂道路行驶的轨迹动画。用属性动画?根本搞不定。最后还是得撸起袖子,自己写自定义View动画。
这一章,我们就来聊聊自定义View动画的三种核心玩法:Canvas动画、PathMeasure动画、SVG动画。说白了,就是让你能控制每一帧画什么、怎么画。
12.1 Canvas动画——最底层的绘制控制
Canvas动画,说白了就是你在 onDraw() 方法里自己画每一帧。你想想看,系统动画无非是帮你调用了 setTranslationX()、setRotation() 这些方法。但如果你要画一个自定义的仪表盘指针旋转,或者一个水波纹扩散效果,系统动画就帮不上忙了。
我个人的习惯是:能用属性动画解决的,绝不用Canvas动画。因为Canvas动画需要你自己处理刷新、性能、坐标系变换,坑比较多。但一旦遇到属性动画搞不定的场景,Canvas动画就是你的王牌。
来看一个典型的Canvas动画实现——让一个圆形沿着正弦曲线运动:
public class SinWaveView extends View {
private float phase = 0f;
private ValueAnimator animator;
public SinWaveView(Context context) {
super(context);
animator = ValueAnimator.ofFloat(0f, 2f * (float) Math.PI);
animator.setDuration(2000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(animation -> {
phase = (float) animation.getAnimatedValue();
invalidate(); // 关键:触发重绘
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float width = getWidth();
float height = getHeight();
float centerY = height / 2f;
// 绘制轨迹线
Paint pathPaint = new Paint();
pathPaint.setStyle(Paint.Style.STROKE);
pathPaint.setColor(Color.LTGRAY);
Path path = new Path();
for (float x = 0; x <= width; x++) {
float y = centerY + 200f * (float) Math.sin(2 * Math.PI * x / width + phase);
if (x == 0) path.moveTo(x, y);
else path.lineTo(x, y);
}
canvas.drawPath(path, pathPaint);
// 绘制运动的小球
float ballX = width / 2f;
float ballY = centerY + 200f * (float) Math.sin(phase);
Paint ballPaint = new Paint();
ballPaint.setColor(Color.RED);
ballPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(ballX, ballY, 20f, ballPaint);
}
}
核心要点:
- 用
ValueAnimator驱动数值变化,在监听器中调用invalidate() - 在
onDraw()中根据当前数值计算绘制内容 - 注意
onDraw()中不要创建对象,避免GC导致卡顿
我曾经踩过的坑:在 onDraw() 里 new Paint() 和 new Path()。那时候做的是一个实时心率图,每帧都new对象,结果GC频繁触发,动画一卡一卡的。后来改成成员变量复用,问题就解决了。
12.2 PathMeasure动画——让物体沿路径运动
PathMeasure 这个类,说实话,是Android动画里被低估的一个工具。它能把一个 Path 对象拆解成坐标和切线信息。什么意思呢?就是你画一条曲线,PathMeasure 能告诉你「在这条曲线的50%位置,坐标是多少,方向朝哪」。
我在项目中做过一个功能:地图上显示车辆从A点到B点的行驶动画。如果用属性动画直接移动,车辆不会沿着道路转弯,看起来像瞬移。用 PathMeasure 就完美解决了。
核心用法就两个方法:
| 方法 | 作用 |
|---|---|
getPosTan(float distance, float[] pos, float[] tan) |
获取路径上指定距离处的坐标和切线方向 |
getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) |
截取路径的一段,用于描边动画 |
来看一个路径描边动画的经典实现——让一条线从无到有地「画」出来:
public class PathTraceView extends View {
private Path path;
private PathMeasure pathMeasure;
private float pathLength;
private float progress = 0f;
public PathTraceView(Context context) {
super(context);
path = new Path();
path.moveTo(100, 100);
path.cubicTo(300, 50, 200, 300, 500, 200);
pathMeasure = new PathMeasure(path, false);
pathLength = pathMeasure.getLength();
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(3000);
anim.setRepeatCount(ValueAnimator.INFINITE);
anim.addUpdateListener(animation -> {
progress = (float) animation.getAnimatedValue();
invalidate();
});
anim.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(8f);
paint.setColor(Color.BLUE);
paint.setStrokeCap(Paint.Cap.ROUND);
Path dst = new Path();
float stopD = pathLength * progress;
pathMeasure.getSegment(0, stopD, dst, true);
canvas.drawPath(dst, paint);
}
}
小技巧:如果你想让一个图标沿着路径运动,用 getPosTan() 获取坐标,再用 Math.atan2(tan[1], tan[0]) 计算旋转角度,就能让图标始终朝向运动方向。我在地图导航项目里就是这么做的。
12.3 SVG动画——矢量图的动画魔法
SVG动画在Android里,其实是通过 VectorDrawable 和 AnimatedVectorDrawable 实现的。你想想看,一个SVG图标,能不能让它像画出来一样?或者让一个箭头从A平滑变形到B?这些就是SVG动画的强项。
我记得第一次接触这个,是在做启动页Logo动画。设计师给了一个SVG格式的Logo,要求从中心向外扩散显示。如果用帧动画,资源包会很大。用SVG动画,一个XML文件就搞定了。
实现步骤分三步:
- 准备SVG路径数据——用
<vector>标签定义矢量图 - 定义动画——用
<objectAnimator>驱动trimPathStart或trimPathEnd属性 - 组合——用
<animated-vector>将两者关联
来看一个最简单的描边动画XML:
<!-- res/drawable/ic_heart.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:name="heart"
android:pathData="M50,85 L20,50 A20,20 0 0,1 50,30 A20,20 0 0,1 80,50 Z"
android:strokeColor="#FF0000"
android:strokeWidth="3"
android:fillColor="@android:color/transparent"
android:trimPathStart="0"
android:trimPathEnd="0" />
</vector>
<!-- res/drawable/avd_heart.xml -->
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_heart">
<target
android:name="heart"
android:animation="@animator/heart_draw" />
</animated-vector>
<!-- res/animator/heart_draw.xml -->
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="trimPathEnd"
android:duration="2000"
android:valueFrom="0"
android:valueTo="1"
android:repeatCount="infinite" />
关键属性说明:
trimPathStart:从路径的起始位置裁剪(0~1)trimPathEnd:从路径的结束位置裁剪(0~1)- 两者配合使用,可以实现「从中间向两边展开」的效果
我曾经踩过的坑:SVG路径数据中的坐标必须与 viewportWidth/viewportHeight 匹配。有一次我从设计师那里拿到的SVG是1000x1000的viewport,但我写成了100x100,结果图标显示不全。后来统一约定用100x100的viewport,路径坐标按比例缩放。
12.4 三种动画的选型对比
说了这么多,到底什么时候用哪种?我根据自己的项目经验,整理了一个对比表:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 自定义绘制(图表、仪表盘) | Canvas动画 | 需要完全控制绘制逻辑 |
| 物体沿复杂路径运动 | PathMeasure动画 | 天然支持路径坐标和方向计算 |
| 图标描边/变形动画 | SVG动画 | 资源小、适配好、支持矢量缩放 |
| 简单位移/旋转/缩放 | 属性动画 | 系统自带,性能最优 |
嗯,到这里,自定义View动画的三种核心玩法就讲完了。Canvas动画给你最底层的绘制自由,PathMeasure动画让路径运动变得简单,SVG动画则把矢量图的优势发挥到极致。这三种技术,基本能覆盖90%以上的复杂动画需求。
下一章,我们会聊聊属性动画的进阶用法——插值器和估值器的自定义。不过那是后话了,先把今天的内容消化掉。
公众号:蓝海资料掘金营,微信deep3321