Camera 相機(jī)詳解(中)

image

前言

  • 在上一篇文章中給小伙伴們介紹了進(jìn)行Camera開發(fā)需要了解的知識(shí)點(diǎn),如果你還沒有看過的話,建議先去看上一篇文章《Android: Camera相機(jī)開發(fā)詳解(上) 》

  • 本篇文章會(huì)帶著小伙伴們一步一步實(shí)現(xiàn)自己的Camera,并在實(shí)現(xiàn)的過程中驗(yàn)證上一篇中所講解的結(jié)論


實(shí)現(xiàn)思路:

  1. 在xml布局中定義一個(gè)SurfaceView,用于預(yù)覽相機(jī)采集的數(shù)據(jù)

  2. 給SurfaceHolder添加回調(diào),在surfaceCreated(holder: SurfaceHolder?)回調(diào)中打開相機(jī)

  3. 成功打開相機(jī)后,設(shè)置相機(jī)參數(shù)。比如:對(duì)焦模式,預(yù)覽大小,照片保存大小等等

  4. 設(shè)置相機(jī)預(yù)覽時(shí)的旋轉(zhuǎn)角度,然后調(diào)用startPreview()開始預(yù)覽

  5. 調(diào)用takePicture方法拍照 或者 是在Camera的預(yù)覽回調(diào)中 保存照片

  6. 對(duì)保存的照片進(jìn)行旋轉(zhuǎn)處理,使其為"自然方向"

  7. 關(guān)閉頁(yè)面,釋放相機(jī)資源

具體實(shí)現(xiàn)步驟:

一丶申請(qǐng)權(quán)限

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

二、在xml布局文件中定義一個(gè)SurfaceView

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

三、創(chuàng)建一個(gè)CameraHelper類

class CameraHelper(activity: Activity, surfaceView: SurfaceView) : Camera.PreviewCallback {
    private var mCamera: Camera? = null                   //Camera對(duì)象
    private lateinit var mParameters: Camera.Parameters   //Camera對(duì)象的參數(shù)
    private var mSurfaceView: SurfaceView = surfaceView   //用于預(yù)覽的SurfaceView對(duì)象
    var mSurfaceHolder: SurfaceHolder                     //SurfaceHolder對(duì)象

    private var mActivity: Activity = activity
    private var mCallBack: CallBack? = null   //自定義的回調(diào)

    var mCameraFacing = Camera.CameraInfo.CAMERA_FACING_BACK  //攝像頭方向
    var mDisplayOrientation: Int = 0    //預(yù)覽旋轉(zhuǎn)的角度

    private var picWidth = 2160        //保存圖片的寬
    private var picHeight = 3840       //保存圖片的高

}

由于對(duì)Camera的操作等代碼比較多,本著各司其職的原則,創(chuàng)建了一個(gè)CameraHelper類來處理Camera相關(guān)的操作,如果放在Activity中對(duì)Camera操作會(huì)使Activity臃腫復(fù)雜

CameraHelper的構(gòu)造方法有兩個(gè),一個(gè)是Activity對(duì)象,一個(gè)是SurfaceView對(duì)象(就是xml文件里定義的SurfaceView)

四、給SurfaceView對(duì)象添加回調(diào)函數(shù),并初始化相機(jī)

    private fun init() {
        mSurfaceHolder.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
            }

            override fun surfaceDestroyed(holder: SurfaceHolder?) {
                releaseCamera() //釋放相機(jī)資源
            }

            override fun surfaceCreated(holder: SurfaceHolder?) { //surface創(chuàng)建
                if (mCamera == null) {
                    openCamera(mCameraFacing)  //打開相機(jī)
                }
                startPreview()  //開始預(yù)覽
            }
        })
    }

    //打開相機(jī)
    private fun openCamera(cameraFacing: Int = Camera.CameraInfo.CAMERA_FACING_BACK): Boolean {
        var supportCameraFacing = supportCameraFacing(cameraFacing)   //判斷手機(jī)是否支持前置/后置攝像頭
        if (supportCameraFacing) {
            try {
                mCamera = Camera.open(cameraFacing)
                initParameters(mCamera!!)          //初始化相機(jī)配置信息
                mCamera?.setPreviewCallback(this)
            } catch (e: Exception) {
                e.printStackTrace()
                toast("打開相機(jī)失敗!")
                return false
            }
        }
        return supportCameraFacing
    }

    //判斷是否支持某個(gè)相機(jī)
    private fun supportCameraFacing(cameraFacing: Int): Boolean {
        var info = Camera.CameraInfo()
        for (i in 0 until Camera.getNumberOfCameras()) {
            Camera.getCameraInfo(i, info)
            if (info.facing == cameraFacing) return true
        }
        return false
    }

