Android 浅谈SurfaceView

SurfaceView简介

SurfaceView在开发中可能不常用到,但是在游戏、视频或者复杂的交互特效中却独显优势了。可能有些开发者说,还有GLSurfaceView或者TextureView,这两个View不在本文探讨范围内。

SurfaceView也是继承自View,不过现在高版本上面查看源码可能是MockView,如果熟悉单元测试,可以看到这种命名方式就是测试中常用的命名方式,可以在/frameworks/base/core/java/android/view/SurfaceView.java查看源码。由于SurfaceView是View的子类,因此可以像其它View一样操作。

既然SurfaceView是View的子类,为什么又有特有优势呢?答案就在于它的实现中内置了一个专门用于绘制的Surface,可以通过SurfaceView来控制Surface的格式和尺寸。

Surface是纵深排序(Z-ordered)的,这点类似css属性z-index,如果熟悉前端开发很容易理解,需要注意的是Surface总在自己所在窗口的后面。SurfaceView提供了一个可见区域,只有在这个可见区域内的Surface部分内容才可见,可见区域外的部分不可见。Surface的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者Surface的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件)。注意,如果Surface上面有透明控件,那么它的每次变化都会引起框架重新计算它和顶层控件的透明效果,这会影响性能。

由于SurfaceView提供了专门用于绘制的Surface,所以的它内容的更新就与常规View有很大的不同,通常View更新的时候都会调用ViewRootImpl中的performXXX()方法,在该方法中会首先使用checkThread()检查是否当前更新位于主线线程,这也是为什么View层级树的更新不可以在子线程。但是,SurfaceView更新就不需要考虑线程的问题,它既可以在子线程更新,也可以在主线程更新。

以前不是使用SurfaceView自定义View时,如果绘制的界面有大量复杂的算法,或者绘制Bitmap时,经常使用Canvas实现一个双缓冲机制,以增强画面的连续性。但是,如果自定义View是扩展自SurfaceView,则不需要再自己实现双缓冲了,因为SurfaceView底层就已经实现了双缓冲机制。

由于SurfaceView可以使用子线程更新UI,这样就不会阻塞主线程交互,再加上它本身就支持双缓冲机制,所以一般常用于对画面有更高要求的交互。优点很明显,当然了也有缺点,SurfaceView对于动画的支持并不是很好,网上有很多博文说SurfaceView不支持动画,其实这句话是很有歧义的。比如在Android5.1和Android8.0都支持属性动画的平移和缩放,但是在5.1系统上面补间动画的就不支持了。透明度动画一直都不支持,如果是旋转动画,看到的效果是整个屏幕界面可以旋转,但是画面不会随着旋转。。。

接下来看一下SurfaceView中相关的几个类。

SurfaceHolder

在上面介绍说SurfaceView中内置了一个专门用于绘制的Surface,这个Surface对象使应用能够渲染要在屏幕上显示的图像。通过SurfaceHolder 接口,开发时可以编辑和控制Surface。

SurfaceHolder是系统用于与应用共享Surface所有权的接口。与Surface配合使用的一些客户端需要SurfaceHolder,因为用于获取和设置 Surface参数的API是通过SurfaceHolder实现的。一个SurfaceView包含一个SurfaceHolder。

从SurfaceHolder名字也可以知道,类似开发中常常定义的ViewHolder,可以理解为SurfaceHolder中持有了一个Surface。

SurfaceView生命周期

这里之所以说SurfaceView的生命周期,主要是因为它有几个回调方法跟Activity生命周期关联。

SurfaceView通过持有的Surface绘制,因此SurfaceView的所有绘制工作必须在Surface创建完成后才可以开始,在Surface销毁前结束,如何知道Surface的创建和销毁呢,通过如下回调就可以监听Surface的改变。

public interface Callback {
	public void surfaceCreated(SurfaceHolder holder);

	public void surfaceChanged(SurfaceHolder holder, int format, int width, int height);

	public void surfaceDestroyed(SurfaceHolder holder);
}
  • surfaceCreated():surface创建的时候调用,一般初始化一些图片、线程资源都是在该方法中。
  • surfaceChanged():surface尺寸发生改变的时候调用,如横竖屏切换、PixelFormat有变动。在surfaceCreated方法被调用后,该方法至少会被调用一次。
  • surfaceDestroyed():surface销毁时被调用。当surface被销毁时,有些与界面相关的资源应该被释放掉,这些代码应写在surfaceDestroyed方法中。

如下是Activity和SurfaceView声明周期的日志信息,操作步骤是先启动Activity,然后点击Home键,最后再返回Activity。

