
上一章《Camera2 預(yù)覽》我們學(xué)習(xí)了如何配置預(yù)覽,接下來(lái)我們來(lái)學(xué)習(xí)如何拍照。
閱讀完本章,你將會(huì)學(xué)到以下幾個(gè)知識(shí)點(diǎn):
- 理解 Capture 工作流程
- 如何拍攝單張照片
- 如何連續(xù)拍攝多張照片
- 如何連拍照片
- 如何配置縮略圖尺寸
- 如何播放快門(mén)音效
- 如何矯正圖片方向
- 如何切換前后置攝像頭
你可以在 https://github.com/darylgo/Camera2Sample 下載相關(guān)的源碼,并且切換到 Tutorial4 標(biāo)簽下。
1 理解 Capture 工作流程
在正式介紹如何拍照之前,我們有必要深入理解幾種不同模式的 Capture 的工作流程,只要理解它們的工作流程就很容易掌握各種拍照模式的實(shí)現(xiàn)原理,在第一章《Camera2 概覽》 里我們介紹了 Capture 有以下幾種不同模式:
單次模式(One-shot):指的是只執(zhí)行一次的 Capture 操作,例如設(shè)置閃光燈模式、對(duì)焦模式和拍一張照片等。多個(gè)單次模式的 Capture 會(huì)進(jìn)入隊(duì)列按順序執(zhí)行。
多次模式(Burst):指的是連續(xù)多次執(zhí)行指定的 Capture 操作,該模式和多次執(zhí)行單次模式的最大區(qū)別是連續(xù)多次 Capture 期間不允許插入其他任何 Capture 操作,例如連續(xù)拍攝 100 張照片,在拍攝這 100 張照片期間任何新的 Capture 請(qǐng)求都會(huì)排隊(duì)等待,直到拍完 100 張照片。多組多次模式的 Capture 會(huì)進(jìn)入隊(duì)列按順序執(zhí)行。
重復(fù)模式(Repeating):指的是不斷重復(fù)執(zhí)行指定的 Capture 操作,當(dāng)有其他模式的 Capture 提交時(shí)會(huì)暫停該模式,轉(zhuǎn)而執(zhí)行其他被模式的 Capture,當(dāng)其他模式的 Capture 執(zhí)行完畢后又會(huì)自動(dòng)恢復(fù)繼續(xù)執(zhí)行該模式的 Capture,例如顯示預(yù)覽畫(huà)面就是不斷 Capture 獲取每一幀畫(huà)面。該模式的 Capture 是全局唯一的,也就是新提交的重復(fù)模式 Capture 會(huì)覆蓋舊的重復(fù)模式 Capture。
我們舉個(gè)例子來(lái)進(jìn)一步說(shuō)明上面三種模式,假設(shè)我們的相機(jī)應(yīng)用程序開(kāi)啟了預(yù)覽,所以會(huì)提交一個(gè)重復(fù)模式的 Capture 用于不斷獲取預(yù)覽畫(huà)面,然后我們提交一個(gè)單次模式的 Capture,接著我們又提交了一組連續(xù)三次的多次模式的 Capture,這些不同模式的 Capture 會(huì)按照下圖所示被執(zhí)行:

