由於項目中自定義的相機經常報奇葩bug,決定學習下 CameraX。通過查看谷歌的官方 Demo ,學到了不少知識,做了這份筆記。
下面是相機的使用步驟及方法解釋等。
CameraX 使用
添加依賴
// CameraX core library
def camerax_version = '1.0.0-beta04'
implementation "androidx.camera:camera-core:$camerax_version"
// CameraX Camera2 extensions
implementation "androidx.camera:camera-camera2:$camerax_version"
// CameraX Lifecycle library
implementation "androidx.camera:camera-lifecycle:$camerax_version"
// CameraX View class
implementation 'androidx.camera:camera-view:1.0.0-alpha11'
xml 佈局
<!-- 建議寬高都用 match -->
<androidx.camera.view.PreviewView
android:id="@+id/view_finder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
// 設置預覽界面的對齊方式,默認 FILL_CENTER 等比例鋪滿 previewView
viewFinder.scaleType = PreviewView.ScaleType.FIT_CENTER
用到的主要類解釋
- 開始的地方:創建 Camera 對象
Camera = ProcessCameraProvider.bindToLifecycle(LifecycleOwner, CameraSelector, Preview, ImageCapture, ImageAnalysis);
涉及對象解釋:
- ProcessCameraProvider 一個單例,用於將相機的生命週期綁定到任何 {@link LifecycleOwner} 中。
- Camera 綁定用例後返回的camera對象,這個對象基本沒用
- LifecycleOwner 與 activity/fragment 生命週期綁定
- CameraSelector 相機選擇器,用於選擇前後置相機
- Preview 相機預覽,通過preview.setSurfaceProvider() 與 PreviewView 綁定實現預覽
- ImageCapture 拍照,拍照由這個對象觸發和監聽
- ImageAnalysis 數據解析,實時監聽相機圖像數據
各類的初始化
-
初始化 ProcessCameraProvider
// 要求指定compileOptions Java8, 否則這個方法會拋異常 val cameraProviderFuture: ListenableFuture<ProcessCameraProvider!> = ProcessCameraProvider.getInstance(context) // 當 ProcessCameraProvider 初始化完成會調用 listener cameraProviderFuture.addListener(Runnable { val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() // 在綁定前先要解綁 cameraProvider.unbindAll() // 這個方法建議try catch, 因爲cameraSelector配置不對或重複綁定等都會拋異常 camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, imageAnalysis) }, ContextCompat.getMainExecutor(context))
-
初始化 CameraSelector
val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
-
初始化 Preview
val preview = Preview.Builder() // 設置了寬高比率,不能再設置分辨率 .setTargetAspectRatio(AspectRatio.RATIO_16_9) // .setTargetResolution(Size(1080, 1920)) // 設置 .setTargetRotation(viewFinder.getDisplay().getRotation()) .build() //設置 SurfaceProvider preview.setSurfaceProvider(viewFinder.createSurfaceProvider())
-
初始化 ImageCapture
val imageCapture = ImageCapture.Builder() .setTargetAspect(AspectRatio.RATIO_16_9) // .setTargetResolution(Size(1080, 1920)) .setRotation(viewFinder.getDisplay().getRataton()) // 閃光燈默認關閉 .setFlashMode(ImageCapture.FLASH_MODE_AUTO) .build() // 注意:這個方法可以做到預覽與照片的比率和顯示區域完全一致 imageCapture.setCropAspectRatio(Rational(viewFinder.width, viewFinder.height))
-
初始化 ImageAnalysis
val imageAnalysis = ImageAnalysis.Builder() .setTargetAspect(AspectRatio.RATIO_16_9) // .setTargetResolution(Size(1080, 1920)) .setRotation(viewFinder.getDisplay().getRataton()) .build() // 第一個參數爲線程池,指定 Analyzer 接口回調的線程;第二個參數 Analyzer 接口對象 imageAnalysis.setAnalyzer(Executors.newSingleThreadExecutor(), 實現 Analyzer 接口)
拍照功能 ImageCapure
拍照有兩種方式,一種直接將圖片保存到指定位置; 另一種返回 ImageProxy 對象,裏面包含圖片信息。看下面代碼演示:
- 直接將圖片保存到指定位置
// 參數1: 指定輸出的圖片路徑 + metadata等;參數2: 線程池,指定拍照成功或失敗的回調線程; 參數3: 拍照成功或失敗的回調 imageCapture.takePicture(ImageCapture.OutputFileOptions, Executor, ImageCapture.OnImageSavedCallback)
對應參數的初始化:
-
初始化 ImageCapture.OutputFileOption
//創建存儲路徑 val file = getPhotoFile(); val metadata = ImageCature.Metadata().apply { // 當爲前置攝像頭時鏡像;前置攝像頭預覽時默認就是鏡像 isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT // 還可設置 Location // location = } val outputOption = ImageCapture.OutputFileOption.Builder(file) .setMetadata(metadata) .build()
-
初始化 ImageCapture.OnImageSavedCallback
val callback = object : ImageCapture.OnImageSavedCallback { override fun onImageSaved(output: ImageCapture.OutputFileResults) { // 通過 savedUri 獲取拍照結果 val savedUri: Uri = output.savedUri ?: Uri.fromFile(file) //注意:這個回調方法在子線程,即上面傳入的 Executor 線程 } override fun onError(exc: ImageCaptureException) { Log.e(TAG, "Photo capture failed: ${exc.message}", exc) Toast.makeText(requireContext(), exc.message, Toast.LENGTH_LONG).show() } }
- 自己處理圖像數據 ImageCapture.OnImageCapturedCallback
-
主方法
// 參數1: 線程池, 拍照成功或失敗的回調在這個線程執行; 參數2: 拍照成功或失敗的回調 imageCapture.takePicture(Executor, ImageCapture.OnImageCapturedCallback)
-
初始化 ImageCapture.OnImageCapturedCallback
val callback = object : ImageCapture.OnImageCapturedCallback() { override fun onCaptureSuccess(image: ImageProxy) { // 將 imageProxy 轉爲 byte數組 val buffer: ByteBuffer = image.planes[0].buffer // 新建指定長度數組 val byteArray = ByteArray(buffer.remaining()) // 倒帶到起始位置 0 buffer.rewind() // 數據複製到數組, 這個 byteArray 包含有 exif 相關信息, // 由於 bitmap 對象不會包含 exif 信息,所以轉爲 bitmap 需要注意保存 exif 信息 buffer.get(byteArray) val imageView = container.findViewById<ImageView>(R.id.image_view) // 主線程更新 UI imageView.post{ Glide.with(imageView) .load(byteArray) .into(imageView) } image.close() } override fun onError(exc: ImageCaptureException) { Log.e(TAG, "Photo capture failed: ${exc.message}", exc) } }
-
額外知識
-
FragmentContainerView
用於容納動態添加的 Fragment 的容器,可替代使用 FrameLayout 等,因爲它解決了動畫 z 排序問題以及分派給 Fragment 的窗口邊襯區的問題。 -
取消activity旋轉動畫
android:rotationAnimation=“seamless” -
各個方法對於的目錄:
當目錄不存在時還會自動創建
//外部存儲 getExternalCacheDirs: /storage/emulated/0/Android/data/com.example.cameraxstudy/cache getObbDir: /storage/emulated/0/Android/obb/com.example.cameraxstudy getExternalMediaDirs: /storage/emulated/0/Android/media/com.example.cameraxstudy getExternalCacheDir: /storage/emulated/0/Android/data/com.example.cameraxstudy/cache // 內部存儲 getCacheDir: /data/user/0/com.example.cameraxstudy/cache getFilesDir: /data/user/0/com.example.cameraxstudy/files getDataDir: /data/user/0/com.example.cameraxstudy getCodeCacheDir: /data/user/0/com.example.cameraxstudy/code_cache getNoBackupFilesDir: /data/user/0/com.example.cameraxstudy/no_backup
-
分享圖片
var mediaFile: File = getMediaFile() val intent = Intent().apply { // 從文件擴展名推斷媒體類型 val mediaType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(mediaFile.extension) // 從我們的FileProvider實現中獲取URI val uri = FileProvider.getUriForFile( view.context, BuildConfig.APPLICATION_ID + ".provider", mediaFile) // 設置適當的 intent extra, type, action and flags putExtra(Intent.EXTRA_STREAM, uri) type = mediaType action = Intent.ACTION_SEND flags = Intent.FLAG_GRANT_READ_URI_PERMISSION } // 啓動意圖,讓用戶選擇要共享的應用程序 startActivity(Intent.createChooser(intent, getString(R.string.share_hint)))