Android App中使用SurfaceView制作多线程动画的实例讲解

1. SurfaceView的定义

通常情况程序的View和用户响应都是在同一个线程中处理的,这也是为什么处理长时间事件(例如访问网络)需要放到另外的线程中去(防止阻塞当前UI线程的操作和绘制)。但是在其他线程中却不能修改UI元素,例如用后台线程更新自定义View(调用View的在自定义View中的onDraw函数)是不允许的。

如果需要在另外的线程绘制界面、需要迅速的更新界面或则渲染UI界面需要较长的时间,这种情况就要使用SurfaceView了。SurfaceView中包含一个Surface对象,而Surface是可以在后台线程中绘制的。SurfaceView的性质决定了其比较适合一些场景:需要界面迅速更新、对帧率要求较高的情况。使用SurfaceView需要注意以下几点情况:

SurfaceView和SurfaceHolder.Callback函数都从当前SurfaceView窗口线程中调用(一般而言就是程序的主线程)。有关资源状态要注意和绘制线程之间的同步。

在绘制线程中必须先合法的获取Surface才能开始绘制内容,在SurfaceHolder.Callback.surfaceCreated() 和SurfaceHolder.Callback.surfaceDestroyed()之间的状态为合法的,另外在Surface类型为SURFACE_TYPE_PUSH_BUFFERS时候是不合法的。

额外的绘制线程会消耗系统的资源,在使用SurfaceView的时候要注意这点。


2. SurfaceView的使用

首先继承SurfaceView,并实现SurfaceHolder.Callback接口,实现它的三个方法:surfaceCreated,surfaceChanged,surfaceDestroyed。

(1)surfaceCreated(SurfaceHolder holder):surface创建的时候调用,一般在该方法中启动绘图的线程。

(2)surfaceChanged(SurfaceHolder holder, int format, int width,int height):surface尺寸发生改变的时候调用,如横竖屏切换。

(3)surfaceDestroyed(SurfaceHolder holder) :surface被销毁的时候调用,如退出游戏画面,一般在该方法中停止绘图线程。

还需要获得SurfaceHolder,并添加回调函数,这样这三个方法才会执行。

只要继承SurfaceView类并实现SurfaceHolder.Callback接口就可以实现一个自定义的SurfaceView了,SurfaceHolder.Callback在底层的Surface状态发生变化的时候通知View,SurfaceHolder.Callback具有如下的接口:

(1)surfaceCreated(SurfaceHolder holder):当Surface第一次创建后会立即调用该函数。程序可以在该函数中做些和绘制界面相关的初始化工作,一般情况下都是在另外的线程来绘制界面,所以不要在这个函数中绘制Surface。

(2)surfaceChanged(SurfaceHolder holder, int format, int width,int height):当Surface的状态(大小和格式)发生变化的时候会调用该函数,在surfaceCreated调用后该函数至少会被调用一次。

(3)surfaceDestroyed(SurfaceHolder holder):当Surface被摧毁前会调用该函数,该函数被调用后就不能继续使用Surface了,一般在该函数中来清理使用的资源。

通过SurfaceView的getHolder()函数可以获取SurfaceHolder对象,Surface 就在SurfaceHolder对象内。虽然Surface保存了当前窗口的像素数据,但是在使用过程中是不直接和Surface打交道的,由SurfaceHolder的Canvas lockCanvas()或则Canvas lockCanvas(Rect dirty)函数来获取Canvas对象,通过在Canvas上绘制内容来修改Surface中的数据。如果Surface不可编辑或则尚未创建调用该函数会返回null,在 unlockCanvas() 和 lockCanvas()中Surface的内容是不缓存的,所以需要完全重绘Surface的内容,为了提高效率只重绘变化的部分则可以调用lockCanvas(Rect dirty)函数来指定一个dirty区域,这样该区域外的内容会缓存起来。在调用lockCanvas函数获取Canvas后,SurfaceView会获取Surface的一个同步锁直到调用unlockCanvasAndPost(Canvas canvas)函数才释放该锁,这里的同步机制保证在Surface绘制过程中不会被改变(被摧毁、修改)。