==启动Activity
MainActivity: onCreate:
MainActivity: onStart:
MainActivity: onResume:
MySurfaceView: surfaceCreated:
MySurfaceView: surfaceChanged:

==点击Home键
MainActivity: onPause:
MySurfaceView: surfaceDestroyed:
MainActivity: onStop:
MainActivity: onStart:

==重新进入Activity
MainActivity: onResume:
MySurfaceView: surfaceCreated:
MySurfaceView: surfaceChanged:

从日志中可以看出,SurfaceView必须在Activity的onResume()方法之后才会执行Surface的创建方法surfaceCreated(),当Activity执行onPause()方法后会执行Surface的surfaceDestroyed()方法。

如果是点按电源按钮锁屏,则只会出现onPause()没有 surfaceDestroyed()。Surface仍处于活动状态,并且可以继续渲染。

自定义SurfaceView

当继承SurfaceView自定义View时,在布局文件的XML文件中不建议设置背景色,因为SurfaceView的Surface默认是在窗口后面的,如果设置了背景色,这时候绘制时就看不到绘制的图像了。如果需要背景色可以直接绘制在画板上面。

SurfaceView虽然继承自View,但是并没有重写onDraw()方法,所以,即使重写了surfaceView的onDraw()方法也不会被调用。如果想要调用onDraw方法,可以在SurfaceHolder.Callback接口的surfaceCreated()方法中添加一句setWillNotDraw(false),在开发中并不建议这么做。

在SurfaceView中绘制图像也是通过Canvas实现的,虽然onDraw()方法不能调用了,但是可以通过lockCanvas()拿到Canvas对象。这里需要记住,在调用lockCanvas()方法获取Canvas后,SurfaceView会获取Surface的一个同步锁,这个锁会一直到调用unlockCanvasAndPost(Canvas canvas)方法才释放,这里的同步机制保证在Surface绘制过程中不会被改变(被摧毁、修改)。

public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
	....
	surfaceHolder = getHolder();
	surfaceHolder.addCallback(callback);
	
	// 将Surface的Z-index提升到前面
	setZOrderOnTop(true);
	// 设置SurfaceView背景为透明背景
	surfaceHolder.setFormat(PixelFormat.TRANSPARENT);
}

private Thread thread = new Thread(new Runnable() {
	@Override
	public void run() {
		Canvas canvas = surfaceHolder.lockCanvas();
		canvas.drawCircle(500, 500, 200, paint);
		surfaceHolder.unlockCanvasAndPost(canvas);
	}
});

private SurfaceHolder.Callback callback = new SurfaceHolder.Callback() {
	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		Log.d(TAG, "surfaceCreated: ");
		thread.start();
	}

	@Override
	public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
		Log.d(TAG, "surfaceChanged: ");
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder holder) {
		Log.d(TAG, "surfaceDestroyed: ");
	}
};

如果要绘制一个动态效果,应该怎么实现呢,在普通的View中,只要视图有变化就会自动调用onDraw()方法,其实这里也容易实现,直接写个死循环即可。当然了要对外暴露一个停止的标记位,当Surface停止时,一定要停止绘制。

这里仍然使用xxxx中的代码示例。
private class RenderThread extends Thread {
	private SurfaceHolder surfaceHolder;

	public RenderThread(SurfaceHolder holder) {
		this.surfaceHolder = holder;
	}

	@Override
	public void run() {
		startTime = System.currentTimeMillis();
		while (true) {
			if (!runFlag) {
				return;
			}
			Canvas canvas = surfaceHolder.lockCanvas();
			
			// 一定要注意判空
			if (canvas == null) {
				return;
			}
			
			//绘制代码省略...
			
			surfaceHolder.unlockCanvasAndPost(canvas);
		}
	}
}

private SurfaceHolder.Callback callback = new SurfaceHolder.Callback() {
	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		Log.d(TAG, "surfaceCreated: ");
		runFlag = true;
		RenderThread thread = new RenderThread(holder);
		thread.start();
	}

	@Override
	public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
		Log.d(TAG, "surfaceChanged: ");
	}

	@Override
	public void surfaceDestroyed(SurfaceHolder holder) {
		Log.d(TAG, "surfaceDestroyed: ");
		runFlag = false;
	}
};

参考资料

Android-SurfaceView生命周期 SurfaceView 和 GLSurfaceView SurfaceView Android 使用SurfaceView进行2D动画的开发 Android SurfaceView 源码分析及使用 Android SurfaceView 多线程绘图

评论

您确定要删除吗?删除之后不可恢复