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: 指定输出的图片路径 + metadata等;
// 参数2: 线程池,指定拍照成功或失败的回调线程; 参数3: 拍照成功或失败的回调
imageCapture.takePicture(ImageCapture.OutputFileOptions, Executor, ImageCapture.OnImageSavedCallback)
// 通过 ImageProxy 对象自己处理图片数据; 参数1: 线程池, 拍照成功或失败的回调在这个线程执行; 参数2: 拍照成功或失败的回调
imageCapture.takePicture(Executor, ImageCapture.OnImageCapturedCallback)

下面分别对参数解释:

  • 初始化 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

    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)
                  // 获取照片 Exif 信息
                  val byteArrayInputStream = ByteArrayInputStream(byteArray)
                  val orientation = ExifInterface(byteArrayInputStream)
    
                  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)
              }
    }
    

额外知识


  • 如果有依赖下载不下来可以用阿里的代理:

    maven { url 'https://maven.aliyun.com/repository/public' }
    maven { url 'https://maven.aliyun.com/repository/google' }
    maven { url 'https://maven.aliyun.com/repository/jcenter' }
    
  • 需要注意的是, 拍的照片有可能在一些手机上发生照片旋转的情况.
    这是因为这些手机认为打开摄像头进行拍摄时手机就应该是横屏的, 因此回到竖屏的情况下就会发生 90 度的旋转. 可以使用下面的方法处理:

    private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
    	val exif = ExifInterface(imagePath)
    	val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
    	return when (orientation) {
    		ExifInterface.ORIENTATION_ROTATE_90 -> rotateBitmap(bitmap, 90)
    		ExifInterface.ORIENTATION_ROTATE_180 -> rotateBitmap(bitmap, 180)
    		ExifInterface.ORIENTATION_ROTATE_270 -> rotateBitmap(bitmap, 270)
    		else -> bitmap
    	}
    }
    
    private fun rotateBitmap(bitmap: Bitmap, degree: Int): Bitmap {
    	val matrix = Matrix()
    	matrix.postRotate(degree.toFloat())
    	val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
    	bitmap.recycle()
    	return rotatedBitmap
    }
    
  • 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)))
    
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值