前言

逐帧动画 (Frame By Frame) 是 Android 系统提供的一种常见的动画形式,通过播放一组连续的图片资源形成动画。当我们想用一组连续的图片播放动画时,首先想到的就是使用系统提供的逐帧动画方式。接下来,我们将简单说明如何使用逐帧动画,以及分析逐帧动画存在的优缺点,最后给出我们的解决方案。

逐帧动画

第一步,将我们所需要的动画素材资源放置在 res/drawable 目录下,切记不要因为是动画所以就错误的将素材资源放置在 res/anim 目录下。

第二步,在 res/anim 目录下新建 drawable 文件 loading.xml ,如下


animation-list 为 drawable 文件的根标签,android:oneshot 设置动画是否只播放一次,子标签 item 具体定义每一帧的动画,android:drawable 定义这一帧动画所使用的资源,android:duration 设置动画的持续时间。

第三步,给想要显示动画的 ImageView 设置资源动画,然后开启动画


我们能看到,逐帧动画使用起来是如此的简单方便,所以当我们想要通过一组图片素材来实现动画的时候首选的就是以上的方案。但是我们却忽略了一个情况,当图片素材很多并且每张图片都很大的情况下,使用以上的方法手机会出现 OOM 以及卡顿问题,这是帧动画的一个比较明显的缺点。

为什么帧动画会出现 OOM 以及卡顿?

我们知道,在第三步给 ImageView 设置图片资源的时候,因为 loading.xml 文件中定义了一系列的图片素材,系统会按照每个定义的顺序把所有的图片都读取到内存中,而系统读取图片的方式是 Bitmap 位图形式,所以就导致了 OOM 的发生。

解决方案

既然一次性读取所有的图片资源会导致内存溢出,那么我们能想到的解决方法就是按照动画的顺序,每次只读取一帧动画资源,读取完毕再显示出来,如果图片过大,我们还需要对图片进行压缩处理。

技术实现

总体思路是这样的,我们在子线程里读取图片资源(包括图片过大,对图片进行处理),读取完毕后通过主线程的 Handler 将在子线程的数据(主要是 Bitmap)发送到主线程中,然后再把 Bitmp 绘制显示出来,每隔一段时间不断读取,然后依次显示出来,这样视觉上就有了动画的效果。实现代码如下

