本文对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); -
} -
}