在CameraHelper的創(chuàng)建后調(diào)用init()方法。在init()方法中,我們首先對(duì)mSurfaceHolder添加了一個(gè)回調(diào),這個(gè)回調(diào)會(huì)告訴我們SurfaceView中surface的變化(在上一篇上有講解)

在surfaceCreated(holder: SurfaceHolder?) 回調(diào)中打開相機(jī)。因?yàn)橄鄼C(jī)開始預(yù)覽的時(shí)候,如果SurfaceView中的surface還沒有創(chuàng)建,就回拋出異常,所以我們?cè)趕urface創(chuàng)建后再對(duì)相機(jī)進(jìn)行操作

我們調(diào)用相機(jī)的open()方法打開一個(gè)攝像頭,在打開攝像頭之前判斷一下手機(jī)是否支持我們將要打開的攝像頭。

五、配置相機(jī)參數(shù)

    //配置相機(jī)參數(shù)
    private fun initParameters(camera: Camera) {
        try {
            mParameters = camera.parameters
            mParameters.previewFormat = ImageFormat.NV21   //設(shè)置預(yù)覽圖片的格式

            //獲取與指定寬高相等或最接近的尺寸
            //設(shè)置預(yù)覽尺寸
            val bestPreviewSize = getBestSize(mSurfaceView.width, mSurfaceView.height, mParameters.supportedPreviewSizes)
            bestPreviewSize?.let {
                mParameters.setPreviewSize(it.width, it.height)
            }
            //設(shè)置保存圖片尺寸
            val bestPicSize = getBestSize(picWidth, picHeight, mParameters.supportedPictureSizes)
            bestPicSize?.let {
                mParameters.setPictureSize(it.width, it.height)
            }
            //對(duì)焦模式
            if (isSupportFocus(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE))
                mParameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE

            camera.parameters = mParameters
        } catch (e: Exception) {
            e.printStackTrace()
            toast("相機(jī)初始化失敗!")
        }
    }

       //獲取與指定寬高相等或最接近的尺寸
    private fun getBestSize(targetWidth: Int, targetHeight: Int, sizeList: List<Camera.Size>): Camera.Size? {
        var bestSize: Camera.Size? = null
        var targetRatio = (targetHeight.toDouble() / targetWidth)  //目標(biāo)大小的寬高比
        var minDiff = targetRatio

        for (size in sizeList) {
            var supportedRatio = (size.width.toDouble() / size.height)
            log("系統(tǒng)支持的尺寸 : ${size.width} * ${size.height} ,    比例$supportedRatio")
        }

        for (size in sizeList) {
            if (size.width == targetHeight && size.height == targetWidth) {
                bestSize = size
                break
            }
            var supportedRatio = (size.width.toDouble() / size.height)
            if (Math.abs(supportedRatio - targetRatio) < minDiff) {
                minDiff = Math.abs(supportedRatio - targetRatio)
                bestSize = size
            }
        }
        log("目標(biāo)尺寸 :$targetWidth * $targetHeight ,   比例  $targetRatio")
        log("最優(yōu)尺寸 :${bestSize?.height} * ${bestSize?.width}")
        return bestSize
    }

我們對(duì)預(yù)覽大小和保存圖片大小進(jìn)行設(shè)置,在設(shè)置的時(shí)候,我們應(yīng)該獲取到與指定寬高相等或最接近的尺寸,這樣的話才能保證圖片既不變形又能最接近我們指定的大小。

下面是vivo x9的后置攝像頭支持的尺寸:

image
image

六、開始預(yù)覽

   //開始預(yù)覽
    fun startPreview() {
        mCamera?.let {
            it.setPreviewDisplay(mSurfaceHolder)         //設(shè)置相機(jī)預(yù)覽對(duì)象
          //  setCameraDisplayOrientation(mActivity)    //設(shè)置預(yù)覽時(shí)相機(jī)旋轉(zhuǎn)的角度
            it.startPreview()
        }
    }

調(diào)用startPreview()方法開始預(yù)覽,我們先看一下預(yù)覽效果:

image

