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

android使用tcpdump抓包

最近游戏在接qq opensdk的时候调用一个cgi一直不成功,文档描述太简单,我们调用的又是互娱这边msdk的api,由msdk调用opensdk相关api,中间跨了两部门,为了...

阅读全文

Android.mk文件解读

我们在Android平台写c/c++程序的时候需要用到Android.mk(Makefile),一般用来编译c/c++源码、引用第三方头文件和库,生成程序所需的so文件。下面是一个cocos2...

阅读全文

Android性能优化案例研究(下)

去掉冗余的图层 为 了去掉重绘我们必须首先理解它从哪里产生的。这就轮到Hierarchy Viewer和Tracer for OpenGL大显身手的时候了。Hierarchy Viewer是ADT工具...

阅读全文

6 条评论

  1. 你好!代码有bug,
    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, //这里第一次载剪时col值为1,1-1等于0,0不可以除的,下面也是,
    (row – 1) * source.getHeight() / rowTotal, source.getWidth()
    / colTotal, source.getHeight() / rowTotal);
    return bitmap;
    }

    1. 在style里面AppTheme里面去掉标题栏

      我就是在android studio中改了这句才去掉的

欢迎留言