SurfaceView基本使用

2020-07-07  本文已影响0人  echoSuny

一般来讲,绝大多数的自定义控件都是继承自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)的区别:

上一篇下一篇

猜你喜欢

热点阅读