Android 视图框架系列2/3——SurfaceView视图框架

        本篇我们来说说 SurfaceView 视图框架。

        一、提出问题

        SurfaceView 继承自 View ,但其实和 View 已经有了很大的区别,可以说这一层的继承,跨的有点大!来看这样一个情景:

        小兵快跑

        在前一篇 视图框架系列1/3——View视图框架 中我们已经知道,利用 View 可以完成动画效果的实现,现在我们来实现这样一个需求——我们绘制一个不断做跑步动作的人,但是位置不变(在 MyView 类中通过子线程不断通知重复重绘右边两个不同状态的小兵即可),动画效果是实现了。但是如果我要求响应事件,点击屏幕让他喊 “军情、军情”,按方向键让他向相应的方向跑。这么做?按照原理来说是:给 MyView 重写 onTouchEvent() 响应触屏事件、重写 onKeyDown() 方法响应方向键。但是别忘了——View框架的重绘是不断重新执行 onDraw() 方法,而 onDraw() 是在 UI 线程中执行的,此时 UI 线程正在不断执行 onDraw() 方法,很容易导致 UI 线程阻塞而不能响应触屏和按键。虽说当线程间断休眠时间够长即相邻两个 onDraw() 之间间断时间足够长时可以响应事件,但是我们怎么可以让我们的程序处于一个不可控的状态呢?现在用 View 便不再能够满足我们的需求。

        有比较才能显出区别:看完 视图框架系列1/3——View视图框架 ,在结合上边的情景,不难总结出这样的结论:View 可以实现动画效果,但是仅仅局限于两种:

        ★(1)“被动更新”的动画,“被动”是指这个动画(“视图”更为准确)不主动更新,它的更新依赖于 onTouchEvent、onKeyDown 等事件来触发 onDraw 的重绘

        ★(2)只显示一个动画而不接受事件响应,很好理解,比如我在页面上显示一个闪动的星星,这个星星只是展示作用,不接受任何事件

        ★另外:View 视图绘制的效率比较低,因为每一次重绘实际上是重新执行以便 onDraw 方法,onDraw 方法是重新绘制整个画布,而在 View 中,canvas 就是整个视图

        而此时的窘境,正是 SurfaceView 视图大显身手的时候!SurfaceView 继承于 View ,所以同样拥有触屏监听、按钮监听等方法,但是请注意,SurfaceView 看名字就和 Surface 脱不了干系,Surface 是 Android 中一个很重要的类,有必要了解一下。每个 View 在和屏幕绑定时都会关联一个对应的 Surface,你可以把 Surface 理解成一块屏幕缓存。但从源码可以看出 SurfaceView 还有一个 Surface 类型的成员变量,所以 SurfaceView 就拥有了两个内存区。 这里就该说 SurfaceView 的双缓冲机制了。

        二、双缓冲技术

        双缓冲技术是游戏开发中一个重要技术,主要原理——当一个动画争先显示时,程序又在改变它,前面的还没有显示完,程序又要求重绘,这样屏幕就会产生闪烁,为了避免这种闪烁,可以将要处理的图片先在内存中处理好之后,再将其显示到屏幕上,这样显示出来的总是完整的,便不会产生闪烁。
        SurfaceView就是一个典型的双缓冲机制,其内嵌的 Surface 专门处理待绘制的内容,包括各式、尺寸等,在真正绘制时 SurfaceView 控制其绘制位置进行真正的绘制显示。所以 Surface 用来做游戏视图处理再合适不过了。

        三、SurfaceView的使用

        在继承 SurfaceView 开发时,自定义类要继承自 SurfaceView ,而且要实现 SurfceHolder.Callback 接口,那这个接口是干嘛的, SurfaceHolder 类又是什么呢?我们先看一段简单代码:
public class MySurfaceView extends SurfaceView implements Callback {
	private SurfaceHolder holder;
	private Paint paint;