public class AnimationView extends View implements Handler.Callback {
public static final int DEFAULT_ANIM_TIME = 100;
public static final int PROCESS_DATA = 1;
public static final int PROCESS_ANIM_FINISH = 1 << 1;
public static final int PROCESS_DELAY = 1 << 2;
public AnimData mCurAnimData;
public int mCurAnimPos;
public boolean mIsRepeat;
public int mAnimTime;
private Handler mHandler ;
private ProcessAnimThread mProcessThread;
private Bitmap mCurShowBmp;
private List mAnimDataList = new ArrayList<>();
public AnimationView(Context context) {
this(context,null);
}
public AnimationView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public AnimationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mHandler = new Handler(this);
mProcessThread = new ProcessAnimThread(getContext(),mHandler);
mAnimTime = DEFAULT_ANIM_TIME;
}
public void setIsRepeat(boolean repeat){
mIsRepeat = repeat;
}
private int mGravity;
public void SetGravity(int gravity)
{
mGravity = gravity;
invalidate();
}
public void setData(List list){
if (list != null ){
mAnimDataList.addAll(list);
}
}
private Matrix mTempMatrix = new Matrix();
@Override
protected void onDraw(Canvas canvas) {
if(mCurShowBmp != null && !mCurShowBmp.isRecycled())
{
int x = 0;
int y = 0;
float scaleX = 1f;
float scaleY = 1f;
switch(mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)
{
case Gravity.LEFT:
x = 0;
break;
case Gravity.RIGHT:
x = this.getWidth() - mCurShowBmp.getWidth();
break;
case Gravity.CENTER_HORIZONTAL:
x = (this.getWidth() - mCurShowBmp.getWidth()) / 2;
break;
case Gravity.FILL_HORIZONTAL:
{
int w = mCurShowBmp.getWidth();
if(w > 0)
{
scaleX = (float)this.getWidth() / (float)w;
}
break;
}
default:
break;
}
switch(mGravity & Gravity.VERTICAL_GRAVITY_MASK)
{
case Gravity.TOP:
y = 0;
break;
case Gravity.BOTTOM:
y = this.getHeight() - mCurShowBmp.getHeight();
break;
case Gravity.CENTER_VERTICAL:
y = (this.getHeight() - mCurShowBmp.getHeight()) / 2;
break;
case Gravity.FILL_VERTICAL:
{
int h = mCurShowBmp.getHeight();
if(h > 0)
{
scaleY = (float)this.getHeight() / (float)h;
}
break;
}
default:
break;
}
if(scaleX == 1 && scaleY != 1)
{
scaleX = scaleY;
switch(mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)
{
case Gravity.RIGHT:
x = this.getWidth() - (int)(mCurShowBmp.getWidth() * scaleX);
break;
case Gravity.CENTER_HORIZONTAL:
x = (this.getWidth() - (int)(mCurShowBmp.getWidth() * scaleX)) / 2;
break;
}
}
else if(scaleX != 1 && scaleY == 1)
{
scaleY = scaleX;
switch(mGravity & Gravity.VERTICAL_GRAVITY_MASK)
{
case Gravity.BOTTOM:
y = this.getHeight() - (int)(mCurShowBmp.getHeight() * scaleY);
break;
case Gravity.CENTER_VERTICAL:
y = (this.getHeight() - (int)(mCurShowBmp.getHeight() * scaleY)) / 2;
break;
}
}
mTempMatrix.reset();
mTempMatrix.postScale(scaleX, scaleY);
mTempMatrix.postTranslate(x, y);
canvas.drawBitmap(mCurShowBmp, mTempMatrix, null);
}
}
private boolean mHasStarted = false;
public void start(){
mHasStarted = true;
if (mWidth == 0 || mHeight == 0 ){
return;
}
startPlay();
}
private void startPlay() {
if ( mAnimDataList != null && mAnimDataList.size() > 0 ){
mCurAnimPos = 0;
AnimData animData = mAnimDataList.get(mCurAnimPos);
mCurShowBmp = ImageUtil.getBitmap(getContext(),animData.filePath,mWidth,mHeight);
invalidate();
if (mListener != null ){
mListener.onAnimChange(mCurAnimPos,mCurShowBmp);
}
checkIsPlayNext();
}
}
private void playNext(final int curAnimPosition ){
Message msg = Message.obtain();
msg.what = PROCESS_DELAY;
msg.arg1 = curAnimPosition;
mHandler.sendMessageDelayed(msg,mAnimTime);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
quit();
}
private void quit(){
mHasStarted = false;
if (mProcessThread != null ){
mProcessThread.clearAll();
}
}
private int mWidth;
private int mHeight;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
if (mProcessThread != null ){
mProcessThread.setSize(w,h);
}
if (mHasStarted){
startPlay();
}
}
private boolean mHavePause = false;
public void pause(){
mHavePause = true;
mHandler.removeMessages(PROCESS_DELAY);
}
public void resume(){
if (mHavePause && mHasStarted){
checkIsPlayNext();
}
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what){
case PROCESS_ANIM_FINISH:{
Bitmap bitmap = (Bitmap) msg.obj;
if (bitmap != null){
if (mCurShowBmp != null ){
mCurShowBmp.recycle();
mCurShowBmp = null;
}
mCurShowBmp = bitmap;
if (mListener != null ){
mListener.onAnimChange(mCurAnimPos,bitmap);
}
invalidate();
}
checkIsPlayNext();
break;
}
case PROCESS_DELAY:{
int curAnimPosition = msg.arg1;
AnimData data = mAnimDataList.get(curAnimPosition);
mProcessThread.processData(data);
break;
}
}
return true;
}
private void checkIsPlayNext() {
mCurAnimPos ++;
if ( mCurAnimPos >= mAnimDataList.size() ){
if (mIsRepeat){
mCurAnimPos = 0;
playNext(mCurAnimPos);
} else {
if ( mListener != null ){
mListener.onAnimEnd();
}
}
} else {
playNext(mCurAnimPos);
}
}
private AnimCallBack mListener;
public void setAnimCallBack(AnimCallBack callBack){
mListener = callBack;
}
public interface AnimCallBack{
void onAnimChange(int position, Bitmap bitmap);
void onAnimEnd();
}
public static class AnimData{
public Object filePath;
}
public static class ProcessAnimThread{
private HandlerThread mHandlerThread;
private Handler mProcessHandler;
private Handler mUiHandler;
private AnimData mCurAnimData;
private int mWidth;
private int mHeight;
private WeakReference mContext;
public ProcessAnimThread(Context context, Handler handler){
mUiHandler = handler;
mContext = new WeakReference(context);
init();
}
public void setSize(int width,int height){
mWidth = width;
mHeight = height;
}
private void init(){
mHandlerThread = new HandlerThread("process_anim_thread");
mHandlerThread.start();
mProcessHandler = new Handler(mHandlerThread.getLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
// 消息是在子线程 HandlerThread 里面被处理,所以这里的 handleMessage 在
//子线程里被调用
switch (msg.what){
case PROCESS_DATA:{
AnimData animData = (AnimData) msg.obj;
Bitmap bitmap = ImageUtil.getBitmap(mContext.get(),animData.filePath,mWidth,mHeight);
if (bitmap != null ){
Message finishMsg = Message.obtain();
finishMsg.what = PROCESS_ANIM_FINISH;
finishMsg.obj = bitmap;
//消息处理完毕,使用主线程的 Handler 将消息发送到主线程
mUiHandler.sendMessage(finishMsg);
}
break;
}
}
return true;
}
});
}
public void processData(AnimData animData){
if ( animData != null ){
Message msg = Message.obtain();
msg.what = PROCESS_DATA;
msg.obj = animData;
mProcessHandler.sendMessage(msg);
}
}
public void clearAll(){
mHandlerThread.quit();
mHandlerThread = null;
}
}
}

首先定义静态的内部类 AnimData,作为我们的动画实体类,filePath 为动画的路径,可以是 res 资源目录下,也可以是 外部存储的路径。


接下来定义封装 ProcessAnimThread 类,用以将资源图片读取为 Bitmap,如果图片过大,我们还需要将其压缩处理。ProcessAnimThread 类中,最为关键的是 HandlerThread ,这是自带有 Looper 的 Thread,继承自 Thread。前面我们说过在子线程里读取 Bitmap, HandlerThread 就是我们上面提及的子线程,使用方法上,我们先构造 HandlerThread ,然后调用 start() 方法开启线程,这时候 HandlerThread 里的 Looper 已经启动可以出来消息了,最后通过这个 Looper 构造 Handler(例子中为 mProcessHandler 变量)。完成以上步骤之后,我们通过 mProcessHandler 发送的消息最终会在 子线程里被处理,处理完毕之后,再讲结果发送到主线程


接下来看主线程收到消息后如何处理。首先将结果取出来,然后刷新显示,接着判断队列是否以及处理结束,未结束则通过发送延迟的消息继续读取图片。


AnimationView 使用步骤

构造帧动画数据队列


调用 AnimationView 的 start() 方法开启动画

写在最后

AnimationView 是解决方案里的一个简单实现,由于知识水平有限,难免有错误和遗漏,欢迎指正。