引子
SurfaceView 是Android中較為特殊的視圖,它繼承自View,但與View不同的是它用于單獨的繪畫圖層,平行與目前Activity的獨立繪畫圖層,且它的圖層在層次排列上在Activity圖層的下面,是以需要在Activity圖層上限時一塊透明的區域,用于顯示SurfaceView圖層,是以其本質是SurfaceView本身任然為Activity其上的一個透明子View,隻是SurfaceView中有一個Surface對象用于繪制一個平行與目前Activity且處于surfaceView之下的圖層。Surface相較于Acitivity圖層的不同在于,Activity圖層之上的每一個View的繪制都會導緻Activity的重繪,View通過重新整理來重繪視圖,Android系統通過發出VSYNC信号來進行螢幕的重繪,重新整理的時間間隔一般為16ms,在一些需要頻繁重新整理的界面,如果重新整理執行很多邏輯繪制操作,就會導緻重新整理使用時間超過了16ms,就會導緻丢幀或者卡頓,比如你更新畫面的時間過長,那麼你的主UI線程會被你的繪制函數阻塞,那麼将無法響應按鍵,觸屏等消息,會造成 ANR 問題。而與View不同SurfaceView的繪制方式效率非常高,因為SurfaceView的視窗重新整理的時候不需要重繪應用程式的視窗,SurfaceView擁有獨立的繪圖表面,即它不與其宿主視窗共享同一個繪圖表面,由于擁有獨立的繪圖表面,是以SurfaceView的UI就可以在一個獨立的線程中進行行繪制,由于不占用主線程資源,使得它可以實作大多複雜而高效的界面繪制,如視訊播放 VideoView 和OpenGl es的 GLSurfaceView。
SurfaceView的特點
-
SurfaceView屬于被動繪制
當SurfaceView為可見狀态下調用surfaceCreated(),建立其内的Surface圖層,并可以進行繪制的初始化操作。當surfaceView為隐藏狀态(不可見)目前surface會被銷毀。屬于被動調用,但并不是說不能主動繪制,一般的,在SurfaceHolder.Callback的surfaceCreated與surfaceDestroyed之間都是可以正常進行繪制的。
-
SurfaceView 可在任意線程進行繪制
與一般的View必須在主線程中繪制不同,SurfaceView由于其特有的單獨圖層的特性讓其可以在任意線程中繪制,可以減少對主線程資源的持有和完成大多比較平凡耗時的繪制工作。
-
SurfaceVie使用雙緩沖機制
雙緩沖即在記憶體中建立一個與螢幕繪圖區域一緻的對象,先将圖形繪制到記憶體中的這個對象上,再一次性将這個對象上的圖形拷貝到螢幕上,這樣能大大加快繪圖的速度。在圖形圖象處理程式設計過程中,雙緩沖是一種基本的技術。在Android中當要繪制的資料量比較大,繪圖時間比較長時,重複繪圖會出現閃爍現象,引起閃爍現象的主要原因是視覺反差比較大,使用雙緩沖技術可以有效解決這個問題。
SurfaceView的使用
- 建立一個自定義的SurfaceView,并實作其内的SurfaceHolder.Callback,如下:
package cn.enjoytoday.shortvideo.test.ui.customsurface
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.SurfaceHolder
import android.view.SurfaceHolder.SURFACE_TYPE_NORMAL
import android.view.SurfaceView
import java.lang.Exception
/**
* 作者: hfcai
* 時間: 19-4-22
* 部落格: http://www.enjoytoday.cn
* 描述: 自定義SurfaceView
*/
class CustomerSurfaceView(context: Context, attributes: AttributeSet?, defStyleAttr:Int)
:SurfaceView(context,attributes,defStyleAttr), SurfaceHolder.Callback {
var mIsDrawing = false
var x =1
var y = 0;
private var mPath:Path?=null
private var mPaint: Paint?=null
var mCanvas:Canvas?=null
/**
* 圖層改變,當Surface的狀态(大小和格式)發生變化的時候會調用該函數,在surfaceCreated調用後該函數至少會被調用一次
*/
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
}
/**
* 圖層銷毀,surfaceView隐藏前surface會被銷毀
*/
override fun surfaceDestroyed(holder: SurfaceHolder?) {
mIsDrawing =false
}
/**
* 圖層建立,surfaceView可見時surface會被建立
* 建立後可以開始繪制surface界面
*
*/
override fun surfaceCreated(holder: SurfaceHolder?) {
holder?.let {
mIsDrawing =true
SinThread().start()
// mCanvas = it.lockCanvas() //lockCanvas鎖定整個畫布,不緩存繪制,同步線程鎖
// mCanvas =it.lockCanvas(Rect(0,0,200,200)) //鎖定指定位置畫布,指定範圍外的畫布不重新繪制(緩存)
// //解除線程鎖,并送出繪制顯示圖像
// it.unlockCanvasAndPost(mCanvas)
}
}
/**
* 構造方法
*/
constructor(context: Context, attributes: AttributeSet?):this(context,attributes,-1)
constructor(context: Context):this(context,null)
init {
//初始化操作
holder.addCallback(this)
isFocusable = true
isFocusableInTouchMode = true
keepScreenOn = true
}
/**
* 重新整理繪制
*/
fun refreshSin(){
SinThread().start()
}
fun cos(){
CosThread().start()
}
/**
* 正弦函數
*/
inner class SinThread :Thread(){
override fun run() {
x =1
y=0
mPaint = Paint()
mPaint?.strokeWidth=12f
mPaint?.color = Color.BLUE
mPath = Path()
while (mIsDrawing) {
try {
mCanvas = holder.lockCanvas()
mCanvas?.drawColor(Color.WHITE)
mCanvas?.drawPath(mPath, mPaint)
} catch (e: Exception) {
e.printStackTrace()
} finally {
if (mCanvas != null) {
holder?.unlockCanvasAndPost(mCanvas)
}
}
x+=1
if (x<=width) {
y = (100*Math.sin(x*2*Math.PI/180)+400).toInt()
mPath?.lineTo(x.toFloat(),y.toFloat())
}else{
break
}
}
}
}
/**
* 餘弦函數
*/
inner class CosThread :Thread(){
override fun run() {
x =1
y=0
mPaint = Paint()
mPaint?.strokeWidth=12f
mPaint?.color = Color.BLUE
mPath = Path()
while (mIsDrawing) {
try {
mCanvas = holder.lockCanvas()
mCanvas?.drawColor(Color.WHITE)
mCanvas?.drawPath(mPath, mPaint)
} catch (e: Exception) {
e.printStackTrace()
} finally {
if (mCanvas != null) {
holder?.unlockCanvasAndPost(mCanvas)
}
}
x+=1
if (x<=width) {
y = (100*Math.cos(x*2*Math.PI/180)+400).toInt()
mPath?.lineTo(x.toFloat(), y.toFloat())
}else{
break
}
}
}
}
}
如上,完成一個被動繪制和開放兩個主動繪制的方法。
- 在xml中使用
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<cn.enjoytoday.shortvideo.test.ui.customsurface.CustomerSurfaceView
android:id="@+id/customerSurfaceView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
android:layout_height="300dp"/>
<LinearLayout
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/customerSurfaceView"
android:layout_marginTop="20dp"
android:padding="10dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/beginDraw"
android:text="開始繪制"
android:onClick="onClick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/cosDraw"
android:text="繪制cos"
android:onClick="onClick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
- 在activity控制
class CustomSurfaceActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom_surface)
}
/**
* 點選事件監聽
*/
fun onClick(view: View){
when(view.id){
R.id.beginDraw -> customerSurfaceView.refreshSin()
R.id.cosDraw -> customerSurfaceView.cos()
}
}
}
測試代碼可見:SurfaceView的使用,歡迎通路我的個人部落格,關注微信公衆号 “音視訊愛好者” 。