Andorid下的控件畫布-SurfaceView

引子

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的使用

  1. 創建一個自定義的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
                 }

             }


         }
     }
   }

如上,完成一個被動繪製和開放兩個主動繪製的方法。

  1. 在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>

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章