当在Canvas中绘制完成后,调用函数unlockCanvasAndPost(Canvas canvas)来通知系统Surface已经绘制完成,这样系统会把绘制完的内容显示出来。为了充分利用不同平台的资源,发挥平台的最优效果可以通过SurfaceHolder的setType函数来设置绘制的类型,目前接收如下的参数:

(1)SURFACE_TYPE_NORMAL:用RAM缓存原生数据的普通Surface

(2)SURFACE_TYPE_HARDWARE:适用于DMA(Direct memory access )引擎和硬件加速的Surface

(3)SURFACE_TYPE_GPU:适用于GPU加速的Surface

(4)SURFACE_TYPE_PUSH_BUFFERS:表明该Surface不包含原生数据,Surface用到的数据由其他对象提供,在Camera图像预览中就使用该类型的Surface,有Camera负责提供给预览Surface数据,这样图像预览会比较流畅。如果设置这种类型则就不能调用lockCanvas来获取Canvas对象了。

访问SurfaceView的底层图形是通过SurfaceHolder接口来实现的,通过getHolder()方法可以得到这个SurfaceHolder对象。你应该实现surfaceCreated(SurfaceHolder)和surfaceDestroyed(SurfaceHolder)方法来知道在这个Surface在窗口的显示和隐藏过程中是什么时候创建和销毁的。

注意:一个SurfaceView只在SurfaceHolder.Callback.surfaceCreated() 和 SurfaceHolder.Callback.surfaceDestroyed()调用之间是可用的,其他时间是得不到它的Canvas对象的(null)。

3. SurfaceView实战

下面通过一个小demo来学习SurfaceView在实际项目中的使用,绘制一个精灵,该精灵有四个方向的行走动画,让精灵沿着屏幕四周不停的行走。游戏中精灵素材和最终实现的效果图:

首先创建核心类GameView.java,源码如下:

public class GameView extends SurfaceView implements

