1. 程式人生 > >開發Android Camera—使用Kotlin語言,完成第一個自定義相機

開發Android Camera—使用Kotlin語言,完成第一個自定義相機

對於首次使用Kotlin語言開發,在網上苦於尋找不到Kotlin語言編寫的相機程式碼,故寫下這篇部落格。
好了,咱們進入主題

在Android 5.0(SDK 21)中,Google使用Camera2替代了Camera介面。Camera2在介面和架構上做了巨大的變動,但是基於眾所周知的原因,我們還必須基於 Android 4.+ 系統進行開發。本文介紹的是Camera介面開發及其使用方法,通過本文章,你將全面地學會Camera介面的開發流程。

開發步驟

  • 新增相機相關許可權
  • 通過Camera.open(int)獲得一個相機例項,也可以使用預設的Camera.open()
  • 利用camera.parameters得到相機例項的預設設定Camera.parameters
  • 如果需要的話,修改Camera.parameters並呼叫camera.parameters=Camera.Parameters來修改相機設定
  • 呼叫camera.setDisplayOrientation(int)來設定正確的預覽方向
  • 呼叫camera.setPreviewDisplay(SurfaceHolder)來設定預覽,如果沒有這一步,相機是無法開始預覽的
  • 呼叫camera.startPreview()來開啟預覽,對於拍照,這一步是必須的
  • 在需要的時候呼叫camera.takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)來拍攝照片
  • 拍攝照片後,相機會停止預覽,如果需要再次拍攝多張照片,需要再次呼叫camera.startPreview()來重新開始預覽
  • 呼叫camera.stopPreview()來停止預覽
  • 一定要在onPause()的時候呼叫camera.release()來釋放camera,在onResume中重新開始camera

相機許可權

在使用相機進行拍照,需要新增以下許可權:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera"
/> <uses-feature android:name="android.hardware.camera.autofocus" />

拍攝的照片需要儲存到記憶體卡的話,還需要記憶體卡讀寫的許可權:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

拍照注意事項

根據拍照步驟就可以使用相機進行拍照,但是在使用過程中,有諸多需要注意的地方。

開啟相機

開啟相機直接呼叫Camera.open(),理論上就可以得到一個相機例項。但是在開啟相機前,最好check一下。雖然目前基本上所有的手機都是前後攝像頭,但是也不排斥有些奇葩的手機,只有後攝像頭,或者乾脆沒有攝像頭的。所有在open一個camera前,先檢查是否存在該id的camera。Camera.getNumberOfCameras()可以獲得當前裝置的Camera的個數N,N為0表示不支援攝像頭,否則對應的Camera的ID就是0—(N-1)。

檢查是否支援相機:

    private fun isCameraAvailable(): Boolean {
        val numberOfCameras = Camera.getNumberOfCameras()
        if (numberOfCameras != 0) {
            return true
        }
        return false
    }

開啟相機:

   private fun getCamera(): Camera? {
        var camera: Camera? = null
        try {
            camera = Camera.open()
        } catch (e: Exception) {
            e.printStackTrace()
        }

        return camera
    }

相機設定

Camera的Parameters提供了諸多屬性,可以對相機進行多種設定,包括預覽大小及格式、照片大小及格式、預覽頻率、拍攝場景、顏色效果、對焦方式等等,具體設定可參考官方手冊。
拍照尤其需要注意的是對預覽大小、照片大小以及對焦方式的設定。
在對預覽大小、照片大小及對焦方式設定時,設定的值必須是當前裝置所支援的。否則,預覽大小和照片大小設定會無效,對焦方式設定會導致崩潰。它們都有相應的方法,獲取相應的支援的列表。對應的依次為getSupportedPictureSizes(),getSupportedPictureSizes(),getSupportedFocusModes()。

相機設定程式碼:

private fun setParameters(camera: Camera?) {
        camera?.let {
            val params: Camera.Parameters = camera.parameters
            params.pictureFormat = ImageFormat.JPEG
            val size = Collections.max(params.supportedPictureSizes, object : Comparator<Camera.Size> {
                override fun compare(lhs: Camera.Size, rhs: Camera.Size): Int {
                    return lhs.width * lhs.height - rhs.width * rhs.height
                }
            })
            params.setPreviewSize(size.width, size.height);  
            params.focusMode = Camera.Parameters.FOCUS_MODE_AUTO
            try {
                camera.parameters = params;    //在設定屬性時,如果遇到未支援的大小時將會直接報錯,故需要捕捉一樣,做異常處理
            } catch (e: Exception) {
                e.printStackTrace()
                try {
                    //遇到上面所說的情況,只能設定一個最小的預覽尺寸
                    params.setPreviewSize(1920, 1080);
                    camera.parameters = params;
                } catch (e1: Exception) {
                    //到這裡還有問題,就是拍照尺寸的鍋了,同樣只能設定一個最小的拍照尺寸
                    e1.printStackTrace();
                    try {
                        params.setPictureSize(1920, 1080);
                        camera.parameters = params;
                    } catch (ignored: Exception) {}}}
              }
    }

相機預覽方向和預覽View的設定

不對相機預覽方向和應用方向設定,通常情況下得到的預覽結果是無法接受的。一般應用設定的方向為固定豎向,預覽設定旋轉90度即可。嚴謹點來說,預覽方向的設定是根據當前window的rotation來設定的,即((WindowManager)displayView.getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation()的值。在Surface.ROTATION_0和Surface.ROTATION_180時,Camera設定displayOrientation為90,否則設定為0。
相機預覽前,必須呼叫camera.setPreviewDisplay(SurfaceHolder)來設定預覽的承載。SurfaceHolder一般取自SurfaceView、SurfaceTexture、TextureView等。一般情況下,如果不對顯示的View大小做合理的設定,預覽中的場景都會被變形。

新增SurfaceHolder的Callback監聽

 val holder = sv_camera.holder
        holder?.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
            }

            override fun surfaceDestroyed(holder: SurfaceHolder?) {
            }

            override fun surfaceCreated(holder: SurfaceHolder?) {
                setStartPreview(mCamera, holder);
            }
        })

