Android Camera2 教程 · 第四章 · 拍照

Android Camera

上一章《Camera2 預(yù)覽》我們學(xué)習(xí)了如何配置預(yù)覽呼渣,接下來我們來學(xué)習(xí)如何拍照媒楼。

閱讀完本章,你將會學(xué)到以下幾個知識點:

  1. 理解 Capture 工作流程
  2. 如何拍攝單張照片
  3. 如何連續(xù)拍攝多張照片
  4. 如何連拍照片
  5. 如何配置縮略圖尺寸
  6. 如何播放快門音效
  7. 如何矯正圖片方向
  8. 如何切換前后置攝像頭

你可以在 https://github.com/darylgo/Camera2Sample 下載相關(guān)的源碼镇眷,并且切換到 Tutorial4 標(biāo)簽下吧史。

1 理解 Capture 工作流程

在正式介紹如何拍照之前,我們有必要深入理解幾種不同模式的 Capture 的工作流程蕴轨,只要理解它們的工作流程就很容易掌握各種拍照模式的實現(xiàn)原理港谊,在第一章《Camera2 概覽》 里我們介紹了 Capture 有以下幾種不同模式:

  • 單次模式(One-shot):指的是只執(zhí)行一次的 Capture 操作,例如設(shè)置閃光燈模式橙弱、對焦模式和拍一張照片等歧寺。多個單次模式的 Capture 會進入隊列按順序執(zhí)行。

  • 多次模式(Burst):指的是連續(xù)多次執(zhí)行指定的 Capture 操作棘脐,該模式和多次執(zhí)行單次模式的最大區(qū)別是連續(xù)多次 Capture 期間不允許插入其他任何 Capture 操作斜筐,例如連續(xù)拍攝 100 張照片,在拍攝這 100 張照片期間任何新的 Capture 請求都會排隊等待蛀缝,直到拍完 100 張照片顷链。多組多次模式的 Capture 會進入隊列按順序執(zhí)行。

  • 重復(fù)模式(Repeating):指的是不斷重復(fù)執(zhí)行指定的 Capture 操作屈梁,當(dāng)有其他模式的 Capture 提交時會暫停該模式嗤练,轉(zhuǎn)而執(zhí)行其他被模式的 Capture,當(dāng)其他模式的 Capture 執(zhí)行完畢后又會自動恢復(fù)繼續(xù)執(zhí)行該模式的 Capture在讶,例如顯示預(yù)覽畫面就是不斷 Capture 獲取每一幀畫面煞抬。該模式的 Capture 是全局唯一的,也就是新提交的重復(fù)模式 Capture 會覆蓋舊的重復(fù)模式 Capture构哺。

我們舉個例子來進一步說明上面三種模式革答,假設(shè)我們的相機應(yīng)用程序開啟了預(yù)覽,所以會提交一個重復(fù)模式的 Capture 用于不斷獲取預(yù)覽畫面,然后我們提交一個單次模式的 Capture残拐,接著我們又提交了一組連續(xù)三次的多次模式的 Capture途茫,這些不同模式的 Capture 會按照下圖所示被執(zhí)行:

Capture 工作原理

下面是幾個重要的注意事項:

  1. 無論 Capture 以何種模式被提交,它們都是按順序串行執(zhí)行的蹦骑,不存在并行執(zhí)行的情況慈省。

  2. 重復(fù)模式是一個比較特殊的模式,因為它會保留我們提交的 CaptureRequest 對象用于不斷重復(fù)執(zhí)行 Capture 操作眠菇,所以大多數(shù)情況下重復(fù)模式的 CaptureRequest 和其他模式的 CaptureRequest 是獨立的边败,這就會導(dǎo)致重復(fù)模式的參數(shù)和其他模式的參數(shù)會有一定的差異,例如重復(fù)模式不會配置 CaptureRequest.AF_TRIGGER_START捎废,因為這會導(dǎo)致相機不斷觸發(fā)對焦的操作笑窜。

  3. 如果某一次的 Capture 沒有配置預(yù)覽的 Surface,例如拍照的時候登疗,就會導(dǎo)致本次 Capture 不會將畫面輸出到預(yù)覽的 Surface 上排截,進而導(dǎo)致預(yù)覽畫面卡頓的情況,所以大部分情況下我們都會將預(yù)覽的 Surface 添加到所有的 CaptureRequest 里辐益。

