Android | 使用CameraX进行相机拍照并保存

写在前面

之前写了 Android | 使用CameraX进行相机预览 一文,今天我们来看看怎么使用 CameraX 进行拍照并保存,当然你还可以干其他事情~

关于权限和依赖

权限和依赖Android | 使用CameraX进行相机预览 一致,我把多的部分写在下面。

1
2
3
4
5
6
7

//我们的权限请求就是依赖这个库的,低版本的没有 registerForActivityResult 这个 API
implementation 'androidx.appcompat:appcompat:1.3.0-beta01'
//Glide图片加载库
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

XML布局部分

依旧在布局中添加 PreviewView 视图,不过此次我们添加了用于显示拍照后的ImageView、切换镜头方向的ImageView和拍照按钮,切换镜头的图片资源请自行在 阿里巴巴矢量图标库 下载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center"
android:orientation="horizontal">

<ImageView
android:id="@+id/ivPhotoPreView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="30dp" />

<Button
android:id="@+id/btnTakePhoto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="拍照" />

<ImageView
android:id="@+id/btnSwitchLens"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:layout_marginStart="30dp"
android:src="@drawable/ic_switch_lens" />

</LinearLayout>

</RelativeLayout>

在Activity / Fragment 中使用

TakePhotoFragment.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210

import android.Manifest
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import cn.cqautotest.im.R
import cn.cqautotest.im.base.BaseFragment
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.android.synthetic.main.take_photo_fragment.*
import java.io.File
import java.util.concurrent.ExecutionException
import kotlin.math.max
import kotlin.math.min

class TakePhotoFragment : BaseFragment() {

private lateinit var registry: ActivityResultLauncher<String>
private lateinit var mImgFile: File
private lateinit var preview: Preview
private lateinit var camera: Camera
private lateinit var imageCapture: ImageCapture

// 设备上的摄像头朝向与设备屏幕相反的方向。
private var lensFacing = CameraSelector.LENS_FACING_BACK

// 是否有拍照权限
private var hasCameraPermission = false

override fun getLayoutResId() = R.layout.take_photo_fragment

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 调用全部的 init 方法,
callAllInit()
// Google官方弃用 onActivityResult() 方法,推荐使用 registerForActivityResult() 方法获取返回结果
// 使用时需要依赖这个版本 implementation 'androidx.appcompat:appcompat:1.3.0-beta01'
// 最佳的做法是弹出 dialog 提示框进行询问是否授权,再做相应的操作
registry = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
hasCameraPermission = it
Log.d(TAG, "onViewCreated: hasCameraPermission ===> $hasCameraPermission")
// 有了相机权限才绑定相机,否则提示用户没有授予相机权限
if (hasCameraPermission) bindCamera() else noCameraPermission()
}.apply {
launch(Manifest.permission.CAMERA)
}
}

private fun noCameraPermission() {
Toast.makeText(context, "您请授予拍照和文件读写权限,否则无法使用该功能", Toast.LENGTH_SHORT).show()
}

override fun initData() {
val photoSavePath: String = mContext.cacheDir.path + "/" + "照骗.png"
mImgFile = File(photoSavePath)
}

override fun initEvent() {
btnTakePhoto.setOnClickListener {
Log.d(TAG, "onViewCreated: hasCameraPermission ===> $hasCameraPermission")
// 如果没有相机权限,则请求获取相机权限
if (hasCameraPermission.not()) {
requestCameraPermission()
} else {
// 拍照
takePhoto()
}
}
btnSwitchLens.setOnClickListener {
Log.d(TAG, "onViewCreated: hasCameraPermission ===> $hasCameraPermission")
// 如果没有相机权限,则请求获取相机权限
if (hasCameraPermission.not()) requestCameraPermission() else {
lensFacing =
if (lensFacing == CameraSelector.LENS_FACING_FRONT) CameraSelector.LENS_FACING_BACK
else CameraSelector.LENS_FACING_FRONT
// 重新绑定相机,因为相机的配置发生了改变
bindCamera()
}
}
}

/**
* 请求相机权限
*
* 向用户描述为什么需要该权限,如果用户拒绝则不申请权限,否则申请权限
*
*/
private fun requestCameraPermission() {
// TODO: 向用户描述为什么需要该权限,如果用户拒绝则不申请权限,否则申请权限
registry.launch(Manifest.permission.CAMERA)
}

