Android SurfaceView使用详解

1. SurfaceView的定义
前面已经介绍过View了,下面来简单介绍一下SurfaceView,参考SDK文档和网络资料:SurfaceView是View的子类,它内嵌了一个专门用于绘制的Surface,你可以控制这个Surface的格式和尺寸,Surfaceview控制这个Surface的绘制位置。surface是纵深排序(Z-ordered)的,说明它总在自己所在窗口的后面。SurfaceView提供了一个可见区域,只有在这个可见区域内的surface内容才可见。surface的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者 surface的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件)。注意,如果surface上面有透明控件,那么每次surface变化都会引起框架重新计算它和顶层控件的透明效果,这会影响性能。
SurfaceView默认使用双缓冲技术的,它支持在子线程中绘制图像,这样就不会阻塞主线程了,所以它更适合于游戏的开发。

2. SurfaceView的使用
首先继承SurfaceView,并实现SurfaceHolder.Callback接口,实现它的三个方法:surfaceCreated,surfaceChanged,surfaceDestroyed。
surfaceCreated(SurfaceHolder holder):surface创建的时候调用,一般在该方法中启动绘图的线程。
surfaceChanged(SurfaceHolder holder, int format, int width,int height):surface尺寸发生改变的时候调用,如横竖屏切换。
surfaceDestroyed(SurfaceHolder holder) :surface被销毁的时候调用,如退出游戏画面,一般在该方法中停止绘图线程。
还需要获得SurfaceHolder,并添加回调函数,这样这三个方法才会执行。

3. SurfaceView实战

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


 

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

Android c调用 surface android.view.surfaceview_java

Android c调用 surface android.view.surfaceview_java_02

View Code

package com.ryuan.game.androidspiritgame;

import java.io.InputStream;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
/**
 * GameSurfaceView绘制界面
 * @author Administrator
 *
 */
public class GameSurfaceView extends SurfaceView implements SurfaceHolder.Callback { // 屏幕宽高
    public static int SCREEN_WIDTH = 720;
    public static int SCREEN_HEIGHT = 1080;
    private Context mContext;
    private SurfaceHolder mHolder;
    // 最大帧数 (1000 / 50)帧/秒
    private static final int DRAW_INTERVAL = 50;
    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);
    }

    /**
     * 初始化资源,将png分解
     */
    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;
        }
    }

    /**
     * 根据编号返回资源图片
     * @param context
     * @param resourseId
     * @return
     */
    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);
    }

    /**
     * 返回图片
     * @param context
     * @param source
     * @param row
     * @param col
     * @param rowTotal
     * @param colTotal
     * @return
     */
    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;
    }

    /**
     * 返回图片数组
     * @param context
     * @param resourseId
     * @param row
     * @param col
     * @return
     */
    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();
        }
    }

    /**
     * 绘制界面子线程
     * @author Administrator
     *
     */
    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();
            }
        }
    }

    /**
     * 绘制
     * @param canvas
     */
    private void drawSprite(Canvas canvas) {
        // 清屏操作
        canvas.drawColor(Color.BLACK);
        mSprite.draw(canvas);
    }
}

 

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

Android c调用 surface android.view.surfaceview_java

Android c调用 surface android.view.surfaceview_java_02

View Code

package com.ryuan.game.androidspiritgame;

import android.graphics.Bitmap;
import android.graphics.Canvas;

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,源码如下:

Android c调用 surface android.view.surfaceview_java

Android c调用 surface android.view.surfaceview_java_02

View Code

package com.ryuan.game.androidspiritgame;

import android.graphics.Bitmap;

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,然后设置全屏显示。

 

Android c调用 surface android.view.surfaceview_java

Android c调用 surface android.view.surfaceview_java_02

View Code

package com.ryuan.game.androidspiritgame;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        GameSurfaceView surfaceView = new GameSurfaceView(this);
        setContentView(surfaceView);
    }
}