引子
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的使用,歡迎訪問我的個人博客,關注微信公衆號 “音視頻愛好者” 。