动画类型
- 帧动画:通过序列帧实现,间隔一段时间播放一张图片。实现简单,但是性能最差
- 补间动画:输入动画类型(透明度,大小,移动,旋转),开始参数和结束参数。通过插值器控制变化速度。实现相对简单,但是并没有真正改变view的所在位置,只是显示变化。
- 属性动画:ValueAnimator,ObjectAnimator和animatorSet。动画优化的核心,见后续详情
帧动画优化
实现
动画xml文件:
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@mipmap/ic_launcher"
android:duration="200" />
<item
android:drawable="@mipmap/ic_launcher"
android:duration="300" />
<item
android:drawable="@mipmap/ic_launcher"
android:duration="400" />
<item
android:drawable="@mipmap/ic_launcher"
android:duration="600" />
<item
android:drawable="@mipmap/ic_launcher"
android:duration="700" />
<item
android:drawable="@mipmap/ic_launcher"
android:duration="800" />
</animation-list>
调用
imageView.setImageResource(R.drawable.anim_test);
AnimationDrawable animationDrawable = (AnimationDrawable) imageView.getDrawable();
animationDrawable.start();
关注点:
- oneshot控制是否循环,建议不用试hanlder控制循环
- 每帧的时间都可以不一样。可以用于控制停顿
- 代码动态添加帧:AnimationDrawable.addFrame(Drawable,time)
- 所有的view的绘制,都需要获取焦点才能成功(详见window机制)。所以需要调用
animationDrawable.start();才能执行动画
优化
帧动画实际上是很难优化的,因为固定要读取bitmap。主要有以下两种优化方式:
- 内存优化:控制bitmap的总数,不使用的bitmap主动回收了,但是这样会导致性能的下降。适合与低内存的手机;另一方面,动画人员给的序列帧是通过工具生成的,有可能生成相同的图片。我们可以复用他们,比如上下左右对称的图片,旋转一周,肯定有存在相同的4张图片。
- 读取优化:提前读取一些图片。
补间动画
类型:直接引用他人总结的
实现:
- 在xml中配置,然后通过 AnimationUtils.loadAnimation(this, R.anim.view_animation)加载
- 代码中设置起始参数,结束参数和插值器
详情参考:https://www.jianshu.com/p/733532041f46
个人认为关于补间动画的实现已经写的很详细了。
关注点
- 补间动画适合于效果单一的动画,比如单纯的平移,旋转等
- 如果效果复杂。建议修改为属性动画再优化,因为每个类型,每个view就需要一个相应动画。这样就会出现很多的动画实例
属性动画
- ValueAnimator:值动画,实际上只是根据时间规律生成一系列数值(int,float,alpha,argb等),然后我们就可以把这些值设置给相应的view操作。让view动起来
- ObjectAnimator:对象动画。是ValueAnimator的实现类之一,需要绑定view对象,通过set/get方法自动把生成的一系列数值设置给相应的参数。set/get方法支持原有的方法,也支持自定义。
- animatorSet:动画组合。用户组合多个属性动画,让他们同时进行或者排序进行。比如多个动画同时进行,通过animatorSet就可以一时间产生多种数值,提高效率。原来的方式为各自的动画各自产生序列。
适用场景:
ObjectAnimator适用于只有一个view。如果多个view,为了更好的性能,则建议适用ValueAnimator
ValueAnimator使用相对麻烦一些,适合于多view
优化
原则:尽量减少动画对象
居于这个原则有以下优化方法:
- 使用animatorSet:通过set统一控制的动画的执行和顺序,但是animatorSet并不支持循环。如果非要循环,建议外部加一层handle,因为动画有可能被中断。
- 对于单view,多效果使用PropertyValuesHolder结合ObjectAnimator。可以只用一个动画对象实现多个效果。以下实现消失动画(逐渐变小,逐渐透明直至完全消失)
PropertyValuesHolder scaleValuesHolder = PropertyValuesHolder.ofFloat("scale", 1,0); PropertyValuesHolder alphaValuesHolder = PropertyValuesHolder.ofFloat("alpha", 1,0); ObjectAnimator.ofPropertyValuesHolder(this, scaleValuesHolder, alphaValuesHolder).start();
- 对于多view,多效果使用PropertyValuesHolder结合ValueAnimator。可以只用一个动画对象实现多个view,多个效果。以下实现两个view的消失动画(逐渐变小,逐渐透明直至完全消失)
final TextView textView1 = findViewById(R.id.tv_test); final TextView textView2 = findViewById(R.id.tv_test2); PropertyValuesHolder scaleValuesHolder = PropertyValuesHolder.ofFloat("scale", 1,0); PropertyValuesHolder alphaValuesHolder = PropertyValuesHolder.ofFloat("alpha", 1,0); ValueAnimator valueAnimator = ValueAnimator.ofPropertyValuesHolder(scaleValuesHolder,alphaValuesHolder); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { textView1.setAlpha((float)animation.getAnimatedValue("alpha")); textView1.setScaleX((float)animation.getAnimatedValue("scale")); textView1.setScaleY((float)animation.getAnimatedValue("scale")); textView2.setAlpha((float)animation.getAnimatedValue("alpha")); textView2.setScaleX((float)animation.getAnimatedValue("scale")); textView2.setScaleY((float)animation.getAnimatedValue("scale")); } });
- 如果动画更复杂,比如先执行scale(500ms),动画开始后100ms执行透明度动画(400ms),然后停顿1.5s再循环。这样的效果以上的方式都无法通过一个动画实现。可以使用keyframe。
final TextView textView1 = findViewById(R.id.tv_test); Keyframe scaleKf0 = Keyframe.ofFloat(0f, 0); Keyframe scaleKf1 = Keyframe.ofFloat(0.25f, 1); Keyframe scaleKf2 = Keyframe.ofFloat(1f, 1); PropertyValuesHolder scaleValuesHolder = PropertyValuesHolder.ofKeyframe("scale", scaleKf0, scaleKf1, scaleKf2); Keyframe alphaKf0 = Keyframe.ofFloat(0f, 0); Keyframe alphaKf1 = Keyframe.ofFloat(0.05f, 0); Keyframe alphaKf2 = Keyframe.ofFloat(0.25f, 1); Keyframe alphaKf3 = Keyframe.ofFloat(1f, 1); PropertyValuesHolder alphaValuesHolder = PropertyValuesHolder.ofKeyframe("alpha",alphaKf0, alphaKf1, alphaKf2, alphaKf3); ObjectAnimator.ofPropertyValuesHolder(textView1,scaleValuesHolder,alphaValuesHolder);
keyframe第一个参数为进度百分比(受插值器控制,如果不是LinearInterpolator,可能很快就到达一半了),第二参数为value。上一帧到下一帧如果不一样可以实现动画,如果一样不处理。还可以独立设置每一个关键帧的插值器。同样,单view使用ObjectAnimator,多view使用valueAnimator。再复杂的动画都可以通过keyframe用最少的动画实现效果。
最终篇-渲染优化
以上的优化思路还停留在不改变view渲染的。有几个动画元素就用多少个view。只是减少了动画对象。但是根据view的渲染机制,并没有优化多少,就是生成插值的内存和性能优化了。效果并不是很好。
以下实践总结的优化:
- 通过canvas,Paint画图/画圆/画矩形等等,结合valueAnimator,改变值以改变位置,大小或者形状。调用invalidate刷新view。
- 通过canvas,Paint画图/画圆/画矩形等等,结合ObjectAnimator,提供相应的set/get方法。
该方式使用较为复杂,可以结合以上的所以情况,无论多么复杂,都可以用一个view,最少的动画对象实现。需要深刻了解自定义view,view渲染和动画机制。将三者结合,实现最佳的性能。
还有很多细节,set/get方法不能混淆。使用keyframe时,如果效果没有发现变化,就不用调用invalidate
以下是实现两个圆,同时平移和缩放。
package com.example.admin.myapplication;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
public class MyAnimView extends View {
private Paint mPaint;
private float mX;
private float mY;
private float mRadius;
public MyAnimView(Context context) {
super(context);
init();
}
public MyAnimView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyAnimView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
}
public void startAnim() {
PropertyValuesHolder scaleValuesHolder = PropertyValuesHolder.ofFloat("scale", 0, 100);
PropertyValuesHolder moveValuesHolder = PropertyValuesHolder.ofFloat("move", 0, 200);
ValueAnimator valueAnimator = ValueAnimator.ofPropertyValuesHolder(scaleValuesHolder, moveValuesHolder);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mRadius = (float) animation.getAnimatedValue("scale");
mX = (float) animation.getAnimatedValue("move");
mY = (float) animation.getAnimatedValue("move");
invalidate();
}
});
valueAnimator.setDuration(1000);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.start();
}
@Override
public void onDraw(Canvas canvas) {
canvas.drawCircle(mX, mY, mRadius, mPaint);
canvas.drawCircle(mX + 100, mY + 100, mRadius, mPaint);
}
}
总结
场景 | 实现方式 | 说明 |
时间紧张,没追求 | 帧动画 | 实现简单,性能极差 |
一个view,简单的动效 | 补间动画 | 实现简单,性能一半,不能改变点击响应区域 |
一个view,简单的动效,需要改变点击响应区域 | ObjectAnimator | 实现较简单,性能一般 |
一个view,多个动效 | ObjectAnimator结合PropertyValuesHolder | 实现难度一般,性能优秀 |
多个view,多个动效 | valueAnimator结合PropertyValuesHolder | 实现难度一般,性能优秀 |
一个view,多个动效,但是启动时间,结束时间不一样 | ObjectAnimator,PropertyValuesHolder结合keyframe | 实现难度较难,性能很优秀 |
多个view,多个动效,但是启动时间,结束时间不一样 | valueAnimator,PropertyValuesHolder结合keyframe | 实现难度较难,性能很优秀 |
复杂的动画的最佳性能优化 | canvas,paint结合valueAnimator或者PropertyValuesHolder | 实现最难,性能最佳 |
如果能用简单的动画实现就简单实现,追求性能的时候再考虑复杂的实现的。