由于项目中自定义的相机经常报奇葩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)))