2 如何拍攝單張照片

拍攝單張照片是最簡單的拍照模式断傲,它使用的就是單次模式的 Capture,我們會使用 ImageReader 創(chuàng)建一個接收照片的 Surface智政,并且把它添加到 CaptureRequest 里提交給相機進行拍照认罩,最后通過 ImageReader 的回調(diào)獲取 Image 對象,進而獲取 JPEG 圖像數(shù)據(jù)進行保存续捂。

2.1 定義回調(diào)接口

當(dāng)拍照完成的時候我們會得到兩個數(shù)據(jù)對象垦垂,一個是通過 onImageAvailable() 回調(diào)給我們的存儲圖像數(shù)據(jù)的 Image,一個是通過 onCaptureCompleted() 回調(diào)給我們的存儲拍照信息的 CaptureResult牙瓢,它們是一一對應(yīng)的劫拗,所以我們定義了如下兩個回調(diào)接口:

private val captureResults: BlockingQueue<CaptureResult> = LinkedBlockingDeque()

private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() {
    @MainThread
    override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
        super.onCaptureCompleted(session, request, result)
        captureResults.put(result)
    }
}
private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener {
    @WorkerThread
    override fun onImageAvailable(imageReader: ImageReader) {
        val image = imageReader.acquireNextImage()
        val captureResult = captureResults.take()
        if (image != null && captureResult != null) {
            // Save image into sdcard.
        }
    }
}

2.2 創(chuàng)建 ImageReader

創(chuàng)建 ImageReader 需要我們指定照片的大小,所以首先我們要獲取支持的照片尺寸列表矾克,并且從中篩選出合適的尺寸页慷,假設(shè)我們要求照片的尺寸最大不能超過 4032x3024,并且比例必須是 4:3胁附,所以會有如下篩選尺寸的代碼片段:

@WorkerThread
private fun getOptimalSize(cameraCharacteristics: CameraCharacteristics, clazz: Class<*>, maxWidth: Int, maxHeight: Int): Size? {
    val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    val supportedSizes = streamConfigurationMap?.getOutputSizes(clazz)
    return getOptimalSize(supportedSizes, maxWidth, maxHeight)
}

@AnyThread
private fun getOptimalSize(supportedSizes: Array<Size>?, maxWidth: Int, maxHeight: Int): Size? {
    val aspectRatio = maxWidth.toFloat() / maxHeight
    if (supportedSizes != null) {
        for (size in supportedSizes) {
            if (size.width.toFloat() / size.height == aspectRatio && size.height <= maxHeight && size.width <= maxWidth) {
                return size
            }
        }
    }
    return null
}

接著我們就可以篩選出合適的尺寸差购,然后創(chuàng)建一個圖像格式是 JPEG 的 ImageReader 對象,并且獲取它的 Surface:

val imageSize = getOptimalSize(cameraCharacteristics, ImageReader::class.java, maxWidth, maxHeight)!!
jpegImageReader = ImageReader.newInstance(imageSize.width, imageSize.height, ImageFormat.JPEG, 5)
jpegImageReader?.setOnImageAvailableListener(OnJpegImageAvailableListener(), cameraHandler)
jpegSurface = jpegImageReader?.surface

2.3 創(chuàng)建 CaptureRequest

接下來我們使用 TEMPLATE_STILL_CAPTURE 模板創(chuàng)建一個用于拍照的 CaptureRequest.Builder 對象汉嗽,并且添加拍照的 Surface 和預(yù)覽的 Surface 到其中:

captureImageRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureImageRequestBuilder.addTarget(previewDataSurface)
captureImageRequestBuilder.addTarget(jpegSurface)

你可能會疑問為什么拍照用的 CaptureRequest 對象需要添加預(yù)覽的 Surface,這一點我們在前面有解釋過了找蜜,如果某一次的 Capture 沒有配置預(yù)覽的 Surface饼暑,例如拍照的時候,就會導(dǎo)致本次 Capture 不會將畫面輸出到預(yù)覽的 Surface 上,進而導(dǎo)致預(yù)覽畫面卡頓的情況弓叛,所以大部分情況下我們都會將預(yù)覽的 Surface 添加到所有的 CaptureRequest 里彰居。

2.4 矯正 JPEG 圖片方向

《Camera2 預(yù)覽》 里我們介紹了一些方向的概念,也提到了攝像頭傳感器的方向很多時候都不是 0°撰筷,這就會導(dǎo)致我們拍出來的照片方向是錯誤的陈惰,例如手機攝像頭傳感器方向是 90° 的時候,垂直拿著手機拍出來的照片很可能是橫著的:

在進行圖片方向矯正的時候毕籽,我們的目的是做到所見即所得抬闯,也就是用戶在預(yù)覽畫面里看到的是什么樣,輸出的圖片就是什么樣关筒。為了做到圖片所見即所得溶握,我們要同時考慮設(shè)備方向和攝像頭傳感器方向,下面是一段來自官方的圖片矯正代碼:

private fun getJpegOrientation(cameraCharacteristics: CameraCharacteristics, deviceOrientation: Int): Int {
    var myDeviceOrientation = deviceOrientation
    if (myDeviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) {
        return 0
    }
    val sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!

    // Round device orientation to a multiple of 90
    myDeviceOrientation = (myDeviceOrientation + 45) / 90 * 90

    // Reverse device orientation for front-facing cameras
    val facingFront = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
    if (facingFront) {
        myDeviceOrientation = -myDeviceOrientation
    }

    // Calculate desired JPEG orientation relative to camera orientation to make
    // the image upright relative to the device orientation
    return (sensorOrientation + myDeviceOrientation + 360) % 360
}

如果你已經(jīng)理解 《Camera2 預(yù)覽》 里我們介紹的一些方向概念蒸播,那么上面這段代碼其實就很容易理解睡榆,唯一特別的地方是前置攝像頭輸出的畫面底層默認做了鏡像的翻轉(zhuǎn)才能保證我們在預(yù)覽的時候看到的畫面就想照鏡子一樣,所以前置攝像頭給的 SENSOR_ORIENTATION 值也是經(jīng)過鏡像的袍榆,但是相機在輸出 JPEG 的時候并沒有進行鏡像操作胀屿,所以在計算 JPEG 矯正角度的時候要對這個默認鏡像的操作進行逆向鏡像。

計算出圖片的矯正角度后包雀,我們要通過 CaptureRequest.JPEG_ORIENTATION 配置這個角度宿崭,相機在拍照輸出 JPEG 圖像的時候會參考這個角度值從以下兩種方式選一種進行圖像方向矯正:

  1. 直接對圖像進行旋轉(zhuǎn),并且將 Exif 的 ORIENTATION 標(biāo)簽賦值為 0馏艾。
  2. 不對圖像進行旋轉(zhuǎn)劳曹,而是將旋轉(zhuǎn)信息寫入 Exif 的 ORIENTATION 標(biāo)簽里。

客戶端在顯示圖片的時候一定要去檢查 Exif 的ORIENTATION 標(biāo)簽的值琅摩,并且根據(jù)這個值對圖片進行對應(yīng)角度的旋轉(zhuǎn)才能保證圖片顯示方向是正確的铁孵。

