CameraX 学习笔记

由于项目中自定义的相机经常报奇葩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. 直接将图片保存到指定位置
    // 参数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()
           }
    
       }
    
  1. 自己处理图像数据 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)))
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章