/**
* 拍照
*/
private fun takePhoto() {
val metadata = ImageCapture.Metadata()
metadata.isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(mImgFile)
.setMetadata(metadata)
.build()
imageCapture.takePicture(outputFileOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {

/**
* 在此回调方法中保存拍摄的图片
*
* 仅当ImageCapture.OutputFileOptions
* 由使用#Builder(ContentResolver, Uri, ContentValues)构造的 MediaStore支持时,才返回此字段,
* 否则都会为 null。
*/
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
// 因为我们构造 ImageCapture.OutputFileOptions.Builder() 时使用的是 File 对象,
// 所以我们不能使用 onImageSaved() 方法传入的参数,不然一直都会为 null 。
// 要使用非 null 的 context 对象
// Glide默认是使用缓存的,要同时取消内存缓存和磁盘缓存,否则显示的图片会不更新
Glide.with(mContext)
.load(mImgFile.path)
// 不使用内存缓存
.skipMemoryCache(true)
// 不使用磁盘缓存
.diskCacheStrategy(DiskCacheStrategy.NONE)
// 将拍摄的照片剪裁成圆形
.circleCrop()
.into(ivPhotoPreView)
}

override fun onError(exception: ImageCaptureException) {
exception.printStackTrace()
}
})
}

/**
* 绑定相机
*/
private fun bindCamera() {
val cameraSelector: CameraSelector = CameraSelector.Builder()
// 面向镜头的有效值为{@link CameraSelectorLENS_FACING_FRONT}和{@link CameraSelectorLENS_FACING_BACK}。
// 如果已经设置了镜头朝向,这将增加对镜头朝向的额外要求,而不是替换之前的设置。
.requireLensFacing(lensFacing)
.build()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(mContext)
cameraProviderFuture.addListener({
initUseCase()
try {
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
preview.setSurfaceProvider(previewView.createSurfaceProvider(camera.cameraInfo))
} catch (e: ExecutionException) {
e.printStackTrace()
} catch (e: InterruptedException) {
e.printStackTrace()
}
}, ContextCompat.getMainExecutor(context))
}

/**
* 初始化相机
*/
private fun initUseCase() {
val ratio = getPreviewRatio()
val display = previewView.display
val rotation: Int = display.rotation
preview = Preview.Builder()
// 设置目标纵横比
.setTargetAspectRatio(ratio)
// 设置目标旋转
.setTargetRotation(rotation)
.build()
imageCapture = ImageCapture.Builder()
// 设置拍摄模式,可以设置照片的拍摄质量优先还是拍摄速度优先
.setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
// 设置目标纵横比
.setTargetAspectRatio(ratio)
// 设置目标旋转
.setTargetRotation(rotation)
.build()
}

/**
* 获取预览比例
*/
private fun getPreviewRatio(): Int {
val displayMetrics = DisplayMetrics()
previewView.display.getRealMetrics(displayMetrics)
val width = displayMetrics.widthPixels
val height = displayMetrics.heightPixels
val previewRatio = max(width, height).toDouble() / min(width, height)
return if (Math.abs(previewRatio - AspectRatio.RATIO_4_3) <= Math.abs(previewRatio - AspectRatio.RATIO_16_9)) {
AspectRatio.RATIO_4_3
} else AspectRatio.RATIO_16_9
}

companion object {
private const val TAG = "TakePhotoFragment"
}
}

封装 BaseFragment

BaseFragment.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment

abstract class BaseFragment : Fragment() {

lateinit var mActivity: AppCompatActivity
lateinit var mContext: Context

override fun onAttach(activity: Activity) {
super.onAttach(activity)
mActivity = activity as AppCompatActivity
}

override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
}

final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(getLayoutResId(), container, false)
onLayoutAfter()
return view
}

open fun getLayoutResId() = 0

open fun onLayoutAfter() {

}

open fun callAllInit() {
initView()
initData()
initEvent()
}

open fun initView() {

}

open fun initData(){

}

open fun initEvent() {

}
}

运行效果图

总结

本来以为写完了,结果测试取消授予拍照权限的时候搞了很久很久,还有就是 ImageCapture.OnImageSavedCallback 的回调方法 onImageSaved 中返回的 Uri 一直为null,通过查阅相关资料发现只有 ImageCapture.OutputFileOptions 由使用#Builder(ContentResolver, Uri, ContentValues)构造的 MediaStore 支持时才会有返回值,详请请参考 OutputFileResults returned by OnImageSavedCallback has an invalid Uri ,以上两个问题搞了一下午简直气到吐血。

不过还好,到最后问题还是解决啦~

请同学们点赞、评论、打赏+关注啦~


Android | 使用CameraX进行相机拍照并保存
https://blog.funkt.cn/2021/01/31/1355791131736100864/
作者
anjiemo
发布于
2021年1月31日
许可协议
未经作者授权,禁止转载