val deviceOrientation = deviceOrientationListener.orientation
val jpegOrientation = getJpegOrientation(cameraCharacteristics, deviceOrientation)
captureImageRequestBuilder[CaptureRequest.JPEG_ORIENTATION] = jpegOrientation

2.5 設(shè)置縮略圖尺寸

相機在輸出 JPEG 圖片的時候,同時會根據(jù)我們通過 CaptureRequest.JPEG_THUMBNAIL_SZIE 配置的縮略圖尺寸生成一張縮略圖寫入圖片的 Exif 信息里房资。在設(shè)置縮略圖尺寸之前蜕劝,我們首先要獲取相機支持哪些縮略圖尺寸,與獲取預(yù)覽尺寸或照片尺寸列表方式不一樣的是轰异,縮略圖尺寸列表是直接通過 CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES 獲取的岖沛。配置縮略圖尺寸的代碼如下所示:

val availableThumbnailSizes = cameraCharacteristics[CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES]
val thumbnailSize = getOptimalSize(availableThumbnailSizes, maxWidth, maxHeight)

在獲取圖片縮略圖的時候,我們不能總是假設(shè)圖片一定會在 Exif 寫入縮略圖搭独,當(dāng) Exif 里面沒有縮略圖數(shù)據(jù)的時候婴削,我們要轉(zhuǎn)而直接 Decode 原圖獲取縮略圖,另外無論是原圖還是縮略圖牙肝,都要根據(jù) Exif 的 ORIENTATION 角度進行角度矯正才能正確顯示唉俗,下面是我們 Demo 中獲取圖片縮略圖的代碼:

@WorkerThread
private fun getThumbnail(jpegPath: String): Bitmap? {
    val exifInterface = ExifInterface(jpegPath)
    val orientationFlag = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
    val orientation = when (orientationFlag) {
        ExifInterface.ORIENTATION_NORMAL -> 0.0F
        ExifInterface.ORIENTATION_ROTATE_90 -> 90.0F
        ExifInterface.ORIENTATION_ROTATE_180 -> 180.0F
        ExifInterface.ORIENTATION_ROTATE_270 -> 270.0F
        else -> 0.0F
    }

    var thumbnail = if (exifInterface.hasThumbnail()) {
        exifInterface.thumbnailBitmap
    } else {
        val options = BitmapFactory.Options()
        options.inSampleSize = 16
        BitmapFactory.decodeFile(jpegPath, options)
    }

    if (orientation != 0.0F && thumbnail != null) {
        val matrix = Matrix()
        matrix.setRotate(orientation)
        thumbnail = Bitmap.createBitmap(thumbnail, 0, 0, thumbnail.width, thumbnail.height, matrix, true)
    }

    return thumbnail
}

2.6 設(shè)置定位信息

拍照的時候嗤朴,通常都會在圖片的 Exif 寫入定位信息,我們可以通過 CaptureRequest.JPEG_GPS_LOCATION 配置定位信息虫溜,代碼如下:

@WorkerThread
private fun getLocation(): Location? {
    val locationManager = getSystemService(LocationManager::class.java)
    if (locationManager != null && ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
        return locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)
    }
    return null
}
val location = getLocation()
captureImageRequestBuilder[CaptureRequest.JPEG_GPS_LOCATION] = location

2.7 播放快門音效

在進行拍照之前雹姊,我們還需要配置拍照時播放的快門音效,因為 Camera2 和 Camera1 不一樣衡楞,拍照時不會有任何聲音吱雏,需要我們在適當(dāng)?shù)臅r候通過 MediaSoundPlayer 播放快門音效,通常情況我們是在 CaptureStateCallback.onCaptureStarted() 回調(diào)的時候播放快門音效:

private val mediaActionSound: MediaActionSound = MediaActionSound()