設定預覽的方向:

  private fun setStartPreview(camera: Camera?, holder: SurfaceHolder?) {
        camera?.let {
            try {
                camera.setPreviewDisplay(holder)
                camera.setDisplayOrientation(Surface.ROTATION_90)
                camera.startPreview()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }

拍照監聽及圖片處理

相機拍照時在預覽時,呼叫takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)(或其過載方法)來實現拍照的,其中第一個引數表示影象捕獲時刻的回撥。可以看到此方法的後三個引數型別是一樣的,都是影象回撥。分別表示原始圖資料回撥、展示影象資料的回撥、JPEG影象資料的回撥。影象回撥中得到byte陣列decode為image後,圖片的方向都是攝像頭原始的影象方向。
可以通過parameters.setRotation(int)來改變最後一個回撥中影象資料的方向。個人推薦不設定,直接在回撥中利用矩陣變換統一處理。因為利用parameters.setRotation(int)來旋轉影象,在不同手機上會有差異。
與預覽View設定類似,pictureSize設定的值,影響了最後的拍照結果,處理時需要對拍照的結果進行裁剪,使圖片結果和在可視區域預覽的結果相同。前攝像頭拍攝的結果還需要做對稱變換,以保證“所見即所得”。

拍照監聽:

    private fun taskPicture() {
        mCamera?.autoFocus { success, camera ->
            run {
                mCamera?.takePicture(null, null, pictureCallback);
            }
        }
    }

圖片處理:

 val pictureCallback = Camera.PictureCallback { data, camera ->
        val pictureFile = File(Environment.getExternalStorageDirectory(), "gaozhongkui-" + System.currentTimeMillis() + ".jpg")
        try {
            val fos = FileOutputStream(pictureFile)
            fos.write(data)
            fos.close();
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
    }

以下是完成的程式碼例項:

xml佈局:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <SurfaceView
        android:id="@+id/sv_camera"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        android:id="@+id/btn_capture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="拍照" />
</FrameLayout>

程式碼邏輯:

import android.graphics.ImageFormat
import android.hardware.Camera
import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity
import android.view.Surface
import android.view.SurfaceHolder
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.*
import kotlin.Comparator

class MainActivity : AppCompatActivity() {

    var mCamera: Camera? = null
    val pictureCallback = Camera.PictureCallback { data, camera ->
        val pictureFile = File(Environment.getExternalStorageDirectory(), "gaozhongkui-" + System.currentTimeMillis() + ".jpg")
        try {
            val fos = FileOutputStream(pictureFile)
            fos.write(data)
            fos.close();
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (isCameraAvailable()) {
            mCamera = getCamera()
            setParameters(mCamera);
        }
        initViewListener();

    }

    private fun  initViewListener(){
        val holder = sv_camera.holder
        holder?.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
            }

            override fun surfaceDestroyed(holder: SurfaceHolder?) {
            }

            override fun surfaceCreated(holder: SurfaceHolder?) {

                setStartPreview(mCamera, holder);
            }

        })
        btn_capture.setOnClickListener {
            taskPicture()
        }
    }

    private fun isCameraAvailable(): Boolean {
        val numberOfCameras = Camera.getNumberOfCameras()
        if (numberOfCameras != 0) {
            return true
        }
        return false
    }


    private fun getCamera(): Camera? {
        var camera: Camera? = null
        try {
            camera = Camera.open()
        } catch (e: Exception) {
            e.printStackTrace()
        }

        return camera
    }

    fun releaseCamera() {
        mCamera?.setPreviewCallback(null)
        mCamera?.stopPreview()
        mCamera?.release()
        mCamera = null
    }


    private fun setStartPreview(camera: Camera?, holder: SurfaceHolder?) {
        camera?.let {
            try {
                camera.setPreviewDisplay(holder)
                camera.setDisplayOrientation(Surface.ROTATION_90)
                camera.startPreview()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }

    private fun setParameters(camera: Camera?) {
        camera?.let {
            val params: Camera.Parameters = camera.parameters
            params.pictureFormat = ImageFormat.JPEG

            val size = Collections.max(params.supportedPictureSizes, object : Comparator<Camera.Size> {
                override fun compare(lhs: Camera.Size, rhs: Camera.Size): Int {
                    return lhs.width * lhs.height - rhs.width * rhs.height
                }
            })
            params.setPreviewSize(size.width, size.height);

            params.focusMode = Camera.Parameters.FOCUS_MODE_AUTO
            try {
                camera.parameters = params;
            } catch (e: Exception) {
                e.printStackTrace()
                try {
                    //遇到上面所說的情況,只能設定一個最小的預覽尺寸
                    params.setPreviewSize(1920, 1080);
                    camera.parameters = params;
                } catch (e1: Exception) {
                    //到這裡還有問題,就是拍照尺寸的鍋了,同樣只能設定一個最小的拍照尺寸
                    e1.printStackTrace();
                    try {
                        params.setPictureSize(1920, 1080);
                        camera.parameters = params;
                    } catch (ignored: Exception) {
                    }
                }

            }
        }
    }

    private fun taskPicture() {
        mCamera?.autoFocus { success, camera ->
            run {
                mCamera?.takePicture(null, null, pictureCallback);
            }
        }
    }
}