下面是幾個(gè)重要的注意事項(xiàng):
無(wú)論 Capture 以何種模式被提交,它們都是按順序串行執(zhí)行的,不存在并行執(zhí)行的情況。
重復(fù)模式是一個(gè)比較特殊的模式,因?yàn)樗鼤?huì)保留我們提交的 CaptureRequest 對(duì)象用于不斷重復(fù)執(zhí)行 Capture 操作,所以大多數(shù)情況下重復(fù)模式的 CaptureRequest 和其他模式的 CaptureRequest 是獨(dú)立的,這就會(huì)導(dǎo)致重復(fù)模式的參數(shù)和其他模式的參數(shù)會(huì)有一定的差異,例如重復(fù)模式不會(huì)配置
CaptureRequest.AF_TRIGGER_START,因?yàn)檫@會(huì)導(dǎo)致相機(jī)不斷觸發(fā)對(duì)焦的操作。如果某一次的 Capture 沒(méi)有配置預(yù)覽的 Surface,例如拍照的時(shí)候,就會(huì)導(dǎo)致本次 Capture 不會(huì)將畫(huà)面輸出到預(yù)覽的 Surface 上,進(jìn)而導(dǎo)致預(yù)覽畫(huà)面卡頓的情況,所以大部分情況下我們都會(huì)將預(yù)覽的 Surface 添加到所有的 CaptureRequest 里。
2 如何拍攝單張照片
拍攝單張照片是最簡(jiǎn)單的拍照模式,它使用的就是單次模式的 Capture,我們會(huì)使用 ImageReader 創(chuàng)建一個(gè)接收照片的 Surface,并且把它添加到 CaptureRequest 里提交給相機(jī)進(jìn)行拍照,最后通過(guò) ImageReader 的回調(diào)獲取 Image 對(duì)象,進(jìn)而獲取 JPEG 圖像數(shù)據(jù)進(jìn)行保存。
2.1 定義回調(diào)接口
當(dāng)拍照完成的時(shí)候我們會(huì)得到兩個(gè)數(shù)據(jù)對(duì)象,一個(gè)是通過(guò) onImageAvailable() 回調(diào)給我們的存儲(chǔ)圖像數(shù)據(jù)的 Image,一個(gè)是通過(guò) onCaptureCompleted() 回調(diào)給我們的存儲(chǔ)拍照信息的 CaptureResult,它們是一一對(duì)應(yīng)的,所以我們定義了如下兩個(gè)回調(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è)我們要求照片的尺寸最大不能超過(guò) 4032x3024,并且比例必須是 4:3,所以會(huì)有如下篩選尺寸的代碼片段:
@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)建一個(gè)圖像格式是 JPEG 的 ImageReader 對(duì)象,并且獲取它的 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
接下來(lái)我們使用 TEMPLATE_STILL_CAPTURE 模板創(chuàng)建一個(gè)用于拍照的 CaptureRequest.Builder 對(duì)象,并且添加拍照的 Surface 和預(yù)覽的 Surface 到其中:
captureImageRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureImageRequestBuilder.addTarget(previewDataSurface)
captureImageRequestBuilder.addTarget(jpegSurface)
你可能會(huì)疑問(wèn)為什么拍照用的 CaptureRequest 對(duì)象需要添加預(yù)覽的 Surface,這一點(diǎn)我們?cè)谇懊嬗薪忉屵^(guò)了,如果某一次的 Capture 沒(méi)有配置預(yù)覽的 Surface,例如拍照的時(shí)候,就會(huì)導(dǎo)致本次 Capture 不會(huì)將畫(huà)面輸出到預(yù)覽的 Surface 上,進(jìn)而導(dǎo)致預(yù)覽畫(huà)面卡頓的情況,所以大部分情況下我們都會(huì)將預(yù)覽的 Surface 添加到所有的 CaptureRequest 里。
2.4 矯正 JPEG 圖片方向
在 《Camera2 預(yù)覽》 里我們介紹了一些方向的概念,也提到了攝像頭傳感器的方向很多時(shí)候都不是 0°,這就會(huì)導(dǎo)致我們拍出來(lái)的照片方向是錯(cuò)誤的,例如手機(jī)攝像頭傳感器方向是 90° 的時(shí)候,垂直拿著手機(jī)拍出來(lái)的照片很可能是橫著的:

在進(jìn)行圖片方向矯正的時(shí)候,我們的目的是做到所見(jiàn)即所得,也就是用戶在預(yù)覽畫(huà)面里看到的是什么樣,輸出的圖片就是什么樣。為了做到圖片所見(jiàn)即所得,我們要同時(shí)考慮設(shè)備方向和攝像頭傳感器方向,下面是一段來(lái)自官方的圖片矯正代碼:
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ù)覽》 里我們介紹的一些方向概念,那么上面這段代碼其實(shí)就很容易理解,唯一特別的地方是前置攝像頭輸出的畫(huà)面底層默認(rèn)做了鏡像的翻轉(zhuǎn)才能保證我們?cè)陬A(yù)覽的時(shí)候看到的畫(huà)面就想照鏡子一樣,所以前置攝像頭給的 SENSOR_ORIENTATION 值也是經(jīng)過(guò)鏡像的,但是相機(jī)在輸出 JPEG 的時(shí)候并沒(méi)有進(jìn)行鏡像操作,所以在計(jì)算 JPEG 矯正角度的時(shí)候要對(duì)這個(gè)默認(rèn)鏡像的操作進(jìn)行逆向鏡像。
計(jì)算出圖片的矯正角度后,我們要通過(guò) CaptureRequest.JPEG_ORIENTATION 配置這個(gè)角度,相機(jī)在拍照輸出 JPEG 圖像的時(shí)候會(huì)參考這個(gè)角度值從以下兩種方式選一種進(jìn)行圖像方向矯正:
- 直接對(duì)圖像進(jìn)行旋轉(zhuǎn),并且將 Exif 的 ORIENTATION 標(biāo)簽賦值為 0。
- 不對(duì)圖像進(jìn)行旋轉(zhuǎn),而是將旋轉(zhuǎn)信息寫(xiě)入 Exif 的 ORIENTATION 標(biāo)簽里。
客戶端在顯示圖片的時(shí)候一定要去檢查 Exif 的ORIENTATION 標(biāo)簽的值,并且根據(jù)這個(gè)值對(duì)圖片進(jìn)行對(duì)應(yīng)角度的旋轉(zhuǎn)才能保證圖片顯示方向是正確的。
val deviceOrientation = deviceOrientationListener.orientation
val jpegOrientation = getJpegOrientation(cameraCharacteristics, deviceOrientation)
captureImageRequestBuilder[CaptureRequest.JPEG_ORIENTATION] = jpegOrientation
2.5 設(shè)置縮略圖尺寸
相機(jī)在輸出 JPEG 圖片的時(shí)候,同時(shí)會(huì)根據(jù)我們通過(guò) CaptureRequest.JPEG_THUMBNAIL_SZIE 配置的縮略圖尺寸生成一張縮略圖寫(xiě)入圖片的 Exif 信息里。在設(shè)置縮略圖尺寸之前,我們首先要獲取相機(jī)支持哪些縮略圖尺寸,與獲取預(yù)覽尺寸或照片尺寸列表方式不一樣的是,縮略圖尺寸列表是直接通過(guò) CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES 獲取的。配置縮略圖尺寸的代碼如下所示:
val availableThumbnailSizes = cameraCharacteristics[CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES]
val thumbnailSize = getOptimalSize(availableThumbnailSizes, maxWidth, maxHeight)
在獲取圖片縮略圖的時(shí)候,我們不能總是假設(shè)圖片一定會(huì)在 Exif 寫(xiě)入縮略圖,當(dāng) Exif 里面沒(méi)有縮略圖數(shù)據(jù)的時(shí)候,我們要轉(zhuǎn)而直接 Decode 原圖獲取縮略圖,另外無(wú)論是原圖還是縮略圖,都要根據(jù) Exif 的 ORIENTATION 角度進(jìn)行角度矯正才能正確顯示,下面是我們 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è)置定位信息
拍照的時(shí)候,通常都會(huì)在圖片的 Exif 寫(xiě)入定位信息,我們可以通過(guò) 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 播放快門(mén)音效
在進(jìn)行拍照之前,我們還需要配置拍照時(shí)播放的快門(mén)音效,因?yàn)?Camera2 和 Camera1 不一樣,拍照時(shí)不會(huì)有任何聲音,需要我們?cè)谶m當(dāng)?shù)臅r(shí)候通過(guò) MediaSoundPlayer 播放快門(mén)音效,通常情況我們是在 CaptureStateCallback.onCaptureStarted() 回調(diào)的時(shí)候播放快門(mén)音效:
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)過(guò)一連串的配置之后,我們終于可以開(kāi)拍照了,直接調(diào)用 CameraCaptureSession.capture() 方法把 CaptureRequest 對(duì)象提交給相機(jī)就可以等待相機(jī)輸出圖片了,該方法要求我們?cè)O(shè)置三個(gè)參數(shù):
- request:本次 Capture 操作使用的 CaptureRequest 對(duì)象。
- listener:監(jiān)聽(tīng) Capture 狀態(tài)的回調(diào)接口。
- handler:回調(diào) Capture 狀態(tài)監(jiān)聽(tīng)接口的 Handler 對(duì)象。
captureSession.capture(captureImageRequest, CaptureImageStateCallback(), mainHandler)
如果一切順利,相機(jī)在拍照完成的時(shí)候會(huì)通過(guò) CaptureStateCallback.onCaptureCompleted() 回調(diào)一個(gè) CaptureResult 對(duì)象給我們,里面包含了本次拍照的所有信息,另外還會(huì)通過(guò) OnImageAvailableListener.onImageAvailable() 回調(diào)一個(gè)代表圖像數(shù)據(jù)的 Image 對(duì)象給我們。在我們的 Demo 中,我們將獲取到的 CaptureResult 對(duì)象保存到一個(gè)阻塞隊(duì)列中,在 OnImageAvailableListener.onImageAvailable() 回調(diào)的時(shí)候就從這個(gè)阻塞隊(duì)列獲取 CaptureResult 對(duì)象,結(jié)合 Image 對(duì)象對(duì)圖片進(jìn)行保存操作,并且還會(huì)在圖片保存完畢的時(shí)候獲取圖片的縮略圖用于刷新 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 前置攝像頭拍照的鏡像問(wèn)題
如果你使用前置攝像頭進(jìn)行拍照,雖然照片的方向已經(jīng)被我們矯正了,但是你會(huì)發(fā)現(xiàn)畫(huà)面卻是相反的,例如你在預(yù)覽的時(shí)候人臉在左邊,拍出來(lái)的照片人臉卻是在右邊。出現(xiàn)這個(gè)問(wèn)題的原因是默認(rèn)情況下相機(jī)不會(huì)對(duì) JPEG 圖像進(jìn)行鏡像操作,導(dǎo)致輸出的原始畫(huà)面是非鏡像的。解決這個(gè)問(wèn)題的一個(gè)辦法是拿到 JPEG 數(shù)據(jù)之后再次對(duì)圖像進(jìn)行鏡像操作,然后才保存圖片。
3 如何連續(xù)拍攝多張圖片
在我們的 Demo 中有一個(gè)特殊的拍照功能,就是當(dāng)用戶雙擊快門(mén)按鈕的時(shí)候會(huì)連續(xù)拍攝 10 張照片,其實(shí)現(xiàn)原理就是采用了多次模式的 Capture,所有的配置流程和拍攝單張照片一樣,唯一的區(qū)別是我們使用 CameraCaptureSession.captureBurst() 進(jìn)行拍照,該方法要求我們傳遞一下三個(gè)參數(shù):
- requests:按順序連續(xù)執(zhí)行的 CaptureRequest 對(duì)象列表,每一個(gè) CaptureRequest 對(duì)象都可以有自己的配置,在我們的 Demo 里出于簡(jiǎn)化的目的,10 個(gè) CaptureRequest 對(duì)象實(shí)際上的都是同一個(gè)。
- listener:監(jiān)聽(tīng) Capture 狀態(tài)的回調(diào)接口,需要注意的是有多少個(gè) CaptureRequest 對(duì)象就會(huì)回調(diào)該接口多少次。
- handler:回調(diào) Capture 狀態(tài)監(jiān)聽(tīng)接口的 Handler 對(duì)象。
val captureImageRequest = captureImageRequestBuilder.build()
val captureImageRequests = mutableListOf<CaptureRequest>()
for (i in 1..burstNumber) {
captureImageRequests.add(captureImageRequest)
}
captureSession.captureBurst(captureImageRequests, CaptureImageStateCallback(), mainHandler)
接下來(lái)所有的流程就和拍攝單招照片一樣了,每輸出一張圖片我們就將其保存到 SD 卡并且刷新媒體庫(kù)和縮略圖。
4 如何連拍
連拍這個(gè)功能在 Camera2 出現(xiàn)之前是不可能實(shí)現(xiàn)的,現(xiàn)在我們只需要使用重復(fù)模式的 Capture 就可以輕松實(shí)現(xiàn)連拍功能。在《Camera2 預(yù)覽》里我們使用了重復(fù)模式的 Capture 來(lái)實(shí)現(xiàn)預(yù)覽功能,而這一次我們不僅要用該模式進(jìn)行預(yù)覽,還要在預(yù)覽的同時(shí)也輸出照片,所以我們會(huì)使用 CameraCaptureSession.setRepeatingRequest() 方法開(kāi)始進(jìn)行連拍:
val captureImageRequest = captureImageRequestBuilder.build()
captureSession.setRepeatingRequest(captureImageRequest, CaptureImageStateCallback(), mainHandler)
停止連拍有以下兩種方式:
- 調(diào)用
CameraCaptueSession.stopRepeating()方法停止重復(fù)模式的 Capture,但是這會(huì)導(dǎo)致預(yù)覽也停止。 - 調(diào)用
CameraCaptueSession.setRepeatingRequest()方法并且使用預(yù)覽的 CaptureRequest 對(duì)象,停止輸出照片。
在我們的 Demo 里使用了第二種方式:
@MainThread
private fun stopCaptureImageContinuously() {
// Restart preview to stop the continuous image capture.
startPreview()
}
5 如何切換前后置攝像頭
切換前后置攝像頭是一個(gè)很常見(jiàn)的功能,雖然和本章的主要內(nèi)容不相關(guān),但是在 Demo 中已經(jīng)實(shí)現(xiàn),所以這里也順便提一下。我們只要按照以下順序進(jìn)行操作就可以輕松實(shí)現(xiàn)前后置攝像頭的切換:
- 關(guān)閉當(dāng)前攝像頭
- 開(kāi)啟新的攝像頭
- 創(chuàng)建新的 Session
- 開(kāi)啟預(yù)覽
下面是代碼片段,詳細(xì)代碼大家可以自行查看 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é)
本章主要講述了如何實(shí)現(xiàn)幾種常見(jiàn)的拍照模式,其核心要領(lǐng)就是理解【重復(fù)模式】、【單詞模式】和【多次模式】的工作流程,根據(jù)實(shí)際業(yè)務(wù)情況靈活運(yùn)用,下面是幾個(gè)小建議:
- 重復(fù)模式和多次模式都可以實(shí)現(xiàn)連拍功能,其中重復(fù)模式適合沒(méi)有連拍上限的情況,而多次模式適合有連拍上限的情況。
- 一個(gè) CaptureRequest 可以添加多個(gè) Surface,這就意味著你可以同時(shí)拍攝多張照片。
- 拍照獲取 CaptureResult 和 Image 對(duì)象走的是兩個(gè)不同的回調(diào)接口,靈活運(yùn)用子線程的阻塞操作可以簡(jiǎn)化你的代碼邏輯。