我們可以看到,畫面并不是"自然方向"而且被拉伸。這個(gè)在上一篇已經(jīng)講解過,下面通過setDisplayOrientation(int degree)方法,使其正常顯示

    //設(shè)置預(yù)覽旋轉(zhuǎn)的角度
    private fun setCameraDisplayOrientation(activity: Activity) {
        var info = Camera.CameraInfo()
        Camera.getCameraInfo(mCameraFacing, info)
        val rotation = activity.windowManager.defaultDisplay.rotation

        var screenDegree = 0
        when (rotation) {
            Surface.ROTATION_0 -> screenDegree = 0
            Surface.ROTATION_90 -> screenDegree = 90
            Surface.ROTATION_180 -> screenDegree = 180
            Surface.ROTATION_270 -> screenDegree = 270
        }

        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            mDisplayOrientation = (info.orientation + screenDegree) % 360
            mDisplayOrientation = (360 - mDisplayOrientation) % 360          // compensate the mirror
        } else {
            mDisplayOrientation = (info.orientation - screenDegree + 360) % 360
        }
        mCamera?.setDisplayOrientation(mDisplayOrientation)

        log("屏幕的旋轉(zhuǎn)角度 : $rotation")
        log("setDisplayOrientation(result) : $mDisplayOrientation")
    }

設(shè)置后預(yù)覽效果如下:

image

上一篇提到的相機(jī)的預(yù)覽方向:

image
image
image

通過日志我們看到,前后攝像頭的預(yù)覽旋轉(zhuǎn)角度都是90
前置攝像頭在進(jìn)行角度旋轉(zhuǎn)之前,圖像會(huì)進(jìn)行一個(gè)水平的鏡像翻轉(zhuǎn),所以前置攝像頭應(yīng)該設(shè)置的旋轉(zhuǎn)角度是 270 - 180 = 90

七、進(jìn)行拍照

拍照的話有兩種方式:

  • 調(diào)用takePicture(ShutterCallback shutter, PictureCallback raw,
    PictureCallback jpeg) 方法

  • 在相機(jī)的預(yù)覽回調(diào)里直接保存

1.調(diào)用takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg) 拍照
    //拍攝照片
    fun takePic() {
        mCamera?.let {
            it.takePicture({}, null, { data, _ ->
                it.startPreview()
                savePic(data)  //保存圖片
            })
        }
    }

   //保存照片
    private fun savePic(data: ByteArray?) {
        thread {
            try {
                val temp = System.currentTimeMillis()
                val picFile = FileUtil.createCameraFile()
                if (picFile != null && data != null) {
                   val rawBitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
                   Okio.buffer(Okio.sink(picFile)).write(BitmapUtils.toByteArray(resultBitmap)).close()
                    runOnUiThread {
                        toast("圖片已保存! ${picFile.absolutePath}")
                        log("圖片已保存! 耗時(shí):${System.currentTimeMillis() - temp}    路徑:  ${picFile.absolutePath}")
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
                runOnUiThread {
                    toast("保存圖片失??!")
                }
            }
        }
    }

takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback jpeg)方法有3個(gè)參數(shù),而且這3個(gè)參數(shù)都是抽象接口:

  • 第一個(gè)是點(diǎn)擊拍照時(shí)的回調(diào)。
    如果傳null,則沒有任何效果
    如果寫一個(gè)空實(shí)現(xiàn),則在點(diǎn)擊拍照時(shí)會(huì)有"咔擦"聲

  • 第二個(gè)和第三個(gè)參數(shù)類型一樣,PictureCallback 有一個(gè)抽象方法
    void onPictureTaken(byte[] data, Camera camera)
    data就是點(diǎn)擊拍照后相機(jī)返回的照片的byte數(shù)組,用該數(shù)組創(chuàng)建一個(gè)bitmap保存下來,就得到了拍攝的照片

2.在相機(jī)的預(yù)覽回調(diào)里直接保存
override fun onPreviewFrame(data: ByteArray?, camera: Camera?) {
    savePic(data)   //保存照片   
}

注意:實(shí)際上這個(gè)回調(diào)方法會(huì)一直一直的調(diào)用,如果要保存一張照片的話應(yīng)該加個(gè)字段進(jìn)行控制,此處只是做演示

在保存圖片的時(shí)候,我們需要開啟一個(gè)子線程來進(jìn)行操作,通過日志輸出可以看到保存圖片所用時(shí)間和保存路徑:

image

八、調(diào)整保存照片的方向

與預(yù)覽時(shí)方向類似,照片在保存時(shí)也有一個(gè)方向。我們先看一下在上一步中保存的照片是什么樣的:

后置攝像頭:

image

前置攝像頭:

image

下面我們?cè)诒4鎴D片的時(shí)候,對(duì)照片進(jìn)行旋轉(zhuǎn)處理,保存照片的方法應(yīng)該如下:

   private fun savePic(data: ByteArray?) {
        thread {
            try {
                val temp = System.currentTimeMillis()
                val picFile = FileUtil.createCameraFile()
                if (picFile != null && data != null) {
                    val rawBitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
                    val resultBitmap = if (mCameraHelper.mCameraFacing == Camera.CameraInfo.CAMERA_FACING_FRONT)
                       BitmapUtils.rotate(rawBitmap, 270f)  //前置攝像頭旋轉(zhuǎn)270°
                    else
                        BitmapUtils.rotate(rawBitmap, 90f)  //后置攝像頭旋轉(zhuǎn)90°

                    Okio.buffer(Okio.sink(picFile)).write(BitmapUtils.toByteArray(resultBitmap)).close()
                    runOnUiThread {
                        toast("圖片已保存! ${picFile.absolutePath}")
                        log("圖片已保存! 耗時(shí):${System.currentTimeMillis() - temp}    路徑:  ${picFile.absolutePath}")
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
                runOnUiThread {
                    toast("保存圖片失敗!")
                }
            }
        }
    }

//圖片工具類
object BitmapUtils {
    //水平鏡像翻轉(zhuǎn)
    fun mirror(rawBitmap: Bitmap): Bitmap {
        var matrix = Matrix()
        matrix.postScale(-1f, 1f)
        return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true)
    }
    //旋轉(zhuǎn)
    fun rotate(rawBitmap: Bitmap, degree: Float): Bitmap {
        var matrix = Matrix()
        matrix.postRotate(degree)
        return Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true)
    }
|

然后我們?cè)谶M(jìn)行一次拍照:

