SurfaceView基本使用
一般来讲,绝大多数的自定义控件都是继承自View或者ViewGroup。但是往往自定义View处理不好的话机会发生卡顿以及性能问题。但是有时候复杂的逻辑处理又是必须的,所以就引入了SurfaceView。
SurfaceView引入了双缓冲技术,并且自带画布,支持在子线程绘制。所谓双缓冲技术,简单来讲就是多加了一块缓冲画布,当需要绘制时,先在缓冲画布上绘制,完成之后再将缓冲画布上的内容更新到主画布上。这样就不会存在逻辑处理时间的问题。
基本用法
SurfaceView派生自View类,和我们的自定义View或者TextView,ImageView没什么区别,都是View的子类。所以SurfaceView或者SurfaceView的子类可以使用View的所有方法和属性。
那么就首先就自定义一个View继承SurfaceView来感受一下:
class UseSurfaceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : SurfaceView(context, attrs, defStyleAttr) {
val paint: Paint
init {
paint = Paint()
paint.apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 8f
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawCircle(width / 2f, height / 2f, 300f, paint)
}
}
我们在view的中心画了一个绿色的圆,现在来看一下效果:
屏幕漆黑一片,这是怎么回事?下面在onDraw()函数中打印一下log:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
Log.d("----->", "onDraw: ")
canvas.drawCircle(width / 2f, height / 2f, 300f, paint)
}
一片空白!原来onDraw()函数根本没有调用。下面我们在init{ }中加入一句代码setWillNotDraw(false),再来看一下效果:
init {
paint = Paint()
paint.apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 8f
}
setWillNotDraw(false)
}
可以看到圆显示出来了,log也打印了。
setWillNotDraw()函数位于View类当中。当设置为true时,表示当前控件没有绘制内容,当屏幕重绘时,这个控件不需要绘制,所以就不会调用onDraw()函数。反之,则表示每次重绘都需要绘制该控件。其实是一种优化手段,让控件显式的告诉系统谁需要重绘,谁不需要重绘,从而提高绘制效率。一般而言一些布局会经常用到这个函数,例如LinearLayout。之所以没有调用onDraw()就是因为SurfaceView在初始化的时候调用了setWillNotDraw(true),所以才需要在继承SurfaceView的时候要显式的调用setWillNotDraw(false)。由此可见,SurfaceView并不希望我们重写onDraw()函数来进行绘制,不然和直接继承View有什么分别,也发挥不了SurfaceView的真正作用,违背了设计这个类的初衷。
class UseSurfaceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : SurfaceView(context, attrs, defStyleAttr) {
val paint: Paint
init {
paint = Paint()
paint.apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 8f
}
holder.addCallback(object :SurfaceHolder.Callback{
override fun surfaceChanged(
holder: SurfaceHolder?,
format: Int,
width: Int,
height: Int
) { }
override fun surfaceDestroyed(holder: SurfaceHolder) { }
override fun surfaceCreated(holder: SurfaceHolder) {
val canvas = holder.lockCanvas()
canvas.drawCircle(width / 2f, height / 2f, 300f, paint)
holder.unlockCanvasAndPost(canvas)
}
})
}
}
前面提到了SurfaceView可以在子线程中绘制,所以这还不是正确的写法,下面我们就把绘制的部分放在子线程中:
fun drawCircle(){
Thread{
val canvas = holder.lockCanvas()
canvas.drawCircle(width / 2f, height / 2f, 300f, paint)
holder.unlockCanvasAndPost(canvas)
}.start()
}
Surface生命周期
其实在上个例子中就已经用到了Surface的生命周期函数。这其中涉及到三个概念:Surface,SurfaceView,SurfaceHolder。Surface保存着缓冲画布和绘图内容相关的所有信息。SurfaceView负责和用户交互,SurfaceHolder用来操作Surface。
holder.addCallback(object :SurfaceHolder.Callback{
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
// 当Surface的格式或大小发生改变时会立即调用
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// 当Surface对象将要销毁时会立即调用
}
override fun surfaceCreated(holder: SurfaceHolder) {
// 当Surface对象被创建后会立即调用
}
})
也就是说当我们需要画图的时候一般都是在surfaceCreated()中来开启线程。如果不在这个回调中使用的话,surface有可能是空的,而surface保存了缓冲画布,那么就不能得到缓冲画布进行绘制。
class UseSurfaceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : SurfaceView(context, attrs, defStyleAttr) {
val paint: Paint
var bgBtm: Bitmap
var flag = true
var offset = 0f
val moveStep = 1f // 每次移动的距离
var moveLeft = true
init {
paint = Paint()
paint.apply {
color = Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 8f
bgBtm = BitmapFactory.decodeResource(resources, R.drawable.flower)
}
holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceChanged(
holder: SurfaceHolder?,
format: Int,
width: Int,
height: Int
) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
flag = false
}
override fun surfaceCreated(holder: SurfaceHolder) {
flag = true
// 缩放图片的宽为view的1.5倍,使图片可以有空间左右移动
bgBtm = Bitmap.createScaledBitmap(bgBtm, width * 3 / 2, height, true)
Thread {
// 开启子线程死循环
while (flag) {
drawBg(holder)
Thread.sleep(50)
}
}.start()
}
})
}
private fun drawBg() {
val canvas: Canvas
try {
canvas = holder.lockCanvas()
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
canvas.drawBitmap(bgBtm, offset, 0f, null)
// 向左移动就每次递减移动的距离
if (moveLeft) {
offset -= moveStep
} else {
offset +=moveStep
}
if (offset <= -width / 2) {
moveLeft = false
}
if (offset >= 0) {
moveLeft = true
}
} finally {
holder.run { unlockCanvasAndPost(canvas) }
}
}
}
SurfaceView的双缓冲技术
双缓冲技术需要两个图形缓冲区,一个前端缓冲区,一个后端缓冲区。前端缓冲区对应当前屏幕上正在显示的内容,后端缓冲区则是接下来要渲染的图形缓冲区。我们通过lockCanvas()获得的是后端缓冲区。当绘图完成之后,调用unlockCanvasAndPost()将后端缓冲区与前端缓冲区交换。后端缓冲区变成前端缓冲区,将内容显示在屏幕上。而原来的前端缓冲区变成后端缓冲区,等待下一次lockCanvas()函数调用返回给用户使用,如此往复。
正是由于两块画布交替绘图,在绘图完成之后交换,而且绘制完成之后直接更新到屏幕上,才使得效率大大提高。但是这样却会存在一个问题:两块画布上的内容不一致。尤其是在多线程的情况下。例如,当我们使用一个线程操作A、B两块画布,且A目前是前端画布。所以当lockCanvas调用之后获得的是B画布。当更新以后,B画布更新到屏幕上,A和B交换位置。而此时如果线程再次申请画布,得到的将是A画布。如果A画布和B画布上的内容不一致,那么在A画布上继续绘制,将肯定和预想的不一样。
假如我们在surfaceCreated()方法中有如下代码:
for (i in 0 until 10) {
val canvas = holder.lockCanvas()
canvas.drawText("$i", i * 40f, 100f, paint)
holder.unlockCanvasAndPost(canvas)
}
按照上面的双缓冲有两个画布的逻辑,显示的应该是B画布,上面的数字也应该是1、3、5、7、9才对。为什么会是这样呢?
现在开启一个子线程并在每次循环的时候休眠一秒:
Thread {
for (i in 0 until 10) {
val canvas = holder.lockCanvas()
canvas.drawText("$i", i * 40f, 100f, paint)
holder.unlockCanvasAndPost(canvas)
Thread.sleep(1000)
}
}.start()
可以看到前三个数字是分别画在三张画布上的。不然就不应该依次显示0、1、2,而是应该0、1、(0 2)。而Google官方给的说明则是:Surface中的缓冲画布的数量是根据需求动态分配的。如果用户获取画布的频率较慢,那么将会分配两块画布。否则将分配3的倍数块缓冲画布。具体多少块,视情况而定
故可以得出结论:Surface肯定会被分配大于等于两个缓冲区,具体多少不可而知
双缓冲局部更新
SurfaceView是支持局部更新的。通过lockCanvas(Rect dirty)函数传入一个矩形来指定画布的区域和大小。这个矩形区域以外的部分将会把现在屏幕上的内容复制过来,以保持一致。
下面说明一下lockCanvas()和lockCanvas(Rect dirty)的区别:
- lockCanvas():用于获取整屏画布。屏幕上的内容不会更新到画布上,画布保持原画布内容。假设只有两块画布A、B。现在A为缓冲画布且正在绘制,绘制了一个圆以后被更新为前端画布显示在屏幕上了。那么当下次A成为缓冲画布的时候,B就肯定为前端画布,且屏幕上显示的是B刚刚画的内容。那么此刻作为缓冲画布的A的上面就只有上一次A作为缓冲画布时候画的圆,B的内容,也就是屏幕上的内容是不会复制到A上的。
- lockCanvas(Rect dirty):用于获取指定区域的画布。这个画布以外的内容则保持和屏幕内容一致,画布以内的区域仍然保持原画布的内容。