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在实际项目中的使用,绘制一个精灵,该精灵有四个方向的行走动画,让精灵沿着屏幕四周不停的行走。游戏中精灵素材和最终实现的效果图:


首先创建核心类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工程,应该就可以看到一个手持宝剑的武士在沿着屏幕不停的走了。