1.媒介
从零开始的车载Android HMI是一个系列性的文章,目标在于展示一些在Android手机应用开中不常用,但是在车载应用开辟中较为常用的一系列Android HMI 组件,盼望可以或许资助初入车载应用开辟的同砚相识车载应用开辟过程中常用的各种UI 组件。
RE: 从零开始的车载Android HMI(一) - Lottie
RE: 从零开始的车载Android HMI(二) - Widget
本文参考资料:
《Android自界说控件开辟入门与实战》 - 启舰
Understanding Canvas and Surface concepts
Surface | Android Developers
2.SurfaceView 简介
信赖每一个Android初学者在自学Android编程时都使用过VideoView来播放视频,当打开VideoView的源码时,会发现VideoView并不是直接继续自我们常用的ViewGroup或是View,它现实上继续自一种更特别的View - SurfaceView
2.1.SurfaceView 是什么
简朴来说,SurfaceView就是一个嵌入了Surface的特别View,Surface中有一个独立的画布Canvas用于绘制内容,SurfaceView本质上是这个Surface的容器,用于控制Surface的格式、尺寸等底子信息。
SurfaceView体现内容时,会在Window上挖一个洞,SurfaceView绘制的内容体如今这个洞里,其他的View继续体如今Window上。
2.2.SurfaceView 应用场景
SurfaceView的出现并不是为了取代View,当界面绘制须要频仍革新,或革新时须要处置惩罚的数据量较大时,就应该考虑使用SurfaceView,比方:视频播放、展示摄像头数据。在车载应用开辟中,我们通常使用SurfaceView展示Camera的数据,比方 泊车雷达 等应用。如果须要对来自Camera的数据进行二次处置惩罚后再展示,应该使用TextureView。
3.SurfaceView 底子用法
接下来我们写一个简朴绘的图板,来看一下SurfaceView的底子用法是怎样的。
1.创建一个继续自SurfaceView的自界说View,并初始化Paint、Path
init { paint = Paint(Paint.ANTI_ALIAS_FLAG) paint.color = Color.BLUEpaint.style = Paint.Style.STROKEpaint.strokeWidth = 5f path = Path()}2.获取SurfaceHolder,并监听Surface生命周期
init { ... // 为了代码的可读性,这里并没有使用Kotlin的简写surfaceHolder = getHolder() surfaceHolder?.addCallback(this)}override fun surfaceCreated(holder: SurfaceHolder) { flag = true drawCanvas()}override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}override fun surfaceDestroyed(holder: SurfaceHolder) { flag = false}SurfaceHolder从名字上就能看出,他是Surface的持有对象,必须通过它才华获取到绘图所必须的画布。下一节会具体先容。
3.监听手势
override fun onTouchEvent(event: MotionEvent?): Boolean { when (event?.action) { MotionEvent.ACTION_DOWN -> { path.moveTo(event.x, event.y) return true } MotionEvent.ACTION_MOVE -> { path.lineTo(event.x, event.y) } } return super.onTouchEvent(event)}通过onTouchEvent监听到屏幕上的手势移动,并将轨迹生存在Path中。
4.在子线程中将轨迹绘制到画布上
private fun drawCanvas() { Thread {while (flag) { val canvas = surfaceHolder.lockCanvas() canvas.drawPath(path, paint) surfaceHolder.unlockCanvasAndPost(canvas) } } .start()}在这一步中,通过SurfaceHolder获取到SurfaceView自带的缓冲画布,并对这个画布加锁surfaceHolder.lockCanvas()。
在绘制完成后,将缓冲画布开释,并将画布的内容更新到主线程的画布上surfaceHolder.unlockCanvasAndPost(canvas),如许缓冲画布的内容就体现到屏幕上了。
完备的源码如下所示:
class CustomSurfaceView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null,) : SurfaceView(context, attrs), SurfaceHolder.Callback { private val paint: Paint private val path: Path private val surfaceHolder: SurfaceHolder private var flag: Boolean = false init { paint = Paint(Paint.ANTI_ALIAS_FLAG) paint.color = Color.BLUEpaint.style = Paint.Style.STROKEpaint.strokeWidth = 5f path = Path() // 为了代码的可读性,这里并没有使用Kotlin的简写surfaceHolder = getHolder() surfaceHolder?.addCallback(this) } override fun onTouchEvent(event: MotionEvent?): Boolean { when (event?.action) { MotionEvent.ACTION_DOWN -> { path.moveTo(event.x, event.y) return true } MotionEvent.ACTION_MOVE -> { path.lineTo(event.x, event.y) } } return super.onTouchEvent(event) } private fun drawCanvas() { Thread {while (flag) { val canvas = surfaceHolder.lockCanvas() canvas.drawPath(path, paint) surfaceHolder.unlockCanvasAndPost(canvas) } } .start() } override fun surfaceCreated(holder: SurfaceHolder) { flag = true drawCanvas() } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { } override fun surfaceDestroyed(holder: SurfaceHolder) { flag = false }}读到这里大概会有几个疑问
- SurfaceView绘制操纵必须在在子线程中吗?
不是必须的,上述的例子,我们改造一下也可以放置在主线程中绘制。
override fun onTouchEvent(event: MotionEvent?): Boolean { ... // 每次触摸屏幕,都主动实行一次绘制 drawCanvas() return super.onTouchEvent(event) } private fun drawCanvas() { // Thread { // while (flag) { val canvas = surfaceHolder.lockCanvas() canvas.drawPath(path, paint) surfaceHolder.unlockCanvasAndPost(canvas) // } // }.start() }onTouchEvent是在主线程中触发的,我们把创建线程的代码解释掉,如许绘制的这一步就会主线程中调用。在主线程中绘制,就必须把while(flag)也解释掉,否则主线程会一连停顿在while循环中,导致UI发生ANR。
但是,如许的操纵是不须要的,如果可以绘制可以在主线程中实行,现实上就不应该使用SurfaceView。
- SurfaceView的绘制可否放在View.onDraw(canvas)中?
不行。SurfaceView在初始化时调用了setWillNotDraw(true)体现该控件没有须要绘制的内容,所以在屏幕革新时,SurfaceView的onDraw()方法默认是不会被调用。
当然我们也可以,再把它设定false来触发onDraw(),不外一般没有如许须要,由于SurfaceView自带了缓冲画布,并不须要onDraw()中画布。
init { // 为了代码的可读性,这里并没有使用Kotlin的简写surfaceHolder = getHolder() surfaceHolder?.addCallback(this) setWillNotDraw( false )}setWillNotDraw()是体系提供的一种优化计谋,它可以体系跳过那些不须要绘制的控件,好比LinearLayout在不须要绘制DividerDrawable时就会声明该属性。
- SurfaceView的画布获取时为何要加锁?
这个很好明白了,由于SurfaceView是可以在子线程中实行绘制的,如果不对画布加锁,那么多个子线程同时更新画布就会产生无法预期的情况,所以须要加锁。
着实,对画布加锁也引入了新的题目。当一个线程对调用surfaceHolder.lockCanvas()哀求画布时,另一个线程也在调用surfaceHolder.lockCanvas()就会发生非常。如下所示
public Canvas lockCanvas(Rect inOutDirty) throws Surface.OutOfResourcesException, IllegalArgumentException { synchronized (mLock) { checkNotReleasedLocked(); if (mLockedObject != 0) { // 抱负情况下,nativeLockCanvas()会在这种情况下引发并防止双重锁定,但如果mNativeObject被更新,则不会发生这种情况。 // 我们不能放弃旧的mLockedObject,由于它大概仍在使用中,所以我们只是拒绝重新锁定Surface。throw new IllegalArgumentException("Surface was already locked"); } mLockedObject = nativeLockCanvas(mNativeObject, mCanvas, inOutDirty); return mCanvas; }}所以调用surfaceHolder.lockCanvas()时要进行须要的非空判定以及参加重试机制,绘制完成后,要实时开释画布。
4.Surface & SurfaceView & SurfaceHolder
上面我们先容了SurfaceView的简朴用法,与SurfaceView相干的有三个概念分别是Surface、SurfaceView、SurfaceHolder。
4.1.Surface
Surface是一个包罗须要渲染到屏幕上的像素对象。屏幕上的每一个窗口都有自己的Surface,而SurfaceFlinger会按照精确的Z轴顺序,将它们合成在屏幕上。
一个Surface会有多个缓冲区来进行双缓冲渲染,体如今屏幕上称为前端缓冲区,还没有体如今屏幕上的称为后端缓冲区,如许应用步伐可以先在后端缓冲区绘制下一帧的内容,每隔一段时间交换两块缓冲区,如许就不须要期待全部内容都绘制完毕,屏幕上就可以体现出内容。
信赖肯定有人会有如许的疑问,Surface、Window、View之间是什么关系?
Window基本上就是我们常见应用步伐的窗口,WindowManger会为每个Window创建一个Surface,并将其提供给应用步伐进行绘制。对于WindowManager来说,Surface只是一个不透明的矩形而已。
View是体如今Window内的可交互的UI元素。View依附于Window,而且运用Window提供的Surface进行UI绘制。
4.2.SurfaceView
SurfaceView是一种特别View,上面我们提到过View用来绘制的Surface是Window提供的,但是SurfaceView差别。SurfaceView持有一个独立的Surface,专门用于一些特别且耗时的绘制。
SurfaceView 的常用方法
- getHolder()
返回SurfaceHolder,提供对该SurfaceView底层Surface的访问和控制。
- setSecure(isSecure: Boolean)
设定是否应将SurfaceView的内容视为安全内容,防止其出如今屏幕截图中或在不安全的体现器上检察。
- setZOrderMediaOverlay(isMediaOverlay: Boolean)
设定SurfaceView的Surface是否放置在窗口中另一个通例Surface的顶部(但仍位于窗口自己的背面)。
- setZOrderOnTop(onTop: Boolean)
设定SurfaceView的Surface是否放置在其窗口的顶部。
4.3.SurfaceHolder
从字面意义上来明白,它就是Surface的持有者,我们在使用SurfaceView时并不能直接操纵Surface,否则大概会产生一些不可预期的操纵,所以Android为我们提供SurfaceHolder来间接操纵Surface。
读到这里应该就能明白了SurfaceView属于范例的MVC构型,Surface中生存着屏幕的绘制信息属于Model,在SurfaceView(View)中借助Surface进行绘制时,须要通过SurfaceHolder(Controller)来操纵Surface。
SurfaceView的MVC构型非常具有参考意义,我们自己在编写较为复杂的自界说View时,应该参考这种筹划思绪。
SurfaceHolder 的常用方法
- addCallback ( SurfaceHolder.Callback callback)
监听Surface的生命周期。
surfaceHolder?.addCallback(object : SurfaceHolder.Callback{ override fun surfaceCreated(holder: SurfaceHolder) { } override fun surfaceChanged( holder: SurfaceHolder, format: Int, width: Int, height: Int ) { } override fun surfaceDestroyed(holder: SurfaceHolder) { }})surfaceCreated:当Surface被创建后,就会被立即调用。
surfaceChanged:当Surface发生任何布局性变革时,就会被立即调用。
surfaceDestroyed:当Surface被烧毁时,就会被立即调用。
- removeCallback ( SurfaceHolder.Callback callback)
移除回调。
- Canvas lockCanvas ()
获取Surface中Canvas
- Canvas lockCanvas ( Rect dirty)
获取指定地区的Canvas。一般在须要局部更新时会用到。
有关“局部更新”下一末节会先容。
- Canvas lockHardwareCanvas **()******
获取硬件加快的Canvas。
- unlockCanvasAndPost ( Canvas canvas)
开释Canvas。使用Canvas绘制完成后,必须开释Canvas才会体如今屏幕上
- Surface getSurface ()
获取Surface对象。
- Rect getSurfaceFrame ()
获取当前Surface的巨细
- boolean isCreating ()
Surface是否正在创建中
- void setFixedSize (int width, int height)
将Surface设定为固定巨细
- void setFormat (int format)
设定Surface的像素格式
- void setSizeFromLayout ()
允许Surface根据容器的布局巨细,调解Surface的巨细。默认就会被调用。
5.SurfaceView 双缓冲机制
5.1.概述
前面我们提到过Surface会基于双缓冲机制进行渲染,简朴来说就是,体如今屏幕上的是前端缓冲区,通过surfaceHolder.lockCanvas()获取到的是后端缓冲区,调用surfaceHolder.unlockCanvasAndPost(canvas)后会交换前后端缓冲区,此时用户绘制在后端缓冲区上的内容就能体如今屏幕上,云云循环。
这里的“缓冲区”就是我们用于绘制的“画布”。
双缓冲机制大大提升了图形的渲染效率,但也造成一些题目,两块相互瓜代的画布上面的内容肯定是不一样的,在多线程的情况下尤其云云。
private fun drawCanvas() { Thread {for (i in 0..9) { val canvas = surfaceHolder.lockCanvas() Log.e("TAG", "drawCanvas: $i" ) canvas?.drawText("$i", i * 30f, 50f, paint) surfaceHolder.unlockCanvasAndPost(canvas) } } .start() }比方,上面的代码,每次循环时获取画布绘制完一个数字后,将前后端缓冲区交换,循环10次。运行代码,我们看到的如下的景象。
- 屏幕为什么只体现了4个数字?
这是由于其他的数字绘制在别的画布上,所以没有体现出来。这也证明白,前端缓冲区颠末交换后,转换为后端缓冲区时,并没有把正体如今屏幕上的缓冲区内容复制下来。
- 为什么是0 3 6 9?两个缓冲区的不应该是1 3 5 7 9吗?
缘故起因很简朴,固然名义上叫双缓冲区但着实并不止两个缓冲区,在这个例子中,现实上有三个缓冲区。
根据Goodle的官方文档表明,Surface中缓冲区个数是根据需求动态分配的,如果用户获取画布的频率较慢,那么将会分配两个缓冲区,否则,将分配3的倍数个缓冲区。总得来说,Surface分配的缓冲画布数量会大于便是2,具体多少须要视情况而定。
private fun drawCanvas() { Thread {for (i in 0..9) { val canvas = surfaceHolder.lockCanvas() Log.e("TAG", "drawCanvas: $i") canvas?.drawText("$i", i * 30f, 50f, paint) surfaceHolder.unlockCanvasAndPost(canvas) Thread.sleep(500) } } .start() }当我们将画布的获取频率低沉时,就可以看出每次缓冲区交换时绘制在屏幕上的内容。这里就可以很显着的看出,当画布的获取频率较慢时,体系只分配了两个画布。如下所示:
原理是搞清楚了,那么我们应该怎么把数字完备地绘制到画布上呢?着实很简朴,把每次绘制存在起来,下一次绘制时,把上一次的数字也绘制出来就可以了。
private val nums: MutableList<Int> = mutableListOf()private fun drawCanvas() { Thread {for (i in 0..9) { val canvas = surfaceHolder.lockCanvas() nums.add(i) for (num in nums) { canvas?.drawText("$num", num * 30f, 50f, paint) } surfaceHolder.unlockCanvasAndPost(canvas) Thread.sleep(500) } } .start()}
5.2.双缓冲局部更新原理
在前面的SurfaceHolder常用方法中,我们提到lockCanvas``(rect)一般会在须要SurfaceView局部更新时用到,那么它和lockCanvas()有什么区别呢?
lockCanvas(): 获取整屏画布。获取到的画布,不是当前屏幕正在体现的画布。获取到画布中绘制的内容,并不一定是当前体如今屏幕上的内容。具体缘故起因,上面已经先容过了。
lockCanvas (Rect dirty): 获取指定的地区的画布。 画布以外的地区保持与屏幕内容划一,画布内的地区依然保持原画布内容。
lockCanvas(Rect dirty)在使用时容易产生一些误解,我们来看一个例子:
private fun drawCanvas() { Thread { // 第一次绘制var canvas = surfaceHolder.lockCanvas(Rect(0, 0, 768, 768)) canvas.drawColor(Color.RED) Log.e("TAG", "drawCanvas1{canvas.clipBounds} ") surfaceHolder.unlockCanvasAndPost(canvas) //第二次绘制canvas = surfaceHolder.lockCanvas(Rect(100, 100, 600, 600)) canvas.drawColor(Color.BLUE) Log.e("TAG", "drawCanvas2{canvas.clipBounds} ") surfaceHolder.unlockCanvasAndPost(canvas) //第三次绘制canvas = surfaceHolder.lockCanvas(Rect(150, 150, 500, 500)) canvas.drawText("3.WU", 200f, 200f, paint) Log.e("TAG", "drawCanvas3{canvas.clipBounds} ") surfaceHolder.unlockCanvasAndPost(canvas) //第四次绘制canvas = surfaceHolder.lockCanvas(Rect(200, 200, 400, 400)) canvas.drawText("4.JIA", 200f, 300f, paint) Log.e("TAG", "drawCanvas4{canvas.clipBounds} ") surfaceHolder.unlockCanvasAndPost(canvas) } .start()}第一次绘制:获取地区画布,绘制上蓝色。
第二次绘制:获取地区画布,绘制成赤色。
第三次绘制:获取地区画布,绘制笔墨“3.WU”
第四次绘制:获取地区画布,绘制笔墨“4.JIA”
运行代码后,我们会看到下面的情况
不知道绘制出的界面,是否与你渴望的征象一样呢?信赖你看到这里大概会产生如许几个疑问
- 第三次绘制并没有绘制配景,为什么配景是玄色的?
由于这里有三个缓冲画布,第三次绘制时,使用的是一块空缺的画布,空缺画布的默认配景就是玄色。
- 第一次绘制应该是指定地区绘制成赤色,为什么看到的却是全屏都是赤色?
打开运行日记,我们会发现:第一获取指定地区的画布巨细,并不是我们指定的巨细,现实上获取到的是全屏画布。
这是由于当我们第一次获取画布时,这块画布还没有被画过,属于脏画布,体系以为它都应该被画上,所以返回了全屏的画布。
由于如许的机制存在,现实上我们在获取地区画布时须要判定,是否是我们指定的地区画布,否则就须要先把画布清算一遍,才华获取到我们渴望的地区画布。清屏代码如下:
while (true) { val canvas = surfaceHolder.lockCanvas(Rect(0, 0, 1, 1)) val rectCanvas = canvas.clipBoundsif (rectCanvas.height() == height && rectCanvas.width() == width) { canvas.drawColor(Color.WHITE) surfaceHolder.unlockCanvasAndPost(canvas) } else { surfaceHolder.unlockCanvasAndPost(canvas) break }}参加清屏代码后,再来看看绘制的效果是怎样的
完备的代码如下:
private fun drawCanvas() { Thread {while (true) { val canvas = surfaceHolder.lockCanvas(Rect(0, 0, 1, 1)) val rectCanvas = canvas.clipBoundsLog.e("TAG", "drawCanvas0{rectCanvas} ") if (rectCanvas.height() == height && rectCanvas.width() == width) { canvas.drawColor(Color.WHITE) surfaceHolder.unlockCanvasAndPost(canvas) } else { surfaceHolder.unlockCanvasAndPost(canvas) break } } // 第一次绘制var canvas = surfaceHolder.lockCanvas(Rect(0, 0, 768, 768)) canvas.drawColor(Color.RED) Log.e("TAG", "drawCanvas1{canvas.clipBounds} ") surfaceHolder.unlockCanvasAndPost(canvas) Thread.sleep(500) //第二次绘制canvas = surfaceHolder.lockCanvas(Rect(100, 100, 600, 600)) canvas.drawColor(Color.BLUE) Log.e("TAG", "drawCanvas2{canvas.clipBounds} ") surfaceHolder.unlockCanvasAndPost(canvas) Thread.sleep(500) //第三次绘制canvas = surfaceHolder.lockCanvas(Rect(150, 150, 500, 500)) canvas.drawText("3.WU", 200f, 200f, paint) Log.e("TAG", "drawCanvas3{canvas.clipBounds} ") surfaceHolder.unlockCanvasAndPost(canvas) Thread.sleep(500) //第四次绘制canvas = surfaceHolder.lockCanvas(Rect(200, 200, 400, 400)) canvas.drawText("4.JIA", 200f, 300f, paint) Log.e("TAG", "drawCanvas4{canvas.clipBounds} ") surfaceHolder.unlockCanvasAndPost(canvas) Thread.sleep(500) } .start()}6.SurfaceView 使用留意事项
6.1.SurfaceView的配景
默认情况下SurfaceView渲染时会体现玄色的配景,如果当我们须要体现透明的配景可以使用如下的代码。弊端是SurfaceView会体如今Window的顶层,遮住其他的View。
surfaceHolder.setFormat(PixelFormat.TRANSPARENT)setZOrderOnTop(true)
如果不盼望遮住上层的View,那么折中的办法是,在SurfaceView的画布上把底层配景绘制出来。当容器的配景很复杂时,变乱就会变得贫困了,这种情况大概TextureView更符合。
private fun drawCanvas() { Thread {while (flag) { val canvas = surfaceHolder.lockCanvas() canvas?.drawColor(context.resources.getColor(R.color.purple_200)) canvas?.drawPath(path, paint) surfaceHolder.unlockCanvasAndPost(canvas) } } .start()}6.2.画布内容差别等
由于双缓冲机制的影响,我们获取到的画布上的内容,与我们现实渴望的大概是不一样的。办理方案有两种
- 生存每次绘制的内容
这个办理方案就是第一个绘制轨迹的例子采取的方案。使用Path生存每次手指的轨迹,在获取到画布时,将Path整个绘制上去。
- 内容不交织时,增量绘制
当每次绘制的内容不会产生交织时,也可以使用lockCanvas(Rect dirty),采取增量绘制的方式,只把怎么每次新增的内容绘制上去,使用lockCanvas(Rect dirty)时要留意先清屏!
|