	public MySurfaceView(Context context) {
		this(context, null);
	}
	public MySurfaceView(Context context, AttributeSet attrs) {
		super(context, attrs);
		holder = getHolder();
		holder.addCallback(this);   //给Holder声明Callback回调

		paint = new Paint();
		paint.setAntiAlias(true);
		paint.setColor(Color.RED);
		paint.setTextSize(50);
	}
	
	/* Callback方法之一,SurfaceView创建时回调 */
	@Override
	public void surfaceCreated(SurfaceHolder arg0) { 
		myDraw();
	}

	/* Callback方法之一,SurfaceView改变时回调 */
	@Override
	public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) { }

	/* Callback方法之一,SurfaceView销毁时回调 */
	@Override
	public void surfaceDestroyed(SurfaceHolder arg0) { }

	/* 我自己的绘制方法 */
	private void myDraw() {
		String text = "Holle-World-!";
		Canvas canvas = holder.lockCanvas();    //获取画布
		canvas.drawColor(Color.WHITE);          //刷屏
		canvas.drawText(text, 0, 50, paint);    //绘制内容
		holder.unlockCanvasAndPost(canvas);     //提交对画布所做的操作
	}
}
        以上算是一个五脏六腑俱全的 Surfaceiew 的使用了。仔细看代码你会发现,我们在使用 SurfaceView 时,并没有直接和 SurfaceView “交手”,而是通过 getHolder() 获取到 SurfaceHolder 类的实例进行操作,通过 holder.lockCanvas()方法获取到 Canvas 对象,就可以进行自己的绘制了,最后用 holder.unLockCanvasAndPost(canvas) 进行提交。*要知道* lockCanvas() 方法不仅仅是获取 Canvas,同时还对即将获取到的 Canvas 添加同步锁,这个机制主要是为了防止 SurfaceView 在修改的过程中被修改、摧毁等情况发生;unLockCanvasAndPost(canvas) 方法是解除同步锁并把之前所做的画布绘制工作进行提交
        *附加* SurfaceHolder 类除了提供lockCanvas() 方法用于获取当前 Canvas (默认与手机屏幕大小一致)外,还提供了其重载的 lockCanvas(Rect mRect) 方法用于获取自定义举行大小的画布,若有问题可以参看 Canvas有关问题整理
        需要重申和留意的是:SurfaceView 是通过 SurfaceHolder 来操作的,所以使用 SurfaceView 时不再使用 onDraw(Canvas canvas) 来绘图,而是通过 holder.lockCanvas() 获取Canvas 来绘图,即使重写了 onDraw() 方法, SurfaceView 在启动时也不会执行到。

        四、刷屏

        在上边的代码的 myDraw() 方法中,有一行 “ canvas.drawColor(Color.WHITE) ;   //刷屏 ”的语句,从字面意思看就是绘制了一种颜色,什么是刷屏呢?我先看问题再解释(我们让上边代码中绘制的“Holle-World-!”随手指触摸屏幕的位置变化):
        首先改造 myDraw() 方法:
	String text = "Holle-World-!";
	private int positionX = 0, positionY = 0;   //初始化X,Y为0
	private void myDraw() {
		 Canvas canvas = holder.lockCanvas(); //获取画布
		 //这里先不画颜色(刷屏)
		 canvas.drawText(text, positionX, positionY + 50, paint); //Y轴方向+50是因为绘制Text时指定的是左下角位置,+50用来校正位置
		 holder.unlockCanvasAndPost(canvas); //提交对画布所做的操作
		
	}
        添加触屏响应:
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			positionX = (int)event.getX();
			positionY = (int)event.getY();
			myDraw();
			break;

		default:
			break;
		}
		return super.onTouchEvent(event);
	}
        运行一下,多点几下就会出现这个样子:
        不刷屏效果
        看这个结果图,说明触屏动作及时响应了,否则也不会出现触点位置的字符,但是并没有抹掉旧状态,就是因为我们没有“刷屏”。接下来我们在 canvas.drawText() 之前加上:
canvas.drawColor(Color.WHITE); //刷屏
        再运行:
        擦!!用白色带刷屏有坑,都看不到边界,用字给框个边界吧,我真是太聪明了!
        这就是“刷屏”(娘的!别再瞎想你的朋友圈刷屏了)。这里就是区别:View类本身提供的两种重绘方法 invalidate()、postInvalidate() 内部已经封装了刷屏的操作(具体是什么操作,最好看源码,我没看,不敢妄言),所以每次重绘之后都看不到之前的历史记录;但是用 SurfaceView 时我们用lockCanvas () 获取到的是同一个画布,绘图用的是自定义绘图方法 myDraw(),系统没有帮我们刷新画布,也,没有提供给我们一张新画布,所以在进行下一次绘画时必定能看到上一次的痕迹,这时候我们就要自己动手“刷屏":
        "刷屏"就是盖调上次绘画的痕迹,所以这里有两种方法:
        (1)每次绘图之前,绘制一个屏幕大小的图形(或是图片,如游戏背景)覆盖在原来的画布上
        (2)每次绘画之前,给画布填充一种颜色(上边我们用到的),如 drawRGB()、drawColor()

        五、在 SurfaceView 中添加线程

        从本篇开篇我们总结前一篇 View 视图框架的局限来看,我们用 SurfaceView 的目的就是要一边完成视图的“主动更新”,一边还要响应用户操作,所以在 SurfaceView 中添加线程完成视图主动更新是必不可少的,比如游戏背景的变化,用户无法操作,而且背景是根据游戏情景不断更新的。但是在 SurfaceView 中国添加线程会遇到一些问题,下面我们来一一探讨一下。
        这是一个比较绕的问题,请集中注意力细细品味
public class MySurfaceView extends SurfaceView implements Runnable, Callback {
	private SurfaceHolder holder;
	private Paint paint;
	private String text = "Holle!";
	private Canvas canvas;

	private boolean flag;
	private Thread thread;
	private int positionX = 0, positionY = 0;
	private int screenW, screenH;
	private int speedX = 10, speedY = 13;

	public MySurfaceView(Context context) {
		this(context, null);
	}

	public MySurfaceView(Context context, AttributeSet attrs) {
		super(context, attrs);
		holder = getHolder();
		holder.addCallback(this);

		paint = new Paint();
		paint.setAntiAlias(true);
		paint.setColor(Color.RED);
		paint.setTextSize(50);
	}

	@Override
	public void surfaceCreated(SurfaceHolder arg0) {
		screenW = getWidth();
		screenH = getHeight();       //看——>说明4
		flag = true;
		thread = new Thread(this);
		thread.start();
	}

