Android中的Frame动画

相信有Android手机的人都玩过一款Kuba的游戏(没玩过的我推荐去玩一下),里面用手指接触到屏幕后产生的爆炸效果确实增加了游戏的不少色彩。那么这个是怎么做出来的呢?

 

很明显,这个效果应该是一个动画序列图实现的,即Frame-by-Frame动画。Android实现Frame-by-Frame动画我会的有两种方法:

 

1、animation-list配置,预先将一个动画按照每帧分解成的多个图片所组成的序列。然后再在Android的配置文件中将这些图片配置到动画里面。

 

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item android:drawable="@drawable/explode1" android:duration="50" />
    <item android:drawable="@drawable/explode2" android:duration="50" />
    <item android:drawable="@drawable/explode3" android:duration="50" />
    <item android:drawable="@drawable/explode4" android:duration="50" />
</animation-list>

 

但是由此带来的不便也是显而易见的:drawable目录下拥挤了过多的动画帧文件。如果游戏大起来,动画效果丰富,那么drawable目录下将拥有数量庞大的图片文件,这将是开发人员的灾难(见下图)。


Android中的Frame动画

 

2、AnimationDrawable动画。其实我们发现,我们完全可以将同一动画序列的每帧图片都合并到一个大的图片中去,然后读取图片的时候按照约定好的宽、高去读就能准确的将该帧图片精确的读出来了。下图是小雪行走序列图。


Android中的Frame动画
 将序列图读出并且转化为动画的核心代码为

animationDrawable = new AnimationDrawable();
Bitmap[] bitmaps = new Bitmap[PlayerConst.PLAYER_XIAOXUE_WALK_FRAME];
for (int frame = 0; frame < bitmaps.length; frame++) {
	Bitmap bitmap = Bitmap.createBitmap(xiaoxueWalkSerBitmap, 
			frame*PlayerConst.PLAYER_XIAOXUE_WALK_WIDTH, 
			lay*PlayerConst.PLAYER_XIAOXUE_WALK_HEIGHT, 
			PlayerConst.PLAYER_XIAOXUE_WALK_WIDTH,
			PlayerConst.PLAYER_XIAOXUE_WALK_HEIGHT);
	animationDrawable.addFrame(new BitmapDrawable(bitmap),100);
}// for,每层有 PLAYER_XIAOXUE_WALK_FRAME 帧
animationDrawable.setOneShot(false);
setBackgroundDrawable(animationDrawable);

 具体例子可以从附件中找到。

 

3、SurfaceView动画。也许你很快就发现,前两个动画都必须依赖View才能展示,并且每个View只能展示一个动画。而在游戏中不可能只有一动画,更恐怖的是很多动画都是随机产生的,并不是事先约定好的,而动态创建/删除View的代价非常高,并不适合做高性能的游戏。这个时候你需要的是SurfaceView。

 

在SurfaceView中的动画有一点是和前边两种动画有区别的:那就是画布上所有的一切都必须自己亲自打理。在前边几个基于Animation的动画你只需关心当前动画的序列即可,其他都由系统帮你处理完毕。而在SurfaceView中,你就是那个处理程序,所有的一切包括背景都必须有你来亲自打理。

 

为此我写了一个框架专门来处理这个琐事,框架只有两个类:AnimationDraw和DrawRunning。其中AnimationDraw则是一个动画类,它负责描述当前动画元素的位置、当前播放到第几帧、每帧的延时是多少、是否重复播放等。

 

import java.util.Date;

import android.graphics.Bitmap;

/**
 * 动画绘画元素
 * @author vlinux
 *
 */
public class AnimationDraw {

	protected float x;
	protected float y;
	protected Bitmap[] bitmaps;
	protected long duration;
	
	protected Long lastBitmapTime;
	protected int step;
	protected boolean repeat;
	
	/**
	 * 动画构造函数-for静态图片
	 * @param x:X坐标<br/>
	 * @param y:Y坐标<br/>
	 * @param bitmap:显示的图片<br/>
	 * @param duration:图片显示的时间<br/>
	 */
	public AnimationDraw(float x, float y, Bitmap bitmap, long duration) {
		Bitmap[] bitmaps = {bitmap};
		this.x = x;
		this.y = y;
		this.bitmaps = bitmaps;
		this.duration = duration;
		this.repeat = true;
		lastBitmapTime = null;
		step = 0;
	}
	
