Android自定义相机—Jetpack CameraX篇

1,990 阅读3分钟

前言

上一篇简单介绍了自己使用Camera+Surfaceview自定义相机的情况,想了解的可直接前往juejin.cn/post/718533… ,那这一篇继续介绍如何使用Jetpack的CameraX来自定义相机。

先来说说自己使用下来的相比Camera的一个感受吧:

  • 使用简单,无需过多关注相机的配置(这里就可以解决相机预览变形和旋转的问题)
  • 生命周期绑定,这里可以省去打开关闭相机和对相机进行生命周期管理的工作

使用CameraX自定义相机也可以简单分为以下几步:

  • 权限配置
  • 布局配置
  • 预览设置
  • 拍照设置

第一步 权限配置

使用CameraX所需要的权限和Camera类似,主要是以下几种:

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
   android:maxSdkVersion="28" />

android.hardware.camera.any的配置是申明CameraX可以使用设备上的任何一个摄像头,不论是前置还是后置,和Camera获取所有相机来进行指定类似。由于上述权限涉及到Android6.0及以上的动态权限申请,我个人习惯直接使用Google的easyPermission库进行动态权限申请。获取许到相应的权限才能继续后续的相机定制化使用哟~

第二步 布局配置

既然是自定义相机,那么布局就根据自己的实际情况布局即可。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
​
    <androidx.camera.view.PreviewView
        android:id="@+id/view_preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
​
    <LinearLayout
        android:id="@+id/view_bottom_operate"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:background="#FFF"
        android:gravity="center"
        android:orientation="horizontal"
        android:paddingStart="10dp"
        android:paddingEnd="10dp"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">
​
        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/btn_cancle"
            android:layout_width="0dp"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:gravity="center"
            android:src="@mipmap/icon_cancle"
            android:textColor="#000"
            android:textSize="18sp" />
​
        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/btn_take_picture"
            android:layout_width="0dp"
            android:layout_height="50dp"
            android:layout_weight="1"
            android:gravity="center"
            android:src="@mipmap/icon_take_picture"
            android:textColor="#000"
            android:textSize="18sp" />
​
        <androidx.appcompat.widget.AppCompatImageView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text=""
            android:textColor="#000"
            android:textSize="18sp" />
    </LinearLayout>
​
</androidx.constraintlayout.widget.ConstraintLayout>

第三步 实现预览

实现预览可以分为以下几步:

  • 获取相机过程提供者ProcessCameraProvider
  • 为相机过程提供者增加监听
  • 创建预览窗口
  • 指定预览相机
  • 绑定相机过程提供者到持有者生命周期
  • 添加执行器

具体的每一步对应的代码可以直接参考代码及注释,这里就不在过多赘述。

// 获取相机过程提供者实例
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
​
// 为相机过程提供者添加监听
cameraProviderFuture.addListener({
​
    // 获取具体的相机过程提供者
    val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
    // 创建相机预览窗口
    val preview = Preview.Builder()
        .build()
        .also {
            // 这里通过我们布局中的Preview进行预览
            it.setSurfaceProvider(binding.viewPreview.surfaceProvider)
        }
​
    // 获取用于拍照的实例
    imageCapture = ImageCapture.Builder()
        .build()
​
    // 指定用于预览的相机,默认为后置相机,如果需要前置相机预览请使用CameraSelector.DEFAULT_FRONT_CAMERA
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
    try {
        // 绑定提供者之前先进行解绑,不能重复绑定
        cameraProvider.unbindAll()
​
        // 绑定提供者,将相机的生命周期进行绑定,因为camerax具有生命周期感知力,所以消除打开和关闭相机的任务
        cameraProvider.bindToLifecycle(
            this, cameraSelector, preview, imageCapture
        )
​
    } catch (e: Exception) {
        Log.e(TAG, "相机绑定异常 ${e.message}")
    }
}, ContextCompat.getMainExecutor(this))

第四步 拍照

在调用拍照前我们需要指定一个拍照后的文件存放位置,然后再进行拍照,拍照后我们可以根据事先指定的路径获取到我们拍摄的文件。由于Android10之后官方逐步的限制我们使用File的方式去管理文件,因此这里我们使用MediaStore进行。因此拍照同样以下几步:

  • 定义照片名称
  • 使用MediaStore操作照片
  • 配置拍照输出参数
  • 开始拍照

1、定义照片名称

这里为了照片不重复或者不被覆盖,建议使用时间戳定义文件名称,这样能有效的防止文件被覆盖。

SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA).format(System.currentTimeMillis())

2、使用MediaStore操作照片

val contentValues = ContentValues().apply {
    put(MediaStore.MediaColumns.DISPLAY_NAME, name)
    put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
        put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
    }
}

3、配置拍照输出参数

// 指定输出参数
val outputOptions = ImageCapture.OutputFileOptions
    .Builder(
        contentResolver,
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        contentValues
    )
    .build()

4、开始拍照

// 开始拍摄相片
imageCapture.takePicture(
    outputOptions,
    ContextCompat.getMainExecutor(this),
    object : ImageCapture.OnImageSavedCallback {
        override fun onError(exc: ImageCaptureException) {
            ......
        }
​
        override fun onImageSaved(output: ImageCapture.OutputFileResults) {
           ......
        }
    }
)

完整拍照代码如下:

/**
 * 拍照
 */
private fun takePhoto() {
    // 校验是否有可用的相机拍摄器
    val imageCapture = imageCapture ?: return
    // 定义拍摄相片名称
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.CHINA)
        .format(System.currentTimeMillis())
​
    // 使用MediaStore操作相片文件
    val contentValues = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, name)
        put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
            put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
        }
    }
​
    // 指定输出参数
    val outputOptions = ImageCapture.OutputFileOptions
        .Builder(
            contentResolver,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues
        )
        .build()
​
    // 开始拍摄相片
    imageCapture.takePicture(
        outputOptions,
        ContextCompat.getMainExecutor(this),
        object : ImageCapture.OnImageSavedCallback {
            override fun onError(exc: ImageCaptureException) {
                // 拍摄失败
                val msg = "Photo capture failed: ${exc.message}"
                Log.e(TAG, msg, exc)
                Toast.makeText(this@CameraActivity, msg, Toast.LENGTH_SHORT).show()
            }
​
            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                // 拍摄成功,saveUri就是图片的uri地址
                val msg = "Photo capture succeeded: ${output.savedUri}"
                val intent = Intent()
                val bundle = Bundle()
                bundle.putString("picture_uri", output.savedUri.toString())
                intent.putExtra("result", bundle)
                this@CameraActivity.setResult(Activity.RESULT_OK, intent)
                this@CameraActivity.finish()
                Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                Log.d(TAG, msg)
            }
        }
    )
}

注:通过uri获取到对应照片后,如果要做显示或者上传都相关操作,个人建议进行适当的压缩处理,毕竟目前的大多数手机拍摄出来的照片质量都比较大,操作起来内存销毁或者网络、时间消耗都比较大。

最后

demo效果同Camera自定义相机一致,

image.png

至此,关于如何使用Jectpack CameraX自定义相机就介绍完了,关于更多定制化的需求可以根据自己的实际情况进行扩展,如需项目相关Demo可以前往个人码云仓库获取: