天天看點

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的使用,歡迎通路我的個人部落格,關注微信公衆号 “音視訊愛好者” 。