美文网首页
SurfaceView基本使用

SurfaceView基本使用

作者: echoSuny | 来源:发表于2020-07-07 17:24 被阅读0次

    一般来讲,绝大多数的自定义控件都是继承自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):用于获取指定区域的画布。这个画布以外的内容则保持和屏幕内容一致,画布以内的区域仍然保持原画布的内容。

    相关文章

      网友评论

          本文标题:SurfaceView基本使用

          本文链接:https://www.haomeiwen.com/subject/dmmvqktx.html