	/**
	 * 动画构造函数
	 * @param x:X坐标<br/>
	 * @param y:Y坐标<br/>
	 * @param bitmap:显示的图片<br/>
	 * @param duration:图片显示的时间<br/>
	 * @param repeat:是否重复动画过程<br/>
	 */
	public AnimationDraw(float x, float y, Bitmap[] bitmaps, long duration, boolean repeat) {
		this.x = x;
		this.y = y;
		this.bitmaps = bitmaps;
		this.duration = duration;
		this.repeat = repeat;
		lastBitmapTime = null;
		step = 0;
	}
	
	
	public Bitmap nextFrame() {

		if (step >= bitmaps.length) {
			// 判断step是否越界
			if( !repeat ) {
				return null;
			} else {
				lastBitmapTime = null;
			}//if
		}// if

		if (null == lastBitmapTime) {
			// 第一次执行
			lastBitmapTime = new Date().getTime();
			return bitmaps[step = 0];
		}// if

		// 第X次执行
		long nowTime = System.currentTimeMillis();
		if (nowTime - lastBitmapTime <= duration) {
			// 如果还在duration的时间段内,则继续返回当前Bitmap
			// 如果duration的值小于0,则表明永远不失效,一般用于背景
			return bitmaps[step];
		}// if
		lastBitmapTime = nowTime;
		return bitmaps[step++];// 返回下一Bitmap
	}
	
	public float getX() {
		return x;
	}

	public float getY() {
		return y;
	}
	
}
 

DrawRunning则是一个负责画图的线程,它是程序的核心。

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.view.SurfaceHolder;

/**
 * 绘画线程
 * 
 * @author vlinux
 * 
 */
public class DrawRunning implements Runnable {

	private List<AnimationDraw> animationDraws;//所有需要画动画的集合
	private List<AnimationDraw> buffers;//缓存前台传入需要展示的动画
	private SurfaceHolder surfaceHolder;
	private boolean running;

	public DrawRunning(SurfaceHolder surfaceHolder) {
		this.surfaceHolder = surfaceHolder;
		animationDraws = new ArrayList<AnimationDraw>();
		buffers = new ArrayList<AnimationDraw>();
		running = true;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		while (running) {
			synchronized (surfaceHolder) {
				Canvas canvas = null;
				try {
					canvas = surfaceHolder.lockCanvas(null);
					doDraw(canvas);
				} finally {
					if (null != canvas) {
						surfaceHolder.unlockCanvasAndPost(canvas);
					}// if
				}// try
			}// syn
		}// while
	}

	private void doDraw(Canvas canvas) {
		synchronized(this) {
			//检查缓存中是否有需要加入的动画
			if( !buffers.isEmpty() ) {
				animationDraws.addAll(buffers);//加入animationDraws
				buffers.clear();//清空缓存
			}//if
		}//syn
		if( animationDraws.isEmpty() ) {
			return;//如果animationDraws里面是空的那就不用画了
		}//if
		//---这里开始绘画
		Iterator<AnimationDraw> bombIt = animationDraws.iterator();
		while (bombIt.hasNext()) {
			AnimationDraw bomb = bombIt.next();
			Bitmap nextFrame = bomb.nextFrame();
			if (null == nextFrame) {
				//下一Frame为null,说明动画序列已经结束
				//该动画已经完成,从动画集合中删除
				bombIt.remove();
				continue;//while
			}// if
			canvas.drawBitmap(nextFrame, bomb.getX(), bomb.getY(), null);
		}// while
	}

	public void addAnimationDraw(AnimationDraw bomb) {
		synchronized(this) {
			//尽量减少这个的同步响应时间,因为这个方法是前台响应的
			//多0.1秒都会直接反应到用户感知
			buffers.add(bomb);//将需要显示动画的内容加入到缓存
		}//syn
	}

	public void stopDrawing() {
		running = false;
	}

}

 

值得注意的是,我用了一个缓存和两个synchronized来提高前台的响应以及确保对集合类、SurfaceHolder的正确操作。

例子可以见附件。