作者 吴亚峰
2.5 2D动画的开发
Android 3D游戏开发技术宝典——OpenGL ES 2.0
虽然本书是着重介绍3D的开发技术,但在大部分的3D应用中也需要有不少的2D界面,如菜单、帮助等。本节将介绍一般用于开发游戏中2D界面的SurfaceView类的使用。其继承自View类,但与View的不同之处在于,View更新画面必须是在UI线程中(也可以理解为主线程中),而SurfaceView更新画面可以在自定义线程中进行,大大方便了开发。
提示 关于Android下的多线程问题,读者可以参考笔者在人民邮电出版社出版的《Android应用开发完全自学手册——核心技术、传感器、2D/3D、多媒体与典型案例》一书中第8章的相关内容,那里有比较详细的介绍。
2.5.1 SurfaceView用法简介
实际开发中,一般采用继承SurfaceView进行自定义的方法来开发2D动画效果。开发时不但要继承SurfaceView类,一般还要实现SurfaceHolder.Callback接口。SurfaceView类中绘制界面的方法为onDraw,其具体签名为“protected void onDraw(Canvas canvas)”,每调用一次该方法,就会重新绘制一帧画面。
SurfaceHolder.Callback接口中主要包含了2D界面的3个生命周期相关的回调方法,其方法签名和说明如表2-3所列。
以上3个生命周期回调方法都有其各自的用途,具体情况如下所列。
每次创建界面时需要初始化图片、线程等资源,这些代码一般写在surfaceCreated方法中。
当SurfaceView变化时,如果需要改变一些值,这些代码应该放在surfaceChanged方法中。
SurfaceView被销毁时,有些与界面相关的资源应该被释放掉,这些代码应写在surfaceDestroyed方法中。
2.5.2 使用SurfaceView实现2D动画
上一小节介绍了SurfaceView的基本用法,本小节将通过一个2D动画的简单案例来具体说明SurfaceView在开发中的应用。本案例中的动画实现了这样的场景:一枚炮弹从屏幕的左下角发射,以抛物线的轨迹划过天空,并在一定的位置爆炸,其效果分别如图3-24、图3-25和图3-26所示。
说明 图3-24、图3-25和图3-26从左到右分别为炮弹刚刚发射、炮弹将近飞到最高点和炮弹爆炸时的效果图。
介绍完本案例的运行效果之后,下面将详细讲解案例中各个类代码的具体实现,具体步骤如下。
(1)首先将开发本案例的主控制类——Sample2_8_Activity,该类在程序开始时执行。其主要功能为设置应用程序为全屏及横屏模式,并跳转到动画呈现对应的SurfaceView,具体代码如下。
1 package com/bn/pp8; //声明包
2 import android.app.Activity; //引入相关类
3 ……//此处省略了部分类的引入代码,读者可自行查看随书光盘的源代码
4 import android.view.WindowManager; //引入相关类
5 public class Sample2_8_Activity extends Activity {
6 MySurfaceView gameView; //游戏界面
7 @Override
8 public void onCreate(Bundle savedInstanceState) { //重写onCreate方法
9 super.onCreate(savedInstanceState);
10 requestWindowFeature(Window.FEATURE_NO_TITLE); //设置为全屏模式
11 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN ,
12 WindowManager.LayoutParams.FLAG_FULLSCREEN);
13 //设置为横屏模式
14 this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
15 gameView = new MySurfaceView(this); //创建2D动画界面对象
16 this.setContentView(gameView); //跳转到2D动画界面
17 }}
第10-14行将Activity设置为全屏,并设置为横屏模式。
第15-16行创建了用于实现2D动画绘制呈现的MySurfaceView类的对象,并跳转到2D动画界面。这样,程序一启动成功用户看到的将是2D动画界面了。
(2)开发完本案例的主控制类后,接下来将对本案例中用于实现2D动画绘制呈现的MySurfaceView类进行开发。该类继承自SurfaceView,同时实现了SurfaceHolder.Callback接口,具体代码如下。
1 package com/bn/pp8; //声明包
2 import android.graphics.Bitmap; //引入相关类
3 ……//此处省略了部分类的引入代码,读者可自行查看随书光盘的源代码
4 import android.view.SurfaceView; //引入相关类
5 public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
6 Sample2_8_Activity activity; //activity的引用
7 Paint paint; //画笔引用
8 DrawThread drawThread; //绘制线程引用
9 Bitmap bgBmp; //背景图片
10 Bitmap bulletBmp; //炮弹位图
11 Bitmap[] explodeBmps; //爆炸位图数组
12 Bullet bullet; //炮弹对象引用
13 public MySurfaceView(Sample2_8_Activity activity) { //构造器
14 super(activity);
15 this.activity = activity;
16 this.requestFocus(); //获取焦点
17 this.setFocusableInTouchMode(true); //设置为可触控
18 getHolder().addCallback(this); //注册回调接口
19 }
20 @Override
21 protected void onDraw(Canvas canvas) { //绘制界面的方法
22 super.onDraw(canvas);
23 canvas.drawBitmap(bgBmp, 0, 0, paint); //绘制背景
24 bullet.drawSelf(canvas, paint); //绘制炮弹
25 }
26 @Override
27 public void surfaceChanged(SurfaceHolder holder,int format,int width,int
height){ }
28 @Override
29 public void surfaceCreated(SurfaceHolder holder){
30 paint = new Paint(); // 创建画笔
31 paint.setAntiAlias(true); // 打开抗锯齿
32 //加载图片资源
33 bulletBmp = BitmapFactory.decodeResource(this.getResources(), R.drawable.
bullet);
34 bgBmp = BitmapFactory.decodeResource(this.getResources(), R.drawable.bg);
35 explodeBmps=new Bitmap[]{
36 BitmapFactory.decodeResource(this.getResources(), R.drawable.
explode0),
37 BitmapFactory.decodeResource(this.getResources(), R.drawable.
explode1),
38 BitmapFactory.decodeResource(this.getResources(), R.drawable.
explode2),
39 BitmapFactory.decodeResource(this.getResources(), R.drawable.
explode3),
40 BitmapFactory.decodeResource(this.getResources(), R.drawable.
explode4),
41 BitmapFactory.decodeResource(this.getResources(), R.drawable.
explode5),
42 };
43 bullet = new Bullet(this, bulletBmp,explodeBmps,0,290,1.3f,-5.9f);
//创建炮弹对象
44 drawThread = new DrawThread(this); //创建绘制线程
45 drawThread.start(); //启动绘制线程
46 }
47 @Override
48 public void surfaceDestroyed(SurfaceHolder holder) { //界面销毁时调用的方法
49 drawThread.setFlag(false); //停止绘制线程
50 }}
第6-12行声明了画笔、绘制线程、位图资源、炮弹等对象的引用。
第13-19行为MySurfaceView类的构造器,其主要功能为获得焦点、设置为可触控,并注册了生命周期回调接口。
第21-25行为重写的onDraw方法,该方法主要功能为呈现2D动画的每一帧。
第27行实现了surfaceChanged方法。由于本案例中当SurfaceView变化时,没有需要做的工作,因此该方法为空实现。
第29-46行实现了surfaceCreated方法。在该方法中创建或加载了必要的资源,如创建画笔、加载图片、创建炮弹类、创建并绘制启动线程等。
第48-50行为surfaceDestroyed方法的实现。由于SurfaceView被销毁时,绘制线程还未关闭,因此应在该方法中停止绘制线程。
(3)开发完本案例的主控制类以及2D动画呈现类之后,接下来将开发绘制线程类——DrawThread。该类主要功能为每隔一定的时间调用onDraw方法,绘制动画中的每一帧,具体代码如下。
1 package com/bn/pp8; //声明包
2 import android.graphics.Canvas; //引入相关类
3 import android.view.SurfaceHolder; //引入相关类
4 public class DrawThread extends Thread {
5 private boolean flag = true; //线程工作标志位
6 private int sleepSpan = 100; //线程休眠时间
7 MySurfaceView gameView; //父界面引用
8 SurfaceHolder surfaceHolder; //surfaceHolder引用
9 public DrawThread(MySurfaceView gameView) { //构造器
10 this.gameView = gameView;
11 this.surfaceHolder = gameView.getHolder(); //创建SurfaceHolder对象
12 }
13 public void run(){
14 Canvas c; //声明画布
15 while (this.flag){ //循环执行刷帧任务
16 c = null;
17 try {
18 c = this.surfaceHolder.lockCanvas(null);//锁定画布
19 synchronized (this.surfaceHolder) { //锁定surfaceHolder
20 gameView.onDraw(c); //绘制一帧画面
21 } }finally{
22 if (c != null){ //释放锁
23 this.surfaceHolder.unlockCanvasAndPost(c);
24 } }
25 try{
26 Thread.sleep(sleepSpan); //线程睡眠指定毫秒数
27 }catch (Exception e){
28 e.printStackTrace(); //打印错误堆栈信息
29 }}}
30 public void setFlag(boolean flag) { //设置工作标志位的方法
31 this.flag = flag;
32 }}
第5-8行为该线程类的成员变量,其中flag为线程是否继续工作的标志,sleepSpan为每两次绘制的时间间隔。
第9-12行为该类的构造器,在构造器中获得了gameView及其surfaceHolder对象。
第13-29行为实现该线程任务的run方法,其中用一个while循环不断调用gameView的onDraw方法进行刷帧。
第30-32行为设置线程工作标志位的setFlag方法。调用该方法并传递参数false,可以停止该线程的绘制工作。
(4)接下来将要开发的是炮弹类——Bullet,此类每个对象表示一枚炮弹,具体代码如下。
1 package com/bn/pp8; //声明包
2 import android.graphics.Bitmap; //引入相关类
3 import android.graphics.Canvas; //引入相关类
4 import android.graphics.Paint; //引入相关类
5 public class Bullet {
6 MySurfaceView gameView;
7 private Bitmap bitmap; // 位图
8 private Bitmap[] bitmaps; // 爆炸动画图组
9 float x; // _x_轴位置
10 float y; // _y_轴位置
11 float vx; // _x_轴速度
12 float vy; // _y_轴速度
13 private float t = 0; // 生存时间
14 private float timeSpan = 0.5f; // 时间间隔
15 int size; // 炮弹尺寸
16 boolean explodeFlag = false; // 是否绘制炮弹的标记
17 Explosion mExplosion; // 爆炸对象引用
18 public Bullet(MySurfaceView gameView, Bitmap bitmap, Bitmap[] bitmaps,
19 float x, float y, float vx, float vy) {
20 this.gameView = gameView;
21 this.bitmap = bitmap; // 初始化炮弹的图片
22 this.bitmaps = bitmaps; // 初始化爆炸动画图片数组
23 this.x = x; // 初始化炮弹的位置
24 this.y = y;
25 this.vx = vx; // 初始化炮弹的速度
26 this.vy = vy;
27 size = bitmap.getHeight(); // 获得图片的高度
28 }
29 public void drawSelf(Canvas canvas, Paint paint) { // 绘制炮弹的方法
30 if (explodeFlag && mExplosion != null) { // 如果已经爆炸,绘制爆炸动画
31 mExplosion.drawSelf(canvas, paint);
32 } else {
33 go(); // 炮弹前进
34 canvas.drawBitmap(bitmap, x, y, paint); // 绘制炮弹
35 } }
36 public void go() { // 炮弹前进的方法
37 x += vx * t; // 水平方向匀速直线运动
38 y += vy * t + 0.5f * Constant.G * t * t; // 竖直方向上抛运动
39 if (x >= Constant.EXPLOSION_X || y >= Constant.SCREEN_HEIGHT) {//特定位置爆炸
40 mExplosion = new Explosion(gameView, bitmaps, x, y);// 创建爆炸对象
41 explodeFlag = true; // 不再绘制炮弹
42 return;
43 }
44 t += timeSpan; // 更新生存时间
45 }}
第18-28行为该类的构造器,在构造器中进行成员变量的初始化工作。
第29-35行为绘制炮弹的方法。如果炮弹已经爆炸,则绘制爆炸动画;否则先调用go方法改变炮弹的位置,再根据位置参数的值绘制炮弹。
第36-45行定义了进行炮弹运动计算的go方法。每调用一次该方法,就根据物理公式更新一次炮弹的位置。炮弹位置的计算分为水平(x轴)和垂直(y轴)两个方向进行,水平方向为匀速直线运动,垂直方向为上抛运动。当炮弹运动到特定位置时,创建爆炸对象,并标记不再绘制炮弹。
(5)接着对本案例中的Explosion类进行开发,该类主要在炮弹爆炸时使用,实现炮弹爆炸动画中每一帧图片的切换,具体代码如下。
1 package com/bn/pp8; //声明包
2 import android.graphics.Bitmap; //引入相关类
3 import android.graphics.Canvas; //引入相关类
4 import android.graphics.Paint; //引入相关类
5 public class Explosion {
6 MySurfaceView gameView;
7 private Bitmap[] bitmaps; // 位图
8 float x; // _x_轴位置
9 float y; // _y_轴位置
10 private int anmiIndex = 0; // 爆炸动画帧索引
11 public Explosion(MySurfaceView gameView, Bitmap[] bitmaps, float x, float y) {
12 this.gameView = gameView; //初始化MySurfaceView对象
13 this.bitmaps = bitmaps;
14 this.x = x; //初始化_x_位置
15 this.y = y; //初始化_y_位置
16 }
17 public void drawSelf(Canvas canvas, Paint paint) { // 绘制背景的方法
18 if (anmiIndex >= bitmaps.length - 1) { // 如果动画播放完毕,不再绘制爆炸效果
19 return;
20 }
21 canvas.drawBitmap(bitmaps[anmiIndex], x, y, paint); // 绘制数组中某一幅图
22 anmiIndex++; // 当前下标加1
23 }}
第11-16行为该类的构造器,在构造器中进行了各成员变量的初始化工作。
第17-23行为绘制爆炸动画的方法。根据anmiIndex的值,选择图片数组中的某一幅图片进行绘制,然后anmiIndex自加1。
提示 从上面的代码中读者可以看出,其实爆炸动画是由一帧一帧单独的画面组成的。绘制线程定时调用绘制方法绘制动画中的每一帧,动画就呈现在用户眼前。
(6)最后需要开发的是本案例中存放一些常量的类——Constant,其代码如下。
1 package com.bn.pp8;
2 public class Constant { //用于统一管理常量的类
3 public static final int SCREEN_WIDTH=480; //屏幕宽度
4 public static final int SCREEN_HEIGHT=320; //屏幕高度
5 public static final int EXPLOSION_X=270; //爆炸X位置
6 public static final float G = 1.0f; //重力加速度的值
7 }
提示 从上面的代码中可以看出,Constant中包含了一些程序运行过程中需要的常量。在实际项目的开发中,将常量独立到一个类中进行管理是非常好的习惯,这很有利于提高代码的可维护性,降低维护的成本。