后置攝像頭:

image

前置攝像頭:

image

對(duì)比一下上一篇文章所講的相機(jī)保存照片的方向:

image

關(guān)于前置攝像頭所拍攝照片,需要注意的是,由于在setDisplayOrientation()設(shè)置相機(jī)預(yù)覽方向的時(shí)候系統(tǒng)默認(rèn)做了一個(gè)水平鏡面的翻轉(zhuǎn),所以我們通過前置攝像頭保存來的照片并不是和預(yù)覽時(shí)看到的一樣,兩者是水平鏡像關(guān)系。所以,一般情況下我們不僅僅需要對(duì)前置攝像頭做旋轉(zhuǎn),還應(yīng)該做一個(gè)水平方向的鏡面翻轉(zhuǎn)處理。

在上面保存圖片的方法中判斷如果是前置攝像頭的話,代碼修改如下:

BitmapUtils.mirror(BitmapUtils.rotate(rawBitmap, 270f)) //旋轉(zhuǎn)270,然后水平鏡面翻轉(zhuǎn)

這樣的話,就能保證所拍攝照片與在預(yù)覽時(shí)所呈現(xiàn)的畫面是一模一樣的,如下圖:

image

注:如果有小伙伴對(duì)這點(diǎn)還不太理解的話,墻裂建議自己用前置攝像頭自拍一張,然后在對(duì)比保存的照片與預(yù)覽時(shí)手機(jī)里顯示的畫面,就很容易理解了
不是我不愿意自己自拍來給小伙們演示,長(zhǎng)相實(shí)在是有點(diǎn)慘,所以大家還是自己親自驗(yàn)證吧o(╥﹏╥)o

九、釋放相機(jī)資源

在Activity銷毀前或者是關(guān)閉相機(jī)時(shí),應(yīng)當(dāng)釋放當(dāng)前相機(jī)資源

   //釋放相機(jī)
    fun releaseCamera() {
        if (mCamera != null) {
            mCamera?.stopPreview()
            mCamera?.setPreviewCallback(null)
            mCamera?.release()
            mCamera = null
        }
    }

完整效果如下:

image

總結(jié)

本篇文章主要給小伙伴們介紹了實(shí)現(xiàn)Camera拍照功能的流程及步驟,并且用實(shí)際效果驗(yàn)證了上一篇文章中所講解的理論
下一篇文章將會(huì)給小伙伴們介紹如何實(shí)現(xiàn)人臉檢測(cè)功能,敬請(qǐng)期待~~

作者:Smashing丶
鏈接:http://www.itdecent.cn/p/e20a2ad6ad9a
來源:簡(jiǎn)書
簡(jiǎn)書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容