Android CameraX适配Android13的踩坑之路
前言:
最近把AGP插件升级到8.1.0,新建项目的时候方针版别和编译版别都是33,发现之前的demo运用Camerax摄影和录像都失利了,于是查看了一下官网和各种材料,找到了Android13的适配计划.
行为改动:以 Android 13 或更高版别为方针渠道的运用
与前期版别一样,Android 13 包含一些行为改动,这些改动可能会影响您的运用。以下行为改动仅影响以 Android 13 或更高版别为方针渠道的运用。如果您的运用以 Android 13 或更高版别为方针渠道,您应该修改自己的运用以适当地支持这些行为(如果适用)。
此外,请必须查看对 Android 13 上运转的所有运用都有影响的行为改动列表。
1. 细化的媒体权限
该对话框的两个按钮,从上至下分别为“Allow”和“Don’t allow””
图 1. 您在恳求 READ_MEDIA_AUDIO
权限时向用户显现的体系权限对话框。
如果您的运用以 Android 13 或更高版别为方针渠道,而且需求访问其他运用现已创建的媒体文件,您必须恳求以下一项或多项细化的媒体权限,而不是READ_EXTERNAL_STORAGE
权限:
媒体类型 | 恳求权限 |
---|---|
图片和照片 | READ_MEDIA_IMAGES |
视频 | READ_MEDIA_VIDEO |
音频文件 | READ_MEDIA_AUDIO |
如果用户之前向您的运用授予了 READ_EXTERNAL_STORAGE
权限,体系会自意向您的运用授予细化的媒体权限。否则,当运用恳求上表中显现的任何权限时,体系会显现面向用户的对话框。在图 1 中,运用恳求 READ_MEDIA_AUDIO
权限。
如果您一同恳求 READ_MEDIA_IMAGES
权限和 READ_MEDIA_VIDEO
权限,体系只会显现一个体系权限对话框。
2.参阅材料如下:
developer.android.google.cn/about/versi…
blog.csdn.net/as425017946…
blog.csdn.net/guolin_blog…
3.依靠导入:
这儿的依靠都是根据AGP8.1.0,Android Studio的插件版别 Gifaffe 2022.3.1
3.1 添加统一的CameraX依靠装备:
在项目的gradle目录下新建libs.version.toml文件
3.2 添加CameraX依靠:
[versions]
agp = "8.1.0"
org-jetbrains-kotlin-android = "1.8.0"
core-ktx = "1.10.1"
junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
espresso-core = "3.5.1"
appcompat = "1.6.1"
material = "1.9.0"
constraintlayout = "2.1.4"
glide = "4.13.0"
glide-compiler = "4.13.0"
camerax = "1.1.0-beta03"
camerax-core = "1.1.0-beta03"
camerax-video = "1.1.0-beta03"
camerax-view = "1.1.0-beta03"
camerax-extensions = "1.1.0-beta03"
camerax-lifecycle = "1.1.0-beta03"
[libraries]
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version = "1.6.1" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
glide = {group = "com.github.bumptech.glide",name = "glide",version.ref = "glide"}
camerax = {group = "androidx.camera",name = "camera-camera2",version.ref = "camerax" }
camerax-core = {group = "androidx.camera",name = "camera-core",version.ref = "camerax-core"}
camerax-video = {group = "androidx.camera",name = "camera-video",version.ref = "camerax-video"}
camerax-view = {group = "androidx.camera",name = "camera-view",version.ref = "camerax-view"}
camerax-extensions = {group = "androidx.camera",name = "camera-extensions",version.ref = "camerax-extensions"}
camerax-lifecycle = {group = "androidx.camera",name = "camera-lifecycle",version.ref = "camerax-lifecycle"}
kotlin-stdlib = {group = "org.jetbrains.kotlin",name = "kotlin-stdlib-jdk7",version.ref = "kotlin-stdlib"}
kotlin-reflect = {group = "org.jetbrains.kotlin",name = "kotlin-reflect",version.ref = "kotlin-reflect"}
kotlinx-coroutines-core = {group = "org.jetbrains.kotlin",name = "kotlinx-coroutines-core",version.ref = "kotlinx-coroutines-core"}
kotlin-kotlinx-coroutines-android = {group = "org.jetbrains.kotlin",name = "kotlinx-coroutines-androidt",version.ref = "kotlinx-coroutines-android"}
glide-compiler = {group = "com.github.bumptech.glide",name = "compiler",version.ref = "glide-compiler"}
utilcodex = {group = "com.blankj",name = "utilcodex",version.ref = "utilcodex"}
3.3 在app的build.gradle目录下引入依靠:
dependencies {
implementation(libs.core.ktx)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.constraintlayout)
implementation(libs.glide)
implementation(libs.camerax)
implementation(libs.camerax.core)
implementation(libs.camerax.view)
implementation(libs.camerax.extensions)
implementation(libs.camerax.lifecycle)
implementation(libs.camerax.video)
implementation(libs.kotlin.stdlib)
implementation(libs.kotlin.reflect)
implementation(libs.utilcodex)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
annotationProcessor(libs.glide.compiler)
}
4.主界面布局文件如下:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.camera.view.PreviewView
android:id="@+id/mPreviewView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/btnCameraCapture"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginBottom="50dp"
android:background="@color/colorPrimaryDark"
android:text="摄影"
android:textColor="@color/white"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/btnVideo" />
<Button
android:id="@+id/btnVideo"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="10dp"
android:layout_marginBottom="50dp"
android:background="@color/colorPrimaryDark"
android:text="录像"
android:textColor="@color/white"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/btnCameraCapture"
app:layout_constraintRight_toLeftOf="@+id/btnSwitch" />
<Button
android:id="@+id/btnSwitch"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="10dp"
android:layout_marginBottom="50dp"
android:background="@color/colorPrimaryDark"
android:gravity="center"
android:text="切换镜头"
android:textColor="@color/white"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/btnVideo"
app:layout_constraintRight_toRightOf="parent" />
<Button
android:id="@+id/btnOpenCamera"
android:layout_width="200dp"
android:layout_height="50dp"
android:background="@color/colorPrimaryDark"
android:text="进入相机摄影界面"
android:textColor="@color/white"
android:textSize="16sp"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>
5.挑选相机界面布局如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Button
android:id="@+id/btnCamera"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="打开相机"
tools:ignore="MissingConstraints" />
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:background="@mipmap/ic_launcher"
app:layout_constraintLeft_toRightOf="@+id/btnCamera"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
6.Android13权限适配:
Android13相较于之前的版别变化很大,细化了权限恳求,详细的参阅上面的官网材料,直接上代码:
6.1 Android13之前的适配:
能够看到,API 32也便是Android 12及以下体系,咱们仍然声明的是READ_EXTERNAL_STORAGE权限。
6.2 Android13及以上的适配:
从Android 13开端,咱们就会运用READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO来代替了。
<!--存储图画或许视频权限-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<!--录制音频权限-->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
7.项目中简单适配:
7.1 Android13之前权限恳求如下:
@SuppressLint("RestrictedApi")
private fun initPermission() {
if (allPermissionsGranted()) {
// ImageCapture
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, Constants.REQUEST_CODE_PERMISSIONS
)
}
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == Constants.REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
ToastUtils.shortToast("请您打开必要权限")
}
}
}
7.2 Android13及以上的版别权限恳求适配如下:
private fun initPermission() {
if (checkPermissions()) {
// ImageCapture
startCamera()
} else {
requestPermission()
}
}
/**
* Android13查看权限进行了细化,每个需求单独申请,这儿我有摄影和录像,所以加入相机和录像权限
*
**/
private fun checkPermissions(): Boolean {
when {
Build.VERSION.SDK_INT >= 33 -> {
val permissions = arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_AUDIO,
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
)
for (permission in permissions) {
return Environment.isExternalStorageManager()
}
}
else -> {
for (permission in REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
}
}
return true
}
/**
* 用户回绝后恳求权限需求一同申请,刚开端我是单独申请的调试后发现一向报错,所以改为一同申请
*
**/
private fun requestPermission() {
when {
Build.VERSION.SDK_INT >= 33 -> {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_AUDIO,
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO),
Constants.REQUEST_CODE_PERMISSIONS
)
}
else -> {
ActivityCompat.requestPermissions(this,
REQUIRED_PERMISSIONS, Constants.REQUEST_CODE_PERMISSIONS)
}
}
}
/**
*用户恳求权限后的回调,这儿我是测验demo,所以用户回绝后我会重复恳求,真实项目根自己的需求来动态申请
*
**/
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
Constants.REQUEST_CODE_PERMISSIONS -> {
var allPermissionsGranted = true
for (result in grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allPermissionsGranted = false
break
}
}
when {
allPermissionsGranted -> {
// 权限已授予,履行文件读写操作
startCamera()
}
else -> {
// 权限被回绝,处理权限恳求失利的状况
ToastUtils.shortToast("请您打开必要权限")
requestPermission()
}
}
}
}
}
8.摄影和录像代码:
8.1 摄影:
/**
* 开端摄影
*/
private fun takePhoto() {
val imageCapture = imageCamera ?: return
val photoFile = createFile(outputDirectory, DATE_FORMAT, PHOTO_EXTENSION)
val metadata = ImageCapture.Metadata().apply {
// Mirror image when using the front camera
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}
val outputOptions =
ImageCapture.OutputFileOptions.Builder(photoFile).setMetadata(metadata).build()
imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
LogUtils.e(TAG, "Photo capture failed: ${exc.message}", exc)
ToastUtils.shortToast(" 摄影失利 ${exc.message}")
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
ToastUtils.shortToast(" 摄影成功 $savedUri")
LogUtils.e(TAG, savedUri.path.toString())
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(savedUri.toFile().extension)
MediaScannerConnection.scanFile(
this@MainActivity,
arrayOf(savedUri.toFile().absolutePath),
arrayOf(mimeType)
) { _, uri ->
LogUtils.d(
TAG,
"Image capture scanned into media store: ${uri.path.toString()}"
)
}
}
})
}
8.2 录像:
之前的版别很老还在测验阶段,所以官方api发生了一些改动:
旧的api示例如下:
/**
* 开端录像
*/
@SuppressLint("RestrictedApi", "ClickableViewAccessibility")
private fun takeVideo() {
isRecordVideo = true
val mFileDateFormat = SimpleDateFormat(DATE_FORMAT, Locale.US)
//视频保存途径
val file = File(FileManager.getCameraVideoPath(), mFileDateFormat.format(Date()) + ".mp4")
//开端录像
videoCapture?.startRecording(
file,
Executors.newSingleThreadExecutor(),
object : OnVideoSavedCallback {
override fun onVideoSaved(@NonNull file: File) {
isRecordVideo = false
LogUtils.d(TAG,"===视频保存的地址为=== ${file.absolutePath}")
//保存视频成功回调,会在中止录制时被调用
ToastUtils.shortToast(" 录像成功 $file")
}
override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
//保存失利的回调,可能在开端或完毕录制时被调用
isRecordVideo = false
LogUtils.e(TAG, "onError: $message")
ToastUtils.shortToast(" 录像失利 $message")
}
})
}
新的api示例如下:
startRecording方法参数发生了一些变化:第一个参数是传入一个文件输出信息类,之前是直接传入文件,其实影响不大
咱们经过val outputOptions = OutputFileOptions.Builder(file)这个类构建一个目标,然后在开端录像时传入即可.
/**
* 开端录像
*/
@SuppressLint("RestrictedApi", "ClickableViewAccessibility", "MissingPermission")
private fun takeVideo() {
//开端录像
try {
isRecordVideo = true
val mFileDateFormat = SimpleDateFormat(DATE_FORMAT, Locale.US)
//视频保存途径
val file =
File(FileManager.getCameraVideoPath(), mFileDateFormat.format(Date()) + ".mp4")
val outputOptions = OutputFileOptions.Builder(file)
videoCapture?.startRecording(
outputOptions.build(),
Executors.newSingleThreadExecutor(),
object : OnVideoSavedCallback {
override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
isRecordVideo = false
LogUtils.d(TAG, "===视频保存的地址为=== ${file.absolutePath}")
//保存视频成功回调,会在中止录制时被调用
ToastUtils.shortToast(" 录像成功 $file")
}
override fun onError(
videoCaptureError: Int,
message: String,
cause: Throwable?
) {
//保存失利的回调,可能在开端或完毕录制时被调用
isRecordVideo = false
LogUtils.e(TAG, "onError: $message")
ToastUtils.shortToast(" 录像失利 $message")
}
})
} catch (e: Exception) {
e.printStackTrace()
LogUtils.e(TAG, "===录像犯错===${e.message}")
}
}
8.3 中止录像:
这儿经过isRecordVideo是否正在录像进行录像和中止录像的操作
btnVideo.setOnClickListener {
if (!isRecordVideo) {
takeVideo()
isRecordVideo = true
btnVideo.text = "中止录像"
} else {
isRecordVideo = false
videoCapture?.stopRecording()//中止录制
//preview?.clear()//清除预览
btnVideo.text = "开端录像"
}
}
9.常量工具类:
object Constants {
const val REQUEST_CODE_PERMISSIONS = 101
const val REQUEST_CODE_CAMERA = 102
const val REQUEST_CODE_CROP = 103
const val DATE_FORMAT = "yyyy-MM-dd HH-mm-ss"
const val PHOTO_EXTENSION = ".jpg"
val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.RECORD_AUDIO
)
}
10.ToastUtils:
package com.example.cameraxdemo.utils
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
import android.view.Gravity
import android.widget.Toast
import androidx.annotation.StringRes
import com.example.cameraxdemo.app.CameraApp
import java.lang.reflect.Field
/**
*@author: njb
*@date: 2023/8/15 17:13
*@desc:
*/
object ToastUtils {
private const val TAG = "ToastUtil"
private var mToast: Toast? = null
private var sField_TN: Field? = null
private var sField_TN_Handler: Field? = null
private var sIsHookFieldInit = false
private const val FIELD_NAME_TN = "mTN"
private const val FIELD_NAME_HANDLER = "mHandler"
private fun showToast(
context: Context, text: CharSequence,
duration: Int, isShowCenterFlag: Boolean
) {
val toastRunnable = ToastRunnable(context, text, duration, isShowCenterFlag)
if (context is Activity) {
if (!context.isFinishing) {
context.runOnUiThread(toastRunnable)
}
} else {
val handler = Handler(context.mainLooper)
handler.post(toastRunnable)
}
}
fun shortToast(context: Context, text: CharSequence) {
showToast(context, text, Toast.LENGTH_SHORT, false)
}
fun longToast(context: Context, text: CharSequence) {
showToast(context, text, Toast.LENGTH_LONG, false)
}
fun shortToast(msg: String) {
showToast(CameraApp.mInstance, msg, Toast.LENGTH_SHORT, false)
}
fun shortToast(@StringRes resId: Int) {
showToast(
CameraApp.mInstance, CameraApp.mInstance.getText(resId),
Toast.LENGTH_SHORT, false
)
}
fun centerShortToast(msg: String) {
showToast(CameraApp.mInstance, msg, Toast.LENGTH_SHORT, true)
}
fun centerShortToast(@StringRes resId: Int) {
showToast(
CameraApp.mInstance, CameraApp.mInstance.getText(resId),
Toast.LENGTH_SHORT, true
)
}
fun cancelToast() {
val looper = Looper.getMainLooper()
if (looper.thread === Thread.currentThread()) {
mToast!!.cancel()
} else {
Handler(looper).post { mToast!!.cancel() }
}
}
@SuppressLint("SoonBlockedPrivateApi")
private fun hookToast(toast: Toast?) {
try {
if (!sIsHookFieldInit) {
sField_TN = Toast::class.java.getDeclaredField(FIELD_NAME_TN)
sField_TN?.run {
isAccessible = true
sField_TN_Handler = type.getDeclaredField(FIELD_NAME_HANDLER)
}
sField_TN_Handler?.isAccessible = true
sIsHookFieldInit = true
}
val tn = sField_TN!![toast]
val originHandler = sField_TN_Handler!![tn] as Handler
sField_TN_Handler!![tn] = SafelyHandlerWrapper(originHandler)
} catch (e: Exception) {
Log.e(TAG, "Hook toast exception=$e")
}
}
private class ToastRunnable(
private val context: Context,
private val text: CharSequence,
private val duration: Int,
private val isShowCenter: Boolean
) : Runnable {
@SuppressLint("ShowToast")
override fun run() {
if (mToast == null) {
mToast = Toast.makeText(context, text, duration)
} else {
mToast!!.setText(text)
if (isShowCenter) {
mToast!!.setGravity(Gravity.CENTER, 0, 0)
}
mToast!!.duration = duration
}
hookToast(mToast)
mToast!!.show()
}
}
private class SafelyHandlerWrapper(private val originHandler: Handler?) : Handler() {
override fun dispatchMessage(msg: Message) {
try {
super.dispatchMessage(msg)
} catch (e: Exception) {
Log.e(TAG, "Catch system toast exception:$e")
}
}
override fun handleMessage(msg: Message) {
originHandler?.handleMessage(msg)
}
}
}
11.FileManager:
package com.example.cameraxdemo.utils;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import com.example.cameraxdemo.app.CameraApp;
import java.io.File;
/**
* @author: njb
* @date: 2023/8/15 17:13
* @desc:
*/
public class FileManager {
// 媒体模块根目录
private static final String SAVE_MEDIA_ROOT_DIR = Environment.DIRECTORY_DCIM;
// 媒体模块存储途径
private static final String SAVE_MEDIA_DIR = SAVE_MEDIA_ROOT_DIR + "/CameraXApp";
private static final String AVATAR_DIR = "/avatar";
private static final String SAVE_MEDIA_VIDEO_DIR = SAVE_MEDIA_DIR + "/video";
private static final String SAVE_MEDIA_PHOTO_DIR = SAVE_MEDIA_DIR + "/photo";
// JPG后缀
public static final String JPG_SUFFIX = ".jpg";
// PNG后缀
public static final String PNG_SUFFIX = ".png";
// MP4后缀
public static final String MP4_SUFFIX = ".mp4";
// YUV后缀
public static final String YUV_SUFFIX = ".yuv";
// h264后缀
public static final String H264_SUFFIX = ".h264";
/**
* 保存图片到体系相册
*
* @param context
* @param file
*/
public static String saveImage(Context context, File file) {
ContentResolver localContentResolver = context.getContentResolver();
ContentValues localContentValues = getImageContentValues(context, file, System.currentTimeMillis());
localContentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, localContentValues);
Intent localIntent = new Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE");
final Uri localUri = Uri.fromFile(file);
localIntent.setData(localUri);
context.sendBroadcast(localIntent);
return file.getAbsolutePath();
}
public static ContentValues getImageContentValues(Context paramContext, File paramFile, long paramLong) {
ContentValues localContentValues = new ContentValues();
localContentValues.put("title", paramFile.getName());
localContentValues.put("_display_name", paramFile.getName());
localContentValues.put("mime_type", "image/jpeg");
localContentValues.put("datetaken", Long.valueOf(paramLong));
localContentValues.put("date_modified", Long.valueOf(paramLong));
localContentValues.put("date_added", Long.valueOf(paramLong));
localContentValues.put("orientation", Integer.valueOf(0));
localContentValues.put("_data", paramFile.getAbsolutePath());
localContentValues.put("_size", Long.valueOf(paramFile.length()));
return localContentValues;
}
/**
* 获取App存储根目录
*/
public static String getAppRootDir() {
String path = getStorageRootDir();
FileUtil.createOrExistsDir(path);
return path;
}
/**
* 获取文件存储根目录
*/
public static String getStorageRootDir() {
File filePath = CameraApp.Companion.getMInstance().getExternalFilesDir("");
String path;
if (filePath != null) {
path = filePath.getAbsolutePath();
} else {
path = CameraApp.Companion.getMInstance().getFilesDir().getAbsolutePath();
}
return path;
}
/**
* 图片地址
*/
public static String getCameraPhotoPath() {
return getFolderDirPath(SAVE_MEDIA_PHOTO_DIR);
}
/**
* 获取摄影一般图片文件
*/
public static File getSavedPictureFile(long timeStamp) {
String fileName = "image"+ "_"+ + timeStamp + JPG_SUFFIX;
return new File(getCameraPhotoPath(), fileName);
}
/**
* 头像地址
*/
public static String getAvatarPath(String fileName) {
String path;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
path = getFolderDirPath(SAVE_MEDIA_DIR + AVATAR_DIR);
} else {
path = getSaveDir(AVATAR_DIR);
}
return path + File.separator + fileName;
}
/**
* 视频地址
*/
public static String getCameraVideoPath() {
return getFolderDirPath(SAVE_MEDIA_VIDEO_DIR);
}
public static String getFolderDirPath(String dstDirPathToCreate) {
File dstFileDir = new File(Environment.getExternalStorageDirectory(), dstDirPathToCreate);
if (!dstFileDir.exists() && !dstFileDir.mkdirs()) {
Log.e("Failed to create file", dstDirPathToCreate);
return null;
}
return dstFileDir.getAbsolutePath();
}
/**
* 获取详细模块存储目录
*/
public static String getSaveDir(@NonNull String directory) {
String path = "";
if (TextUtils.isEmpty(directory) || "/".equals(directory)) {
path = "";
} else if (directory.startsWith("/")) {
path = directory;
} else {
path = "/" + directory;
}
path = getAppRootDir() + path;
FileUtil.createOrExistsDir(path);
return path;
}
/**
* 经过媒体文件Uri获取文件-Android 11兼容
*
* @param fileUri 文件Uri
*/
public static File getMediaUri2File(Uri fileUri) {
String[] projection = {MediaStore.Images.Media.DATA};
Cursor cursor = CameraApp.Companion.getMInstance().getContentResolver().query(fileUri, projection,
null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
String path = cursor.getString(columnIndex);
cursor.close();
return new File(path);
}
}
return null;
}
/**
* 根据Uri获取图片绝对途径,处理Android4.4以上版别Uri转换
*
* @param context 上下文
* @param imageUri 图片地址
*/
public static String getImageAbsolutePath(Activity context, Uri imageUri) {
if (context == null || imageUri == null)
return null;
if (DocumentsContract.isDocumentUri(context, imageUri)) {
if (isExternalStorageDocument(imageUri)) {
String docId = DocumentsContract.getDocumentId(imageUri);
String[] split = docId.split(":");
String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
} else if (isDownloadsDocument(imageUri)) {
String id = DocumentsContract.getDocumentId(imageUri);
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.parseLong(id));
return getDataColumn(context, contentUri, null, null);
} else if (isMediaDocument(imageUri)) {
String docId = DocumentsContract.getDocumentId(imageUri);
String[] split = docId.split(":");
String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
String selection = MediaStore.Images.Media._ID + "=?";
String[] selectionArgs = new String[]{split[1]};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
} // MediaStore (and general)
else if ("content".equalsIgnoreCase(imageUri.getScheme())) {
// Return the remote address
if (isGooglePhotosUri(imageUri))
return imageUri.getLastPathSegment();
return getDataColumn(context, imageUri, null, null);
}
// File
else if ("file".equalsIgnoreCase(imageUri.getScheme())) {
return imageUri.getPath();
}
return null;
}
/**
* @param uri The Uri to checkRemote.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to checkRemote.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to checkRemote.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to checkRemote.
* @return Whether the Uri authority is Google Photos.
*/
public static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}
public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
String column = MediaStore.Images.Media.DATA;
String[] projection = {column};
try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) {
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
}
return null;
}
}
12.VideoFileUtils
package com.example.cameraxdemo.utils
import android.content.Context
import android.os.Environment
import com.example.cameraxdemo.R
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
/**
*@author: njb
*@date: 2023/8/15 17:13
*@desc:
*/
object VideoFileUtils {
/**
* 获取视频文件途径
*/
fun getVideoName(): String {
val videoPath = Environment.getExternalStorageDirectory().toString() + "/CameraXApp"
val dir = File(videoPath)
if (!dir.exists() && !dir.mkdirs()) {
ToastUtils.shortToast("文件不存在")
}
return videoPath
}
/**
* 获取图片文件途径
*/
fun getImageFileName(): String {
val imagePath = Environment.getExternalStorageDirectory().toString() + "/images"
val dir = File(imagePath)
if (!dir.exists() && !dir.mkdirs()) {
ToastUtils.shortToast("文件不存在")
}
return imagePath
}
/**
* 摄影文件保存途径
* @param context
* @return
*/
fun getPhotoDir(context: Context?): String? {
return FileManager.getFolderDirPath(
"DCIM/Camera/CameraXApp/photo"
)
}
/**
* 视频文件保存途径
* @param context
* @return
*/
fun getVideoDir(): String? {
return FileManager.getFolderDirPath(
"DCIM/Camera/CameraXApp/video"
)
}
/** Use external media if it is available, our app's file directory otherwise */
fun getOutputDirectory(context: Context): File {
val appContext = context.applicationContext
val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() }
}
return if (mediaDir != null && mediaDir.exists())
mediaDir else appContext.filesDir
}
fun createFile(baseFolder: File, format: String, extension: String) =
File(
baseFolder, SimpleDateFormat(format, Locale.US)
.format(System.currentTimeMillis()) + extension
)
}
13.CameraApp:
package com.example.cameraxdemo.app
import android.app.Application
/**
*@author: njb
*@date: 2023/8/15 17:07
*@desc:
*/
class CameraApp : Application() {
override fun onCreate() {
super.onCreate()
mInstance = this
}
companion object {
lateinit var mInstance: CameraApp
private set
}
}
14.完好的示例代码如下:
package com.example.cameraxdemo
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.webkit.MimeTypeMap
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.AspectRatio
import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.core.VideoCapture
import androidx.camera.core.VideoCapture.OnVideoSavedCallback
import androidx.camera.core.VideoCapture.OutputFileOptions
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import com.blankj.utilcode.util.LogUtils
import com.example.cameraxdemo.activity.CameraActivity
import com.example.cameraxdemo.utils.Constants
import com.example.cameraxdemo.utils.Constants.Companion.DATE_FORMAT
import com.example.cameraxdemo.utils.Constants.Companion.PHOTO_EXTENSION
import com.example.cameraxdemo.utils.Constants.Companion.REQUIRED_PERMISSIONS
import com.example.cameraxdemo.utils.FileManager
import com.example.cameraxdemo.utils.ToastUtils
import com.example.cameraxdemo.utils.VideoFileUtils.createFile
import com.example.cameraxdemo.utils.VideoFileUtils.getOutputDirectory
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() {
private var imageCamera: ImageCapture? = null
private lateinit var cameraExecutor: ExecutorService
private var videoCapture: VideoCapture? = null
private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA//当前相机
private var preview: Preview? = null//预览目标
private var cameraProvider: ProcessCameraProvider? = null//相机信息
private lateinit var camera: Camera //相机目标
private var isRecordVideo: Boolean = false
private val TAG = "CameraXApp"
private lateinit var outputDirectory: File
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
private val btnCameraCapture: Button by lazy { findViewById(R.id.btnCameraCapture) }
private val btnVideo: Button by lazy { findViewById(R.id.btnVideo) }
private val btnSwitch: Button by lazy { findViewById(R.id.btnSwitch) }
private val btnOpenCamera: Button by lazy { findViewById(R.id.btnOpenCamera) }
private val viewFinder: PreviewView by lazy { findViewById(R.id.mPreviewView) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initPermission()
initView()
}
private fun initView() {
outputDirectory = getOutputDirectory(this)
}
@SuppressLint("RestrictedApi")
private fun initListener() {
btnCameraCapture.setOnClickListener {
takePhoto()
}
btnVideo.setOnClickListener {
if (!isRecordVideo) {
takeVideo()
isRecordVideo = true
btnVideo.text = "中止录像"
} else {
isRecordVideo = false
videoCapture?.stopRecording()//中止录制
//preview?.clear()//清除预览
btnVideo.text = "开端录像"
}
}
btnSwitch.setOnClickListener {
cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
CameraSelector.DEFAULT_FRONT_CAMERA
} else {
CameraSelector.DEFAULT_BACK_CAMERA
}
if (!isRecordVideo) {
startCamera()
}
}
btnOpenCamera.setOnClickListener {
val intent = Intent(this, CameraActivity::class.java)
startActivity(intent)
}
}
private fun initPermission() {
if (checkPermissions()) {
// ImageCapture
startCamera()
} else {
requestPermission()
}
}
private fun requestPermission() {
when {
Build.VERSION.SDK_INT >= 33 -> {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.READ_MEDIA_IMAGES,Manifest.permission.READ_MEDIA_AUDIO,Manifest.permission.READ_MEDIA_VIDEO,Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO),
Constants.REQUEST_CODE_PERMISSIONS
)
}
else -> {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, Constants.REQUEST_CODE_PERMISSIONS)
}
}
}
/**
* 开端摄影
*/
private fun takePhoto() {
val imageCapture = imageCamera ?: return
val photoFile = createFile(outputDirectory, DATE_FORMAT, PHOTO_EXTENSION)
val metadata = ImageCapture.Metadata().apply {
// Mirror image when using the front camera
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
}
val outputOptions =
ImageCapture.OutputFileOptions.Builder(photoFile).setMetadata(metadata).build()
imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
LogUtils.e(TAG, "Photo capture failed: ${exc.message}", exc)
ToastUtils.shortToast(" 摄影失利 ${exc.message}")
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
ToastUtils.shortToast(" 摄影成功 $savedUri")
LogUtils.e(TAG, savedUri.path.toString())
val mimeType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(savedUri.toFile().extension)
MediaScannerConnection.scanFile(
this@MainActivity,
arrayOf(savedUri.toFile().absolutePath),
arrayOf(mimeType)
) { _, uri ->
LogUtils.d(
TAG,
"Image capture scanned into media store: ${uri.path.toString()}"
)
}
}
})
}
/**
* 开端录像
*/
@SuppressLint("RestrictedApi", "ClickableViewAccessibility", "MissingPermission")
private fun takeVideo() {
//开端录像
try {
isRecordVideo = true
val mFileDateFormat = SimpleDateFormat(DATE_FORMAT, Locale.US)
//视频保存途径
val file =
File(FileManager.getCameraVideoPath(), mFileDateFormat.format(Date()) + ".mp4")
val outputOptions = OutputFileOptions.Builder(file)
videoCapture?.startRecording(
outputOptions.build(),
Executors.newSingleThreadExecutor(),
object : OnVideoSavedCallback {
override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
isRecordVideo = false
LogUtils.d(TAG, "===视频保存的地址为=== ${file.absolutePath}")
//保存视频成功回调,会在中止录制时被调用
ToastUtils.shortToast(" 录像成功 $file")
}
override fun onError(
videoCaptureError: Int,
message: String,
cause: Throwable?
) {
//保存失利的回调,可能在开端或完毕录制时被调用
isRecordVideo = false
LogUtils.e(TAG, "onError: $message")
ToastUtils.shortToast(" 录像失利 $message")
}
})
} catch (e: Exception) {
e.printStackTrace()
LogUtils.e(TAG, "===录像犯错===${e.message}")
}
}
/**
* 开端相机预览
*/
@SuppressLint("RestrictedApi")
private fun startCamera() {
cameraExecutor = Executors.newSingleThreadExecutor()
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
cameraProvider = cameraProviderFuture.get()//获取相机信息
//预览装备
preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
imageCamera = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.build()
videoCapture = VideoCapture.Builder()//录像用例装备
.setTargetAspectRatio(AspectRatio.RATIO_16_9) //设置高宽比
//.setTargetRotation(viewFinder.display!!.rotation)//设置旋转视点
.build()
try {
cameraProvider?.unbindAll()//先解绑所有用例
camera = cameraProvider?.bindToLifecycle(
this,
cameraSelector,
preview,
imageCamera,
videoCapture
)!!//绑定用例
} catch (e: Exception) {
LogUtils.e(TAG, "Use case binding failed", e.message)
}
}, ContextCompat.getMainExecutor(this))
initListener()
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
Constants.REQUEST_CODE_PERMISSIONS -> {
var allPermissionsGranted = true
for (result in grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allPermissionsGranted = false
break
}
}
when {
allPermissionsGranted -> {
// 权限已授予,履行文件读写操作
startCamera()
}
else -> {
// 权限被回绝,处理权限恳求失利的状况
ToastUtils.shortToast("请您打开必要权限")
requestPermission()
}
}
}
}
}
private fun checkPermissions(): Boolean {
when {
Build.VERSION.SDK_INT >= 33 -> {
val permissions = arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_AUDIO,
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
)
for (permission in permissions) {
return Environment.isExternalStorageManager()
}
}
else -> {
for (permission in REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
}
}
return true
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
}
15.实现的作用如下:
16.日志打印:
模拟器日志如下:
真机日志打印:
17.挑选相册的代码如下:
package com.example.cameraxdemo.activity
import androidx.appcompat.app.AppCompatActivity
import android.content.ContentValues
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Button
import android.widget.ImageView
import com.blankj.utilcode.util.LogUtils
import com.bumptech.glide.Glide
import com.example.cameraxdemo.R
import com.example.cameraxdemo.utils.Constants.Companion.REQUEST_CODE_CAMERA
import com.example.cameraxdemo.utils.Constants.Companion.REQUEST_CODE_CROP
import com.example.cameraxdemo.utils.FileManager
import com.example.cameraxdemo.utils.FileUtil
import java.io.File
/**
*@author: njb
*@date: 2023/8/15 17:20
*@desc:
*/
class CameraActivity :AppCompatActivity(){
private var mUploadImageUri: Uri? = null
private var mUploadImageFile: File? = null
private var photoUri: Uri? = null
private val btnCamera:Button by lazy { findViewById(R.id.btnCamera) }
private val ivAvatar:ImageView by lazy { findViewById(R.id.iv_avatar) }
private val TAG = CameraActivity::class.java.name
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera)
initView()
}
private fun initView() {
btnCamera.setOnClickListener {
startSystemCamera()
}
}
/**
* 调起体系相机摄影
*/
private fun startSystemCamera() {
val takeIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
val values = ContentValues()
//根据uri查询图片地址
photoUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
LogUtils.d(TAG, "photoUri:" + photoUri?.authority + ",photoUri:" + photoUri?.path)
//放入摄影后的地址
takeIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
//调起摄影
startActivityForResult(
takeIntent,
REQUEST_CODE_CAMERA
)
}
/**
* 设置用户头像
*/
private fun setAvatar() {
val file: File? = if (mUploadImageUri != null) {
FileManager.getMediaUri2File(mUploadImageUri)
} else {
mUploadImageFile
}
Glide.with(this).load(file).into(ivAvatar)
LogUtils.d(TAG,"filepath"+ file!!.absolutePath)
}
/**
* 体系裁剪方法
*/
private fun workCropFun(imgPathUri: Uri?) {
mUploadImageUri = null
mUploadImageFile = null
if (imgPathUri != null) {
val imageObject: Any = FileUtil.getHeadJpgFile()
if (imageObject is Uri) {
mUploadImageUri = imageObject
}
if (imageObject is File) {
mUploadImageFile = imageObject
}
val intent = Intent("com.android.camera.action.CROP")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
intent.run {
setDataAndType(imgPathUri, "image/*")// 图片资源
putExtra("crop", "true") // 裁剪
putExtra("aspectX", 1) // 宽度比
putExtra("aspectY", 1) // 高度比
putExtra("outputX", 150) // 裁剪框宽度
putExtra("outputY", 150) // 裁剪框高度
putExtra("scale", true) // 缩放
putExtra("return-data", false) // true-回来缩略图-data,false-不回来-需求经过Uri
putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) // 保存的图片格式
putExtra("noFaceDetection", true) // 取消人脸识别
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putExtra(MediaStore.EXTRA_OUTPUT, mUploadImageUri)
} else {
val imgCropUri = Uri.fromFile(mUploadImageFile)
putExtra(MediaStore.EXTRA_OUTPUT, imgCropUri)
}
}
startActivityForResult(
intent, REQUEST_CODE_CROP
)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_CODE_CAMERA) {//摄影回调
workCropFun(photoUri)
} else if (requestCode == REQUEST_CODE_CROP) {//裁剪回调
setAvatar()
}
}
}
}
18.挑选相册后的作用如下:
19.总结:
今天虽然遇到了不少问题,可是出现问题后调试的过程很闲适,基本找出原因和处理花费了将近3个小时才把完好的demo整理出来,花费这么久的时间有三点原因:
1.Android13适配的规则没有搞清楚就直接升级更换了新版别,导致恳求和回绝后一向犯错。
2.CameraX新的api和录像权限没看完好导致这儿来回折腾了很久,最终找到了官网api才处理。
3.关于AGP8.1.0运用不熟悉,导致刚开端装备依靠也浪费了一点时间。
“路漫漫其修远兮,吾将上下而求索”,后面会把整理出来的完好适配Android13的例子也放出来,还有关于AGP8.1.0装备依靠方法改动的也会整理,遇到问题进行求索的过程比结果更重要,只要有一颗战胜困难的心,信任问题最终都会处理,如果感兴趣的能够尝试一下,若有其他问题能够提出来一同评论处理,一同学习,一同成长.
20.demo源码地址如下:
gitee.com/jackning_ad…