前言
上一篇简单介绍了自己使用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自定义相机一致,