本文对Android中的常用动画技术进行了较为全面的总结,并给出了代码示例(Java代码实现和XML中实现)。由于内容较多,所以尽可能简洁表述,并重点指出其中不易理解、容易出错的内容。
本文提到的Android动画主要有三类:
- Drawable动画
- Animation与AnimationSet
- Animator与AnimatorSet
示例代码、思维导图可在此下载
https://github.com/jzj1993/AndroidAnimation
Animatable Drawable 动画
用Drawable实现动画,适用于不需要变换View,只需要对View中所展示的Drawable图形产生动画的情况。
由于不像Animation需要对View进行矩阵变换,更不需要像Animator使用反射机制,实现同样的效果,Drawable动画通常性能较好,推荐使用。
原理简介
当给一个View的背景、ImageView的src设置了Drawable后,View会将自身设置为Drawable的Drawable.Callback(View实现了Drawable.Callback接口)。于是在Drawable需要刷新时,可通过这个接口调用View的invalidate,从而触发View.onDraw方法进行重绘。具体可参考Android源码。
支持动画的Drawable,应实现Animatable接口。有些View属性会对Drawable进行判断(例如ImageView的src属性),如果实现了Animatable接口,就会自动调用其start方法启动动画。
-
public class ImageView extends View implements Drawable.Callback {
-
public void setImageDrawable(Drawable drawable) {
-
if (mDrawable != drawable) {
-
// ...
-
updateDrawable(drawable);
-
// ...
-
}
-
}
-
private void updateDrawable(Drawable d) {
-
if (mDrawable != null) {
-
mDrawable.setCallback(null);
-
unscheduleDrawable(mDrawable);
-
}
-
mDrawable = d;
-
if (d != null) {
-
d.setCallback(this);
-
if (d.isStateful()) {
-
d.setState(getDrawableState());
-
}
-
d.setLevel(mLevel);
-
d.setLayoutDirection(getLayoutDirection());
-
d.setVisible(getVisibility() == VISIBLE, true);
-
mDrawableWidth = d.getIntrinsicWidth();
-
mDrawableHeight = d.getIntrinsicHeight();
-
applyColorMod();
-
configureBounds();
-
} else {
-
mDrawableWidth = mDrawableHeight = -1;
-
}
-
}
-
}
FrameAnimation 逐帧动画(AnimationDrawable)
逐帧动画实际上就是一种支持动画效果的Drawable
public class AnimationDrawable extends DrawableContainer implements Runnable, Animatable { }
从XML创建,使用Java代码加载:
-
将每一帧的图片放在资源文件夹
res/drawable
-
在XML中定义动画每一帧及其持续时间
-
在Java代码中加载动画并设置给View,然后启动动画
-
OneShot属性为true则只播放一次,否则不断循环播放
-
<?xml version="1.0" encoding="utf-8"?>
-
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
-
android:oneshot="false">
-
<item
-
android:drawable="@drawable/icon1"
-
android:duration="200" />
-
<item
-
android:drawable="@drawable/icon2"
-
android:duration="200" />
-
<item
-
android:drawable="@drawable/icon3"
-
android:duration="200" />
-
<item
-
android:drawable="@drawable/icon4"
-
android:duration="200" />
-
</animation-list>
-
mFrameAnimation = (AnimationDrawable) getResources().getDrawable(R.drawable.frame_anim); // 从XML加载动画
-
mTextView.setBackground(mFrameAnimation);
-
mFrameAnimation.start();
- 也可以在Java代码中实例化AnimationDrawable对象,并添加帧和持续时间
mFrameAnimation = new AnimationDrawable();
mFrameAnimation.addFrame(getResources().getDrawable(R.drawable.icon1), 200);
mFrameAnimation.addFrame(getResources().getDrawable(R.drawable.icon2), 200);
注意:
Android系统提供的这种逐帧动画,通常每一帧是一个BitmapDrawable,会在内存中一直保存每一帧的Bitmap,如果帧数较多、每一帧图片较大,消耗的内存会很大。
如果对内存有要求,可以自行实现逐帧动画,每切换一帧的时候临时加载该帧的BitmapDrawable,这样虽然增加了一些CPU资源消耗,但减少了内存占用。
AnimatedRotateDrawable旋转动画
和逐帧动画类似,Android系统还提供了AnimatedRotateDrawable,可以实现图片旋转的效果,常用于展示进度条动画。
由于构造函数是私有的,AnimatedRotateDrawable不支持在Java代码中实例化,只能从XML加载。
res/drawable/loading.png是一个PNG文件
res/drawable/animated_rotate.xml
<?xml version="1.0" encoding="utf-8"?>
<animated-rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/loading"
android:fromDegrees="0.0"
android:pivotX="50.0%"
android:pivotY="50.0%"
android:toDegrees="360.0" />
注:因为是Drawable,animated_rotate的XML文件应该放在drawable文件夹下。
在XML中引用Drawable即可
-
<ProgressBar
-
android:id="@+id/progress"
-
android:layout_width="50dp"
-
android:layout_height="50dp"
-
android:indeterminate="true"
-
android:indeterminateDrawable="@drawable/animated_rotate" />
-
<ImageView
-
android:id="@+id/image_view"
-
android:layout_width="50dp"
-
android:layout_height="50dp"
-
android:layout_marginTop="60dp"
-
android:src="@drawable/animated_rotate" />
需要注意的是,不同的View和属性,是否会自动启动Drawable动画的行为不同。例如ProgressBar从XML加载indeterminateDrawable是可以启动动画的,但从Java加载则需要显示调用start方法。而ImageView的src属性,则可以自动启动动画。
-
如果用Java代码给ProgressBar的indeterminateDrawable设置Drawable,一般需要用setBounds指定drawable的宽高,并显示调用Animatable.start()方法启动动画。
-
如果用Java代码给ImageView的src设置Drawable,ImageView会自动处理Drawable的尺寸,并判断其是否实现了Animatable接口,从而启动动画。
-
ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress);
-
final Drawable drawable = getResources().getDrawable(R.drawable.animated_rotate);
-
if (drawable != null) {
-
drawable.setBounds(0, 0, progressBar.getWidth(), progressBar.getHeight());
-
progressBar.setIndeterminateDrawable(drawable);
-
if (drawable instanceof Animatable) {
-
((Animatable) drawable).start();
-
}
-
}
-
ImageView imageView = (ImageView) findViewById(R.id.image_view);
-
imageView.setImageDrawable(getResources().getDrawable(R.drawable.animated_rotate));
自定义DrawableAnimation
通过实现Animatable接口,可以自行定义支持动画效果的Drawable。
示例代码是一个每隔0.5s切换一种随机颜色值的Drawable,具体效果可参考工程源码。
-
public class MyAnimDrawable extends Drawable implements Animatable, Runnable {
-
private boolean mRunning = false;
-
@Override
-
public void draw(Canvas canvas) {
-
canvas.drawARGB(128, (int) (Math.random() * 255), (int) (Math.random() * 255), (int) (Math.random() * 255));
-
}
-
@Override
-
public void setAlpha(int alpha) {
-
}
-
@Override
-
public void setColorFilter(ColorFilter cf) {
-
}
-
@Override
-
public int getOpacity() {
-
return 0;
-
}
-
@Override
-
public boolean setVisible(boolean visible, boolean restart) {
-
boolean changed = super.setVisible(visible, restart);
-
if (visible) {
-
if (changed restart) {
-
nextFrame();
-
}
-
} else {
-
unscheduleSelf(this);
-
}
-
return changed;
-
}
-
@Override
-
public void start() {
-
if (!mRunning) {
-
mRunning = true;
-
nextFrame();
-
}
-
}
-
@Override
-
public void stop() {
-
unscheduleSelf(this);
-
mRunning = false;
-
}
-
@Override
-
public boolean isRunning() {
-
return mRunning;
-
}
-
@Override
-
public void run() {
-
invalidateSelf();
-
nextFrame();
-
}
-
private void nextFrame() {
-
unscheduleSelf(this);
-
scheduleSelf(this, SystemClock.uptimeMillis() + 500);
-
}
-
}