Android進(jìn)階CameraX與Camera2使用比對(duì)詳解
正文
相機(jī),作為手機(jī)最重要的一個(gè)多媒體工具,被應(yīng)用于眾多app軟件中,如果整個(gè)項(xiàng)目中涉及到拍照、直播、錄視頻、掃碼,那么相機(jī)就必須要用到。傳統(tǒng)的相機(jī)app,一般使用到Camera或者Camera2比較多,但是Google的JectPack框架中引入了CameraX組件作為官方推薦相機(jī)架構(gòu),既然推出此框架,那么一定是有它自身的優(yōu)勢(shì)之處在的,本文將會(huì)從CameraX和Camera2的框架機(jī)制出發(fā),分析兩者的不同以及性能差異。
1 Google相機(jī)元老 Camera2
其實(shí)在早期開發(fā)相機(jī)app的時(shí)候,有一部分會(huì)使用Camera,有一部分會(huì)使用Camera2,但是用起來真的是苦不堪言,往往在相機(jī)配置時(shí),為了調(diào)出預(yù)覽頁面,至少要寫1000行代碼,而且僅僅是一個(gè)預(yù)覽頁面,后面拍照、錄視頻等等,還需要做額外的開發(fā),說這么多,我們先看下Camera2是如何使用的吧。
1.1 Camera2的使用
首先Camera2是Google原生的相機(jī)框架,所以不需要引任何框架進(jìn)來。
第一步:創(chuàng)建承載相機(jī)的容器
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextureView android:id="@+id/camera_view_container" android:layout_width="match_parent" android:layout_height="match_parent"/> </FrameLayout>
一般來說,承載相機(jī)預(yù)覽頁面的就是TextureView,當(dāng)TextureView創(chuàng)建完成之后,就可以配置相機(jī)參數(shù),展示預(yù)覽頁面。
第二步:打開攝像頭的時(shí)機(jī)
Camera2和Camera不同的是,Camera2是將應(yīng)用層與相機(jī)內(nèi)核層做了完全的解耦,需要通過Camera Service來獲取到CameraManager以此調(diào)用相機(jī)內(nèi)核層提供的能力。
那么在什么時(shí)機(jī)才能打開攝像頭呢?就是在TextureView的onSurfaceTextureAvailable回調(diào)的時(shí)候,去初始化相機(jī)參數(shù),開啟攝像頭。
/** * 初始化相機(jī) * 觸發(fā)時(shí)機(jī) TextureView 的 onSurfaceTextureAvailable回調(diào) */ private fun initCamera(){ } override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { initCamera() } override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { } override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { return false } override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { }
第三步:搭建應(yīng)用層與相機(jī)內(nèi)核的橋梁
/** * 開啟攝像頭 */ @SuppressLint("MissingPermission") @Synchronized fun start(textureView: TextureView) { val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager //建立數(shù)據(jù)傳輸?shù)臉蛄? imageReader = ImageReader.newInstance( previewWidth, previewHeight, ImageFormat.YUV_420_888, 2 ) val handlerThread = HandlerThread("Camera2Manager") handlerThread.start() handler = Handler(handlerThread.looper) imageReader?.setOnImageAvailableListener(this, handler) //真正地開啟攝像頭 cameraManager.openCamera("0", this, handler) } // setOnImageAvailableListener的回調(diào) override fun onImageAvailable(reader: ImageReader?) { } // Device StateCallback override fun onOpened(camera: CameraDevice) { } override fun onDisconnected(camera: CameraDevice) { } override fun onError(camera: CameraDevice, error: Int) { }
因?yàn)閼?yīng)用層和相機(jī)內(nèi)核層已經(jīng)完全解耦,所以兩者想要進(jìn)行數(shù)據(jù)傳遞,必須要建立橋梁,那么通過ImageReader就可以完成,可以通過傳入一些參數(shù):預(yù)覽尺寸、圖像格式等,設(shè)置setOnImageAvailableListener,有數(shù)據(jù)發(fā)送過來之后,通過onImageAvailable獲取到數(shù)據(jù)顯示即可。
然后,調(diào)用CameraManager的openCamera方法,才是真正打開攝像頭,那么成功還是失敗呢?就需要通過 CameraDevice.StateCallback的回調(diào)來判斷。
第四步:建立會(huì)話,創(chuàng)建預(yù)覽請(qǐng)求
override fun onOpened(camera: CameraDevice) { this.cameraDevice = camera //建立會(huì)話 createPreviewSession() } override fun onDisconnected(camera: CameraDevice) { cameraDevice?.close() cameraDevice = null } override fun onError(camera: CameraDevice, error: Int) { cameraDevice?.close() cameraDevice = null }
當(dāng)打開Camera之后,如何判斷是否成功開啟攝像頭呢?就是通過onOpened這個(gè)回調(diào)來判斷,當(dāng)成功開啟攝像頭之后,就需要與相機(jī)內(nèi)核建立會(huì)話,發(fā)起預(yù)覽請(qǐng)求createCaptureRequest。
private fun createPreviewSession() { //創(chuàng)建Surface,所有的畫面渲染都是由Surface處理 val texture = textureView?.surfaceTexture texture?.setDefaultBufferSize(previewWidth, previewHeight) val surface = Surface(texture) //開啟預(yù)覽會(huì)話,這樣就可以預(yù)覽數(shù)據(jù) val builder = cameraDevice?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) builder?.addTarget(surface) builder?.set( CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE ) //想要獲取每一幀的數(shù)據(jù),需要給ImageReader設(shè)置Target imageReader?.surface?.let { builder?.addTarget(it) } //建立連接,開啟會(huì)話 cameraDevice?.createCaptureSession( mutableListOf(surface, imageReader?.surface), CaptureSessionCallback(), handler )
我們知道,如果使用TextureView,那么所有畫面的渲染都是通過Surface來渲染,因此在創(chuàng)建Surface之后,就可以添加到cameraDevice中,此時(shí)頁面中就會(huì)有圖像了,如果想要分析每一幀的數(shù)據(jù),那么也需要給ImageReader設(shè)置Surface.
完成一系列配置之后,調(diào)用CameraDevice的createCaptureSession方法,開啟會(huì)話。
inner class CaptureSessionCallback : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { mSession = session if (cameraDevice == null) return session.setRepeatingRequest(requestBuilder?.build()!!, null, handler) } override fun onConfigureFailed(session: CameraCaptureSession) { } }
因?yàn)樾枰獙?shí)時(shí)的數(shù)據(jù)流一幀一幀地傳遞過來,因此需要發(fā)起重復(fù)請(qǐng)求setRepeatingRequest,以此將實(shí)時(shí)的圖像幀發(fā)送到手機(jī)頁面上進(jìn)行渲染。
1.2 小結(jié)
至此我們先做一個(gè)小小的總結(jié),我們知道如果做一個(gè)相機(jī)app,最主要的兩個(gè)功能:預(yù)覽和分析圖片幀。
我們?cè)诖蜷_攝像頭之前,首先會(huì)通過ImageReader來完成應(yīng)用層和相機(jī)內(nèi)核層的建立,通過監(jiān)聽回調(diào)獲取每一幀的圖像數(shù)據(jù);然后開啟攝像頭之后,又完成的一個(gè)工作就是配置Surface,將其掛載到CameraDevice上,然后如果想要請(qǐng)求獲取圖像幀,那么就需要開啟session,重復(fù)發(fā)起預(yù)覽的請(qǐng)求,來獲取到每一幀的數(shù)據(jù),也正是下圖所展示的。
1.3 問題分析
如果我們做到了上圖的配置,我們運(yùn)行之后發(fā)現(xiàn),只拿到了一幀數(shù)據(jù),緊接著拋出了一個(gè)異常,這個(gè)是在onError中回調(diào)的。
2023-01-14 14:56:14.220 29697-29803/com.lay.highavailablecamera E/HighAvailableCamera: onOpened 2023-01-14 14:56:14.574 29697-29803/com.lay.highavailablecamera E/HighAvailableCamera: onImageAvailable android.media.ImageReader@f04668 2023-01-14 14:59:14.122 29697-29803/com.lay.highavailablecamera E/HighAvailableCamera: onError 3
如果做過相機(jī)應(yīng)用的伙伴應(yīng)該也見到過這個(gè)情況,主要原因就是,一幀一幀地?cái)?shù)據(jù)傳遞過來之后,我們沒有處理,導(dǎo)致數(shù)據(jù)一直阻塞沒有關(guān)閉,從而無法處理下一幀數(shù)據(jù),因此在拿到最新的數(shù)據(jù)之后,需要主動(dòng)調(diào)用close方法,以便下一幀數(shù)據(jù)進(jìn)入到屏幕中。
override fun onImageAvailable(reader: ImageReader?) { Log.e(Constants.TAG, "onImageAvailable $reader") val latestImage = reader?.acquireNextImage() //NOTE 做數(shù)據(jù)處理 latestImage?.close() }
2 CameraX的便捷之處
前面我們用Camera2完成了頁面的預(yù)覽,伙伴們?nèi)绻行枰创a的,可以直接找我。通過前面對(duì)于Camera2的使用,我們發(fā)現(xiàn)太麻煩了,就為了完成一個(gè)頁面預(yù)覽,寫了很多配置,好處就是這些配置是通用的,不需要頻繁地改動(dòng),但是我們想的就是能夠一鍵式搭建一個(gè)相機(jī)應(yīng)用,那么CameraX就是我們需要的”傻瓜相機(jī)“。
2.1 UseCase分類
其實(shí),CameraX對(duì)于每個(gè)功能都做了詳細(xì)的劃分,例如預(yù)覽的PreView、圖像分析的ImageAnalysis等,因此在做Camera配置的時(shí)候,可以選擇自己想要的UseCase進(jìn)行初始化。
private fun initCameraConfig(context: Context) { val cameraProviderFuture = ProcessCameraProvider.getInstance(context) cameraProviderFuture.addListener({ // 預(yù)覽配置 val preview = Preview.Builder() .build() .also { it.setSurfaceProvider(binding.basePreview.surfaceProvider) } //設(shè)置攝像頭是前置還是后置 val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA if (cameraProvider == null) { cameraProvider = cameraProviderFuture.get() //拍照useCase配置 if (imageCapture == null) { imageCapture = ImageCapture .Builder() .setTargetRotation(Surface.ROTATION_90) .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) .setFlashMode(ImageCapture.FLASH_MODE_AUTO) .setTargetAspectRatio(AspectRatio.RATIO_16_9) .build() } //圖像分析useCase配置 if (imageAnalysis == null) { imageAnalysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setTargetAspectRatio(AspectRatio.RATIO_16_9) .build() } //圖像數(shù)據(jù)處 try { cameraProvider?.unbindAll() cameraProvider?.bindToLifecycle( lifecycleOwner!!, cameraSelector, preview, imageCapture, imageAnalysis ) } catch (exc: Exception) { } } }, ContextCompat.getMainExecutor(context)) }
其實(shí)我們可以看到,這里我們是將拍照的userCase以及圖像分析的useCase都做了配置,如果只需要做一個(gè)預(yù)覽配置,也就幾十行代碼就能夠完成,單就使用上,比Camera2要簡(jiǎn)單的多的多。
<androidx.camera.view.PreviewView android:id="@+id/base_preview" android:layout_width="match_parent" android:layout_height="match_parent"/>
對(duì)于預(yù)覽頁面,CameraX中也提供了PreviewView容器,它其實(shí)是繼承自FrameLayout。
其實(shí)對(duì)于CameraX這塊僅僅是講了如何使用,具體的核心實(shí)現(xiàn)等下篇文章細(xì)細(xì)道來,更多關(guān)于Android CameraX與Camera2對(duì)比的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Flutter中使用setState時(shí)的6個(gè)簡(jiǎn)單技巧總結(jié)
平常在使用flutter的控件時(shí)我們都知道,要刷新頁面那么只需要調(diào)用setState()方法即可,這篇文章主要給大家介紹了關(guān)于Flutter中使用setState時(shí)的6個(gè)簡(jiǎn)單技巧,需要的朋友可以參考下2022-05-05Android自定義控件EditText實(shí)現(xiàn)清除和抖動(dòng)功能
這篇文章主要為大家詳細(xì)介紹了Android自定義控件EditText實(shí)現(xiàn)清除和抖動(dòng)功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12Android創(chuàng)建與解析XML(三)——詳解Sax方式
本篇文章主要介紹了Android創(chuàng)建與解析XML(三)——詳解Sax方式 ,這里整理了詳細(xì)的代碼,有需要的小伙伴可以參考下。2016-11-11android 實(shí)現(xiàn)類似微信緩存和即時(shí)更新好友頭像示例
本篇文章主要介紹了android 實(shí)現(xiàn)類似微信緩存和即時(shí)更新好友頭像示例,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-01-01Android日期選擇器對(duì)話框DatePickerDialog使用詳解
這篇文章主要為大家詳細(xì)介紹了Android日期選擇器對(duì)話框DatePickerDialog的使用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01Android判斷json格式將錯(cuò)誤信息提交給服務(wù)器
今天小編就為大家分享一篇關(guān)于Android判斷json格式將錯(cuò)誤信息提交給服務(wù)器,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-03-03Android實(shí)現(xiàn)傾斜角標(biāo)樣式
最新小編接到這樣一個(gè)項(xiàng)目,需要在一個(gè)距形卡片上做一個(gè)傾斜的Tag,類似支付寶上的一個(gè)功能,接著小編給大家?guī)砹藢?shí)現(xiàn)思路,對(duì)android 傾斜角標(biāo)的實(shí)現(xiàn)方法感興趣的朋友跟隨小編一起看看吧2019-10-10