	@Override
	public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder arg0) {
		flag = false;
	}

	private void myDraw() {
		try {                             //看——>说明2
			canvas = holder.lockCanvas();
			if (canvas != null) {
				canvas.drawColor(Color.WHITE);
				canvas.drawText(text, positionX, positionY + 50, paint);
			}
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			holder.unlockCanvasAndPost(canvas);       //看——>说明3
		}
	}

	/**
	 * 模拟一个逻辑
	 */
	private void myLogic() {
		if (positionX < 0 || positionX > screenW - 100) {
			speedX = -speedX;
		}
		if (positionY < 0 || positionY > screenH) {
			speedY = -speedY;
		}
		positionX += speedX;
		positionY += speedY;
	}

	@Override
	public void run() {
		while (flag) {              //看——>说明1
			long start = System.currentTimeMillis();        //看——>说明5
			myDraw();
			myLogic();
			long end = System.currentTimeMillis();
			try {
				if (end - start < 100) {
					Thread.sleep(100 - (end - start));
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
        结果不用看,就是一个跑动的字符串,重点看代码的设计思路
        这是一个完成视图“主动更新”的部分,不包含用户动作响应,但是“主动”这一模块是完整的。下边对代码中涉及到的问题进行说明:

        (一)说明1:线程标识 flag 的作用

        线程标志 flag 主要有两个作用:
       (1)便于线程的消亡,资源释放
        一个线程一旦启动就开始执行 run() 函数,run() 函数执行完毕后退出,该线程随之消亡,这是一般的线程。二般的需求是,拿游戏背景举例,要在线程中 While 循环,不断更新背景的显示,在游戏暂停或退出时便需要有个 flag 判断 run() 完成。
       (2)防止线程重复创建及线程启动异常
        大家都知道,Android 退出当前界面有两种方式:Back 键、Home 键。对于一个 SurfaceView ,通过这两种方式退出,再返回程序中,有什么区别呢?
                Back键:surfaceDestroyed ——> 构造函数 ——> surfaceCreated ——> surfaceChanged;
                Home键:surfaceDestroyed ——> surfaceCreated ——> surfaceChanged
        很明显,Back 键返回再进入额外执行了一次构造函数,也就是说,Back 返回再进入时 SurfaceView 会重新加载一次。也正是这个原因,线程的初始化和线程启动的位置就很有讲究了:
        
        有  种方式(总不能 thread 先启动再初始化吧):
        蓝色线 :线程初始化在构造函数(以下称为 struct)中,线程启动在视图创建函数(以下称为 surfaceCreated)中,在这种情况下会有线程启动异常情况发生:点 Home 键返回再进入时,会尝试启动线程,此时报错如下:
        
        原因是 Home 情况下未执行 struct 便没有新创建 thread,尝试启动是线程仍是之前已经启动过的线程,系统报错线程已经启动。(附加:Back 情况正确运行);
        黑色线 :线程初始化和启动都在 struct 中,这种情况更明显:Home 情况下,再次进入后线程不会再运行,视图不动了;
        最好的办法就是 红色线 :线程初始化和启动都在 surfaceCreated 中,并且在 surfaceDestroy 时将 flag 置为 false 使线程停止运行而销毁。这样既避免了线程的重复创建(flag 的作用),又避免了“线程已启动”的异常。

        (二)说明2:视图绘制时的异常 try

        因为 SurfaceView 在未创建或者不可编辑时,调用 lockCanvas() 时将返回 null,Canvas 在绘图时也将出现不可预知的错误,所以用 try...catch...进行处理,并且为避免 lockCanvas() 为 null 时的问题,在绘制之前判断 canvas 是否为空进行处理。

        (三)说明3:画布提交的时机

        绘图的时候可能出现不可预知的 Bug,虽然用 try...catch...包裹起来保证程序不会崩溃;但是如果在画布提交之前出现异常,本次将跳过提交,在下次获取画布时就会抛出异常,原因就是上次画布就没有解锁也没有提交。所以要将 unlockCanvasAndPost() 放在 finally 语句中。
        并且提交之前也要判断 canvas 不为 null,保证提交的是有效的画布。

        (四)说明4:视图尺寸的获取时机

        SurfaceView 的 getWidth()、getHeight() 要在视图创建之后即 surfaceCreated 调用之后执行,否则得到的结果永远为 0,因为在这之前试图还没有创建,宽高当然是 0。

        (五)说明5:刷新频率一致性的保证

        不管是动画还是变形,都要保证变动的效果要保持流畅,所以很有必要保证线程每运行完一次的时间相同,即刷新频率的一致。就像代码中做的那样,设定一个刷新周期,并记录下线程开始时的时间戳,和绘图、逻辑运行完后的时间戳,比较运行时间和刷新周期,如果“运行时间 < 刷新周期”,就让线程把刷新周期中剩余的时间“睡”过去,如果“运行时间 > 刷新周期”时,当然不要耽搁,尽快开始下一个周期。

        好了,SurfaceView 主要内容就差不多了,可是累死了!如感觉对你有帮助,那就一起来继续下一篇 “Android 视图框架系列3/3——View和SurfaceView之间的抉择” 吧!
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值