SurfaceHolder.Callback {

//屏幕宽高

public static int SCREEN_WIDTH;

public static int SCREEN_HEIGHT;

private Context mContext;

private SurfaceHolder mHolder;

//最大帧数 (1000 / 30)

private static final int DRAW_INTERVAL = 30;

private DrawThread mDrawThread;

private FrameAnimation []spriteAnimations;

private Sprite mSprite;

private int spriteWidth = 0;

private int spriteHeight = 0;

private float spriteSpeed = (float)((500 * SCREEN_WIDTH / 480) * 0.001);

private int row = 4;

private int col = 4;

public GameSurfaceView(Context context) {

super(context);

this.mContext = context;

mHolder = this.getHolder();

mHolder.addCallback(this);

initResources();

mSprite = new Sprite(spriteAnimations,0,0,spriteWidth,spriteHeight,spriteSpeed);

}

private void initResources() {

Bitmap[][] spriteImgs = generateBitmapArray(mContext, R.drawable.sprite, row, col);

spriteAnimations = new FrameAnimation[row];

for(int i = 0; i < row; i ++) {

Bitmap []spriteImg = spriteImgs[i];

FrameAnimation spriteAnimation = new FrameAnimation(spriteImg,new int[]{150,150,150,150},true);

spriteAnimations[i] = spriteAnimation;

}

}

public Bitmap decodeBitmapFromRes(Context context, int resourseId) {

BitmapFactory.Options opt = new BitmapFactory.Options();

opt.inPreferredConfig = Bitmap.Config.RGB_565;

opt.inPurgeable = true;

opt.inInputShareable = true;

InputStream is = context.getResources().openRawResource(resourseId);

return BitmapFactory.decodeStream(is, null, opt);

}

public Bitmap createBitmap(Context context, Bitmap source, int row,

int col, int rowTotal, int colTotal) {

Bitmap bitmap = Bitmap.createBitmap(source,

(col - 1) * source.getWidth() / colTotal,

(row - 1) * source.getHeight() / rowTotal, source.getWidth()

/ colTotal, source.getHeight() / rowTotal);

return bitmap;

}

public Bitmap[][] generateBitmapArray(Context context, int resourseId,

int row, int col) {

Bitmap bitmaps[][] = new Bitmap[row][col];

Bitmap source = decodeBitmapFromRes(context, resourseId);

this.spriteWidth = source.getWidth() / col;

this.spriteHeight = source.getHeight() / row;

for (int i = 1; i <= row; i++) {

for (int j = 1; j <= col; j++) {

bitmaps[i - 1][j - 1] = createBitmap(context, source, i, j,

row, col);

}

}

if (source != null && !source.isRecycled()) {

source.recycle();

source = null;

}

return bitmaps;

}

public void surfaceChanged(SurfaceHolder holder, int format, int width,

int height) {

}

public void surfaceCreated(SurfaceHolder holder) {

if(null == mDrawThread) {

mDrawThread = new DrawThread();

mDrawThread.start();

}

}

public void surfaceDestroyed(SurfaceHolder holder) {

if(null != mDrawThread) {

mDrawThread.stopThread();

}

}

private class DrawThread extends Thread {

public boolean isRunning = false;

public DrawThread() {

isRunning = true;

}

public void stopThread() {

isRunning = false;

boolean workIsNotFinish = true;

while (workIsNotFinish) {

try {

this.join();// 保证run方法执行完毕

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

workIsNotFinish = false;

}

}

public void run() {

long deltaTime = 0;

long tickTime = 0;

tickTime = System.currentTimeMillis();

while (isRunning) {

Canvas canvas = null;

try {

synchronized (mHolder) {

canvas = mHolder.lockCanvas();

//设置方向

mSprite.setDirection();

//更新精灵位置

mSprite.updatePosition(deltaTime);

drawSprite(canvas);

}

} catch (Exception e) {

e.printStackTrace();

} finally {

if (null != mHolder) {

mHolder.unlockCanvasAndPost(canvas);

}

}

deltaTime = System.currentTimeMillis() - tickTime;

if(deltaTime < DRAW_INTERVAL) {

try {

Thread.sleep(DRAW_INTERVAL - deltaTime);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

tickTime = System.currentTimeMillis();

}

}

}

private void drawSprite(Canvas canvas) {

//清屏操作

canvas.drawColor(Color.BLACK);

mSprite.draw(canvas);

}

}

GameView.java中包含了一个绘图线程DrawThread,在线程的run方法中锁定Canvas、绘制精灵、更新精灵位置、释放Canvas等操作。因为精灵素材是一张大图,所以这里进行了裁剪生成一个二维数组。使用这个二维数组初始化了精灵四个方向的动画,下面看Sprite.java的源码。

public class Sprite {

public static final int DOWN = 0;

public static final int LEFT = 1;

public static final int RIGHT = 2;

public static final int UP = 3;

public float x;

public float y;

public int width;

public int height;

//精灵行走速度

public double speed;

//精灵当前行走方向

public int direction;

//精灵四个方向的动画

public FrameAnimation[] frameAnimations;

public Sprite(FrameAnimation[] frameAnimations, int positionX,

int positionY, int width, int height, float speed) {

this.frameAnimations = frameAnimations;

this.x = positionX;

this.y = positionY;

this.width = width;

this.height = height;

this.speed = speed;

}

public void updatePosition(long deltaTime) {

switch (direction) {

case LEFT:

//让物体的移动速度不受机器性能的影响,每帧精灵需要移动的距离为:移动速度*时间间隔

this.x = this.x - (float) (this.speed * deltaTime);

break;

case DOWN:

this.y = this.y + (float) (this.speed * deltaTime);

break;

case RIGHT:

this.x = this.x + (float) (this.speed * deltaTime);

break;

case UP:

this.y = this.y - (float) (this.speed * deltaTime);

break;

}

}

/**

* 根据精灵的当前位置判断是否改变行走方向

*/

public void setDirection() {

if (this.x <= 0

&& (this.y + this.height) < GameSurfaceView.SCREEN_HEIGHT) {

if (this.x < 0)

this.x = 0;

this.direction = Sprite.DOWN;

} else if ((this.y + this.height) >= GameSurfaceView.SCREEN_HEIGHT

&& (this.x + this.width) < GameSurfaceView.SCREEN_WIDTH) {

if ((this.y + this.height) > GameSurfaceView.SCREEN_HEIGHT)

this.y = GameSurfaceView.SCREEN_HEIGHT - this.height;

this.direction = Sprite.RIGHT;

} else if ((this.x + this.width) >= GameSurfaceView.SCREEN_WIDTH

&& this.y > 0) {

if ((this.x + this.width) > GameSurfaceView.SCREEN_WIDTH)

this.x = GameSurfaceView.SCREEN_WIDTH - this.width;

this.direction = Sprite.UP;

} else {

if (this.y < 0)

this.y = 0;

this.direction = Sprite.LEFT;

}

}

public void draw(Canvas canvas) {

FrameAnimation frameAnimation = frameAnimations[this.direction];

Bitmap bitmap = frameAnimation.nextFrame();

if (null != bitmap) {

canvas.drawBitmap(bitmap, x, y, null);

}

}

}

精灵类主要是根据当前位置判断行走的方向,然后根据行走的方向更新精灵的位置,再绘制自身的动画。由于精灵的动画是一帧一帧的播放图片,所以这里封装了FrameAnimation.java,源码如下:

public class FrameAnimation{

/**动画显示的需要的资源 */

private Bitmap[] bitmaps;

/**动画每帧显示的时间 */

private int[] duration;

/**动画上一帧显示的时间 */

protected Long lastBitmapTime;

/**动画显示的索引值,防止数组越界 */

protected int step;

/**动画是否重复播放 */

protected boolean repeat;

/**动画重复播放的次数*/

protected int repeatCount;

/**

* @param bitmap:显示的图片<br/>

* @param duration:图片显示的时间<br/>

* @param repeat:是否重复动画过程<br/>

*/

public FrameAnimation(Bitmap[] bitmaps, int duration[], boolean repeat) {

this.bitmaps = bitmaps;

this.duration = duration;

this.repeat = repeat;

lastBitmapTime = null;

step = 0;

}

public Bitmap nextFrame() {

// 判断step是否越界

if (step >= bitmaps.length) {

//如果不无限循环

if( !repeat ) {

return null;

} else {

lastBitmapTime = null;

}

}

if (null == lastBitmapTime) {

// 第一次执行

lastBitmapTime = System.currentTimeMillis();

return bitmaps[step = 0];

}

// 第X次执行

long nowTime = System.currentTimeMillis();

if (nowTime - lastBitmapTime <= duration[step]) {

// 如果还在duration的时间段内,则继续返回当前Bitmap

// 如果duration的值小于0,则表明永远不失效,一般用于背景

return bitmaps[step];

}

lastBitmapTime = nowTime;

return bitmaps[step++];// 返回下一Bitmap

}

}

FrameAnimation根据每一帧的显示时间返回当前的图片帧,若没有超过指定的时间则继续返回当前帧,否则返回下一帧。

接下来需要做的是让Activty显示的View为我们之前创建的GameView,然后设置全屏显示。

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,

WindowManager.LayoutParams.FLAG_FULLSCREEN);

requestWindowFeature(Window.FEATURE_NO_TITLE);

getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,

WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

DisplayMetrics outMetrics = new DisplayMetrics();

this.getWindowManager().getDefaultDisplay().getMetrics(outMetrics);

GameSurfaceView.SCREEN_WIDTH = outMetrics.widthPixels;

GameSurfaceView.SCREEN_HEIGHT = outMetrics.heightPixels;

GameSurfaceView gameView = new GameSurfaceView(this);

setContentView(gameView);

}

现在运行Android工程,应该就可以看到一个手持宝剑的武士在沿着屏幕不停的走了。

以上是 Android App中使用SurfaceView制作多线程动画的实例讲解 的全部内容, 来源链接: utcz.com/z/330067.html

回到顶部