private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() {

    @MainThread
    override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
        super.onCaptureStarted(session, request, timestamp, frameNumber)
        // Play the shutter click sound.
        cameraHandler?.post { mediaActionSound.play(MediaActionSound.SHUTTER_CLICK) }
    }

    @MainThread
    override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
        super.onCaptureCompleted(session, request, result)
        captureResults.put(result)
    }
}

2.8 拍照并保存圖片

經(jīng)過一連串的配置之后瘾境,我們終于可以開拍照了歧杏,直接調(diào)用 CameraCaptureSession.capture() 方法把 CaptureRequest 對象提交給相機就可以等待相機輸出圖片了,該方法要求我們設(shè)置三個參數(shù):

  • request:本次 Capture 操作使用的 CaptureRequest 對象寄雀。
  • listener:監(jiān)聽 Capture 狀態(tài)的回調(diào)接口得滤。
  • handler:回調(diào) Capture 狀態(tài)監(jiān)聽接口的 Handler 對象。
captureSession.capture(captureImageRequest, CaptureImageStateCallback(), mainHandler)

如果一切順利盒犹,相機在拍照完成的時候會通過 CaptureStateCallback.onCaptureCompleted() 回調(diào)一個 CaptureResult 對象給我們懂更,里面包含了本次拍照的所有信息,另外還會通過 OnImageAvailableListener.onImageAvailable() 回調(diào)一個代表圖像數(shù)據(jù)的 Image 對象給我們急膀。在我們的 Demo 中沮协,我們將獲取到的 CaptureResult 對象保存到一個阻塞隊列中,在 OnImageAvailableListener.onImageAvailable() 回調(diào)的時候就從這個阻塞隊列獲取 CaptureResult 對象卓嫂,結(jié)合 Image 對象對圖片進行保存操作慷暂,并且還會在圖片保存完畢的時候獲取圖片的縮略圖用于刷新 UI,代碼如下所示:

private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener {

    private val dateFormat: DateFormat = SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.getDefault())
    private val cameraDir: String = "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)}/Camera"

    @WorkerThread
    override fun onImageAvailable(imageReader: ImageReader) {
        val image = imageReader.acquireNextImage()
        val captureResult = captureResults.take()
        if (image != null && captureResult != null) {
            image.use {
                val jpegByteBuffer = it.planes[0].buffer// Jpeg image data only occupy the planes[0].
                val jpegByteArray = ByteArray(jpegByteBuffer.remaining())
                jpegByteBuffer.get(jpegByteArray)
                val width = it.width
                val height = it.height
                saveImageExecutor.execute {
                    val date = System.currentTimeMillis()
                    val title = "IMG_${dateFormat.format(date)}"http:// e.g. IMG_20190211100833786
                    val displayName = "$title.jpeg"http:// e.g. IMG_20190211100833786.jpeg
                    val path = "$cameraDir/$displayName"http:// e.g. /sdcard/DCIM/Camera/IMG_20190211100833786.jpeg
                    val orientation = captureResult[CaptureResult.JPEG_ORIENTATION]
                    val location = captureResult[CaptureResult.JPEG_GPS_LOCATION]
                    val longitude = location?.longitude ?: 0.0
                    val latitude = location?.latitude ?: 0.0

                    // Write the jpeg data into the specified file.
                    File(path).writeBytes(jpegByteArray)

                    // Insert the image information into the media store.
                    val values = ContentValues()
                    values.put(MediaStore.Images.ImageColumns.TITLE, title)
                    values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, displayName)
                    values.put(MediaStore.Images.ImageColumns.DATA, path)
                    values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, date)
                    values.put(MediaStore.Images.ImageColumns.WIDTH, width)
                    values.put(MediaStore.Images.ImageColumns.HEIGHT, height)
                    values.put(MediaStore.Images.ImageColumns.ORIENTATION, orientation)
                    values.put(MediaStore.Images.ImageColumns.LONGITUDE, longitude)
                    values.put(MediaStore.Images.ImageColumns.LATITUDE, latitude)
                    contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)

                    // Refresh the thumbnail of image.
                    val thumbnail = getThumbnail(path)
                    if (thumbnail != null) {
                        runOnUiThread {
                            thumbnailView.setImageBitmap(thumbnail)
                            thumbnailView.scaleX = 0.8F
                            thumbnailView.scaleY = 0.8F
                            thumbnailView.animate().setDuration(50).scaleX(1.0F).scaleY(1.0F).start()
                        }
                    }
                }
            }
        }
    }
}

