写在前面
之前写了 Android | 使用CameraX进行相机预览 一文,今天我们来看看怎么使用 CameraX 进行拍照并保存,当然你还可以干其他事情~
关于权限和依赖
权限和依赖 与 Android | 使用CameraX进行相机预览 一致,我把多的部分写在下面。
//我们的权限请求就是依赖这个库的,低版本的没有 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和拍照按钮,切换镜头的图片资源请自行在 阿里巴巴矢量图标库 下载。
<?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
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
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 ,以上两个问题搞了一下午简直气到吐血。
不过还好,到最后问题还是解决啦~
请同学们点赞、评论、打赏+关注啦~
本文由
A lonely cat
原创发布于
阳光沙滩
,未经作者授权,禁止转载