2.9 前置攝像頭拍照的鏡像問題

如果你使用前置攝像頭進行拍照晨雳,雖然照片的方向已經(jīng)被我們矯正了行瑞,但是你會發(fā)現(xiàn)畫面卻是相反的,例如你在預(yù)覽的時候人臉在左邊餐禁,拍出來的照片人臉卻是在右邊血久。出現(xiàn)這個問題的原因是默認情況下相機不會對 JPEG 圖像進行鏡像操作,導(dǎo)致輸出的原始畫面是非鏡像的帮非。解決這個問題的一個辦法是拿到 JPEG 數(shù)據(jù)之后再次對圖像進行鏡像操作氧吐,然后才保存圖片。

3 如何連續(xù)拍攝多張圖片

在我們的 Demo 中有一個特殊的拍照功能末盔,就是當(dāng)用戶雙擊快門按鈕的時候會連續(xù)拍攝 10 張照片筑舅,其實現(xiàn)原理就是采用了多次模式的 Capture,所有的配置流程和拍攝單張照片一樣陨舱,唯一的區(qū)別是我們使用 CameraCaptureSession.captureBurst() 進行拍照翠拣,該方法要求我們傳遞一下三個參數(shù):

  • requests:按順序連續(xù)執(zhí)行的 CaptureRequest 對象列表,每一個 CaptureRequest 對象都可以有自己的配置游盲,在我們的 Demo 里出于簡化的目的误墓,10 個 CaptureRequest 對象實際上的都是同一個邦尊。
  • listener:監(jiān)聽 Capture 狀態(tài)的回調(diào)接口,需要注意的是有多少個 CaptureRequest 對象就會回調(diào)該接口多少次优烧。
  • handler:回調(diào) Capture 狀態(tài)監(jiān)聽接口的 Handler 對象。
val captureImageRequest = captureImageRequestBuilder.build()
val captureImageRequests = mutableListOf<CaptureRequest>()
for (i in 1..burstNumber) {
    captureImageRequests.add(captureImageRequest)
}
captureSession.captureBurst(captureImageRequests, CaptureImageStateCallback(), mainHandler)

接下來所有的流程就和拍攝單招照片一樣了链峭,每輸出一張圖片我們就將其保存到 SD 卡并且刷新媒體庫和縮略圖畦娄。

4 如何連拍

連拍這個功能在 Camera2 出現(xiàn)之前是不可能實現(xiàn)的,現(xiàn)在我們只需要使用重復(fù)模式的 Capture 就可以輕松實現(xiàn)連拍功能弊仪。在《Camera2 預(yù)覽》里我們使用了重復(fù)模式的 Capture 來實現(xiàn)預(yù)覽功能熙卡,而這一次我們不僅要用該模式進行預(yù)覽,還要在預(yù)覽的同時也輸出照片励饵,所以我們會使用 CameraCaptureSession.setRepeatingRequest() 方法開始進行連拍:

val captureImageRequest = captureImageRequestBuilder.build()
captureSession.setRepeatingRequest(captureImageRequest, CaptureImageStateCallback(), mainHandler)

停止連拍有以下兩種方式:

  1. 調(diào)用 CameraCaptueSession.stopRepeating() 方法停止重復(fù)模式的 Capture驳癌,但是這會導(dǎo)致預(yù)覽也停止。
  2. 調(diào)用 CameraCaptueSession.setRepeatingRequest() 方法并且使用預(yù)覽的 CaptureRequest 對象役听,停止輸出照片颓鲜。

在我們的 Demo 里使用了第二種方式:

@MainThread
private fun stopCaptureImageContinuously() {
    // Restart preview to stop the continuous image capture.
    startPreview()
}

5 如何切換前后置攝像頭

切換前后置攝像頭是一個很常見的功能,雖然和本章的主要內(nèi)容不相關(guān)典予,但是在 Demo 中已經(jīng)實現(xiàn)甜滨,所以這里也順便提一下。我們只要按照以下順序進行操作就可以輕松實現(xiàn)前后置攝像頭的切換:

  1. 關(guān)閉當(dāng)前攝像頭
  2. 開啟新的攝像頭
  3. 創(chuàng)建新的 Session
  4. 開啟預(yù)覽

下面是代碼片段瘤袖,詳細代碼大家可以自行查看 Demo 源碼:

@MainThread
private fun switchCamera() {
    val cameraDevice = cameraDeviceFuture?.get()
    val oldCameraId = cameraDevice?.id
    val newCameraId = if (oldCameraId == frontCameraId) backCameraId else frontCameraId
    if (newCameraId != null) {
        closeCamera()
        openCamera(newCameraId)
        createCaptureRequestBuilders()
        setPreviewSize(MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT)
        setImageSize(MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT)
        createSession()
        startPreview()
    }
}

6 總結(jié)

本章主要講述了如何實現(xiàn)幾種常見的拍照模式衣摩,其核心要領(lǐng)就是理解【重復(fù)模式】、【單詞模式】和【多次模式】的工作流程捂敌,根據(jù)實際業(yè)務(wù)情況靈活運用艾扮,下面是幾個小建議:

  1. 重復(fù)模式和多次模式都可以實現(xiàn)連拍功能,其中重復(fù)模式適合沒有連拍上限的情況占婉,而多次模式適合有連拍上限的情況泡嘴。
  2. 一個 CaptureRequest 可以添加多個 Surface,這就意味著你可以同時拍攝多張照片锐涯。
  3. 拍照獲取 CaptureResult 和 Image 對象走的是兩個不同的回調(diào)接口磕诊,靈活運用子線程的阻塞操作可以簡化你的代碼邏輯。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末纹腌,一起剝皮案震驚了整個濱河市霎终,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌升薯,老刑警劉巖莱褒,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異涎劈,居然都是意外死亡广凸,警方通過查閱死者的電腦和手機阅茶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谅海,“玉大人脸哀,你說我怎么就攤上這事∨び酰” “怎么了撞蜂?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長侥袜。 經(jīng)常有香客問我蝌诡,道長,這世上最難降的妖魔是什么枫吧? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任浦旱,我火速辦了婚禮,結(jié)果婚禮上九杂,老公的妹妹穿的比我還像新娘颁湖。我一直安慰自己,他們只是感情好尼酿,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布爷狈。 她就那樣靜靜地躺著,像睡著了一般裳擎。 火紅的嫁衣襯著肌膚如雪涎永。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天鹿响,我揣著相機與錄音羡微,去河邊找鬼。 笑死惶我,一個胖子當(dāng)著我的面吹牛妈倔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播绸贡,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼盯蝴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了听怕?” 一聲冷哼從身側(cè)響起捧挺,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎尿瞭,沒想到半個月后闽烙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡声搁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年黑竞,在試婚紗的時候發(fā)現(xiàn)自己被綠了捕发。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡很魂,死狀恐怖扎酷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情遏匆,我是刑警寧澤霞玄,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站拉岁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏惰爬。R本人自食惡果不足惜喊暖,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望撕瞧。 院中可真熱鬧陵叽,春花似錦、人聲如沸丛版。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽页畦。三九已至胖替,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間豫缨,已是汗流浹背独令。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留好芭,地道東北人燃箭。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像舍败,于是被迫代替她去往敵國和親招狸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容