当前位置:   article > 正文

Android CameraX适配Android13的踩坑之路

camerax

6b6156f7632fd2a64bfb9141a3d61455.jpeg

前言:

最近把AGP插件升级到8.1.0,新建项目的时候目标版本和编译版本都是33,发现之前的demo使用Camerax拍照和录像都失败了,于是查看了一下官网和各种资料,找到了Android13的适配方案.

  1. 作者:一笑的小酒馆
  2. 链接:https://juejin.cn/post/7267840969605382198

行为变更:以 Android 13 或更高版本为目标平台的应用

与早期版本一样,Android 13 包含一些行为变更,这些变更可能会影响您的应用。以下行为变更仅影响以 Android 13 或更高版本为目标平台的应用。如果您的应用以 Android 13 或更高版本为目标平台,您应该修改自己的应用以适当地支持这些行为(如果适用)。

此外,请务必查看对 Android 13 上运行的所有应用都有影响的行为变更列表。

1. 细化的媒体权限

d6c2106584ba6fea9bd8229d46d2a06b.jpeg

对话框的两个按钮,从上至下分别为“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.参考资料如下:

https://developer.android.google.cn/about/versions/13/features?hl=zh-cn

https://blog.csdn.net/as425017946/article/details/127530660

https://blog.csdn.net/guolin_blog/article/details/127024559

3.依赖导入:

这里的依赖都是基于AGP8.1.0,Android Studio的插件版本 Gifaffe 2022.3.1

3.1 添加统一的CameraX依赖配置:

在项目的gradle目录下新建libs.version.toml文件

7725c0ea94d7aab5106c45b7fc27a008.jpeg

3.2 添加CameraX依赖:

  1. [versions]
  2. agp = "8.1.0"
  3. org-jetbrains-kotlin-android = "1.8.0"
  4. core-ktx = "1.10.1"
  5. junit = "4.13.2"
  6. androidx-test-ext-junit = "1.1.5"
  7. espresso-core = "3.5.1"
  8. appcompat = "1.6.1"
  9. material = "1.9.0"
  10. constraintlayout = "2.1.4"
  11. glide = "4.13.0"
  12. glide-compiler = "4.13.0"
  13. camerax = "1.1.0-beta03"
  14. camerax-core = "1.1.0-beta03"
  15. camerax-video = "1.1.0-beta03"
  16. camerax-view = "1.1.0-beta03"
  17. camerax-extensions = "1.1.0-beta03"
  18. camerax-lifecycle = "1.1.0-beta03"
  19. [libraries]
  20. core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
  21. junit = { group = "junit", name = "junit", version.ref = "junit" }
  22. androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
  23. espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
  24. appcompat = { group = "androidx.appcompat", name = "appcompat", version = "1.6.1" }
  25. material = { group = "com.google.android.material", name = "material", version.ref = "material" }
  26. constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
  27. glide = {group = "com.github.bumptech.glide",name = "glide",version.ref = "glide"}
  28. camerax = {group = "androidx.camera",name = "camera-camera2",version.ref = "camerax" }
  29. camerax-core = {group = "androidx.camera",name = "camera-core",version.ref = "camerax-core"}
  30. camerax-video = {group = "androidx.camera",name = "camera-video",version.ref = "camerax-video"}
  31. camerax-view = {group = "androidx.camera",name = "camera-view",version.ref = "camerax-view"}
  32. camerax-extensions = {group = "androidx.camera",name = "camera-extensions",version.ref = "camerax-extensions"}
  33. camerax-lifecycle = {group = "androidx.camera",name = "camera-lifecycle",version.ref = "camerax-lifecycle"}
  34. kotlin-stdlib = {group = "org.jetbrains.kotlin",name = "kotlin-stdlib-jdk7",version.ref = "kotlin-stdlib"}
  35. kotlin-reflect = {group = "org.jetbrains.kotlin",name = "kotlin-reflect",version.ref = "kotlin-reflect"}
  36. kotlinx-coroutines-core = {group = "org.jetbrains.kotlin",name = "kotlinx-coroutines-core",version.ref = "kotlinx-coroutines-core"}
  37. kotlin-kotlinx-coroutines-android = {group = "org.jetbrains.kotlin",name = "kotlinx-coroutines-androidt",version.ref = "kotlinx-coroutines-android"}
  38. glide-compiler = {group = "com.github.bumptech.glide",name = "compiler",version.ref = "glide-compiler"}
  39. utilcodex = {group = "com.blankj",name = "utilcodex",version.ref = "utilcodex"}

3.3 在app的build.gradle目录下引入依赖:

  1. dependencies {
  2. implementation(libs.core.ktx)
  3. implementation(libs.appcompat)
  4. implementation(libs.material)
  5. implementation(libs.constraintlayout)
  6. implementation(libs.glide)
  7. implementation(libs.camerax)
  8. implementation(libs.camerax.core)
  9. implementation(libs.camerax.view)
  10. implementation(libs.camerax.extensions)
  11. implementation(libs.camerax.lifecycle)
  12. implementation(libs.camerax.video)
  13. implementation(libs.kotlin.stdlib)
  14. implementation(libs.kotlin.reflect)
  15. implementation(libs.utilcodex)
  16. testImplementation(libs.junit)
  17. androidTestImplementation(libs.androidx.test.ext.junit)
  18. androidTestImplementation(libs.espresso.core)
  19. annotationProcessor(libs.glide.compiler)
  20. }

2050e5d372a0a8e07f79e9e29f3c905f.jpeg

4.主界面布局文件如下:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:app="http://schemas.android.com/apk/res-auto"
  4. xmlns:tools="http://schemas.android.com/tools"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. tools:context=".MainActivity">
  8. <androidx.camera.view.PreviewView
  9. android:id="@+id/mPreviewView"
  10. android:layout_width="0dp"
  11. android:layout_height="0dp"
  12. app:layout_constraintBottom_toBottomOf="parent"
  13. app:layout_constraintLeft_toLeftOf="parent"
  14. app:layout_constraintRight_toRightOf="parent"
  15. app:layout_constraintTop_toTopOf="parent" />
  16. <Button
  17. android:id="@+id/btnCameraCapture"
  18. android:layout_width="0dp"
  19. android:layout_height="50dp"
  20. android:layout_marginBottom="50dp"
  21. android:background="@color/colorPrimaryDark"
  22. android:text="拍照"
  23. android:textColor="@color/white"
  24. android:textSize="16sp"
  25. app:layout_constraintBottom_toBottomOf="parent"
  26. app:layout_constraintLeft_toLeftOf="parent"
  27. app:layout_constraintRight_toLeftOf="@+id/btnVideo" />
  28. <Button
  29. android:id="@+id/btnVideo"
  30. android:layout_width="0dp"
  31. android:layout_height="50dp"
  32. android:layout_marginStart="10dp"
  33. android:layout_marginBottom="50dp"
  34. android:background="@color/colorPrimaryDark"
  35. android:text="录像"
  36. android:textColor="@color/white"
  37. android:textSize="16sp"
  38. app:layout_constraintBottom_toBottomOf="parent"
  39. app:layout_constraintLeft_toRightOf="@+id/btnCameraCapture"
  40. app:layout_constraintRight_toLeftOf="@+id/btnSwitch" />
  41. <Button
  42. android:id="@+id/btnSwitch"
  43. android:layout_width="0dp"
  44. android:layout_height="50dp"
  45. android:layout_marginStart="10dp"
  46. android:layout_marginBottom="50dp"
  47. android:background="@color/colorPrimaryDark"
  48. android:gravity="center"
  49. android:text="切换镜头"
  50. android:textColor="@color/white"
  51. android:textSize="16sp"
  52. app:layout_constraintBottom_toBottomOf="parent"
  53. app:layout_constraintLeft_toRightOf="@+id/btnVideo"
  54. app:layout_constraintRight_toRightOf="parent" />
  55. <Button
  56. android:id="@+id/btnOpenCamera"
  57. android:layout_width="200dp"
  58. android:layout_height="50dp"
  59. android:background="@color/colorPrimaryDark"
  60. android:text="进入相机拍照界面"
  61. android:textColor="@color/white"
  62. android:textSize="16sp"
  63. tools:ignore="MissingConstraints" />
  64. </androidx.constraintlayout.widget.ConstraintLayout>

5.选择相机界面布局如下:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent"
  6. xmlns:app="http://schemas.android.com/apk/res-auto">
  7. <Button
  8. android:id="@+id/btnCamera"
  9. android:layout_width="0dp"
  10. android:layout_height="wrap_content"
  11. android:text="打开相机"
  12. tools:ignore="MissingConstraints" />
  13. <ImageView
  14. android:id="@+id/iv_avatar"
  15. android:layout_width="100dp"
  16. android:layout_height="100dp"
  17. android:layout_marginStart="20dp"
  18. android:layout_marginTop="20dp"
  19. android:background="@mipmap/ic_launcher"
  20. app:layout_constraintLeft_toRightOf="@+id/btnCamera"
  21. app:layout_constraintTop_toTopOf="parent" />
  22. </androidx.constraintlayout.widget.ConstraintLayout>

6.Android13权限适配:

Android13相较于之前的版本变化很大,细化了权限请求,具体的参考上面的官网资料,直接上代码:

6.1 Android13之前的适配:

可以看到,API 32也就是Android 12及以下系统,我们仍然声明的是READ_EXTERNAL_STORAGE权限。

d7e9e43ebbc12f34768bf1f02ee5a7b0.jpeg

6.2 Android13及以上的适配:

从Android 13开始,我们就会使用READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO来替代了。

  1. <!--存储图像或者视频权限-->
  2. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
  3. android:maxSdkVersion="32" />
  4. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
  5. tools:ignore="ScopedStorage" android:maxSdkVersion="32"/>
  6. <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
  7. android:maxSdkVersion="32"
  8. tools:ignore="ScopedStorage" />
  9. <!--录制音频权限-->
  10. <uses-permission android:name="android.permission.RECORD_AUDIO"/>
  11. <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
  12. <uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
  13. <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
7.项目中简单适配:

7.1 Android13之前权限请求如下:

  1. @SuppressLint("RestrictedApi")
  2. private fun initPermission() {
  3. if (allPermissionsGranted()) {
  4. // ImageCapture
  5. startCamera()
  6. } else {
  7. ActivityCompat.requestPermissions(
  8. this, REQUIRED_PERMISSIONS, Constants.REQUEST_CODE_PERMISSIONS
  9. )
  10. }
  11. }
  12. private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
  13. ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
  14. }
  15. override fun onRequestPermissionsResult(
  16. requestCode: Int, permissions: Array<String>, grantResults:
  17. IntArray
  18. ) {
  19. super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  20. if (requestCode == Constants.REQUEST_CODE_PERMISSIONS) {
  21. if (allPermissionsGranted()) {
  22. startCamera()
  23. } else {
  24. ToastUtils.shortToast("请您打开必要权限")
  25. }
  26. }
  27. }
7.2 Android13及以上的版本权限请求适配如下:
  1. private fun initPermission() {
  2. if (checkPermissions()) {
  3. // ImageCapture
  4. startCamera()
  5. } else {
  6. requestPermission()
  7. }
  8. }
  9. /**
  10. * Android13检查权限进行了细化,每个需要单独申请,这里我有拍照和录像,所以加入相机和录像权限
  11. *
  12. **/
  13. private fun checkPermissions(): Boolean {
  14. when {
  15. Build.VERSION.SDK_INT >= 33 -> {
  16. val permissions = arrayOf(
  17. Manifest.permission.READ_MEDIA_IMAGES,
  18. Manifest.permission.READ_MEDIA_AUDIO,
  19. Manifest.permission.READ_MEDIA_VIDEO,
  20. Manifest.permission.CAMERA,
  21. Manifest.permission.RECORD_AUDIO,
  22. )
  23. for (permission in permissions) {
  24. return Environment.isExternalStorageManager()
  25. }
  26. }
  27. else -> {
  28. for (permission in REQUIRED_PERMISSIONS) {
  29. if (ContextCompat.checkSelfPermission(
  30. this,
  31. permission
  32. ) != PackageManager.PERMISSION_GRANTED
  33. ) {
  34. return false
  35. }
  36. }
  37. }
  38. }
  39. return true
  40. }
  41. /**
  42. * 用户拒绝后请求权限需要同时申请,刚开始我是单独申请的调试后发现一直报错,所以改为一起申请
  43. *
  44. **/
  45. private fun requestPermission() {
  46. when {
  47. Build.VERSION.SDK_INT >= 33 -> {
  48. ActivityCompat.requestPermissions(
  49. this,
  50. arrayOf(Manifest.permission.READ_MEDIA_IMAGES,
  51. Manifest.permission.READ_MEDIA_AUDIO,
  52. Manifest.permission.READ_MEDIA_VIDEO,
  53. Manifest.permission.CAMERA,
  54. Manifest.permission.RECORD_AUDIO),
  55. Constants.REQUEST_CODE_PERMISSIONS
  56. )
  57. }
  58. else -> {
  59. ActivityCompat.requestPermissions(this,
  60. REQUIRED_PERMISSIONS, Constants.REQUEST_CODE_PERMISSIONS)
  61. }
  62. }
  63. }
  64. /**
  65. *用户请求权限后的回调,这里我是测试demo,所以用户拒绝后我会重复请求,真实项目根自己的需求来动态申请
  66. *
  67. **/
  68. override fun onRequestPermissionsResult(
  69. requestCode: Int, permissions: Array<String>, grantResults:
  70. IntArray
  71. ) {
  72. super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  73. when (requestCode) {
  74. Constants.REQUEST_CODE_PERMISSIONS -> {
  75. var allPermissionsGranted = true
  76. for (result in grantResults) {
  77. if (result != PackageManager.PERMISSION_GRANTED) {
  78. allPermissionsGranted = false
  79. break
  80. }
  81. }
  82. when {
  83. allPermissionsGranted -> {
  84. // 权限已授予,执行文件读写操作
  85. startCamera()
  86. }
  87. else -> {
  88. // 权限被拒绝,处理权限请求失败的情况
  89. ToastUtils.shortToast("请您打开必要权限")
  90. requestPermission()
  91. }
  92. }
  93. }
  94. }
  95. }

8.拍照和录像代码:

8.1 拍照:

  1. /**
  2. * 开始拍照
  3. */
  4. private fun takePhoto() {
  5. val imageCapture = imageCamera ?: return
  6. val photoFile = createFile(outputDirectory, DATE_FORMAT, PHOTO_EXTENSION)
  7. val metadata = ImageCapture.Metadata().apply {
  8. // Mirror image when using the front camera
  9. isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
  10. }
  11. val outputOptions =
  12. ImageCapture.OutputFileOptions.Builder(photoFile).setMetadata(metadata).build()
  13. imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this),
  14. object : ImageCapture.OnImageSavedCallback {
  15. override fun onError(exc: ImageCaptureException) {
  16. LogUtils.e(TAG, "Photo capture failed: ${exc.message}", exc)
  17. ToastUtils.shortToast(" 拍照失败 ${exc.message}")
  18. }
  19. override fun onImageSaved(output: ImageCapture.OutputFileResults) {
  20. val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
  21. ToastUtils.shortToast(" 拍照成功 $savedUri")
  22. LogUtils.e(TAG, savedUri.path.toString())
  23. val mimeType = MimeTypeMap.getSingleton()
  24. .getMimeTypeFromExtension(savedUri.toFile().extension)
  25. MediaScannerConnection.scanFile(
  26. this@MainActivity,
  27. arrayOf(savedUri.toFile().absolutePath),
  28. arrayOf(mimeType)
  29. ) { _, uri ->
  30. LogUtils.d(
  31. TAG,
  32. "Image capture scanned into media store: ${uri.path.toString()}"
  33. )
  34. }
  35. }
  36. })
  37. }

8.2 录像:

之前的版本很老还在测试阶段,所以官方api发生了一些改变:

旧的api示例如下:

  1. /**
  2. * 开始录像
  3. */
  4. @SuppressLint("RestrictedApi", "ClickableViewAccessibility")
  5. private fun takeVideo() {
  6. isRecordVideo = true
  7. val mFileDateFormat = SimpleDateFormat(DATE_FORMAT, Locale.US)
  8. //视频保存路径
  9. val file = File(FileManager.getCameraVideoPath(), mFileDateFormat.format(Date()) + ".mp4")
  10. //开始录像
  11. videoCapture?.startRecording(
  12. file,
  13. Executors.newSingleThreadExecutor(),
  14. object : OnVideoSavedCallback {
  15. override fun onVideoSaved(@NonNull file: File) {
  16. isRecordVideo = false
  17. LogUtils.d(TAG,"===视频保存的地址为=== ${file.absolutePath}")
  18. //保存视频成功回调,会在停止录制时被调用
  19. ToastUtils.shortToast(" 录像成功 $file")
  20. }
  21. override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
  22. //保存失败的回调,可能在开始或结束录制时被调用
  23. isRecordVideo = false
  24. LogUtils.e(TAG, "onError: $message")
  25. ToastUtils.shortToast(" 录像失败 $message")
  26. }
  27. })
  28. }

新的api示例如下:

startRecording方法参数发生了一些变化:第一个参数是传入一个文件输出信息类,之前是直接传入文件,其实影响不大

我们通过val outputOptions = OutputFileOptions.Builder(file)这个类构建一个对象,然后在开始录像时传入即可.

  1. /**
  2. * 开始录像
  3. */
  4. @SuppressLint("RestrictedApi", "ClickableViewAccessibility", "MissingPermission")
  5. private fun takeVideo() {
  6. //开始录像
  7. try {
  8. isRecordVideo = true
  9. val mFileDateFormat = SimpleDateFormat(DATE_FORMAT, Locale.US)
  10. //视频保存路径
  11. val file =
  12. File(FileManager.getCameraVideoPath(), mFileDateFormat.format(Date()) + ".mp4")
  13. val outputOptions = OutputFileOptions.Builder(file)
  14. videoCapture?.startRecording(
  15. outputOptions.build(),
  16. Executors.newSingleThreadExecutor(),
  17. object : OnVideoSavedCallback {
  18. override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
  19. isRecordVideo = false
  20. LogUtils.d(TAG, "===视频保存的地址为=== ${file.absolutePath}")
  21. //保存视频成功回调,会在停止录制时被调用
  22. ToastUtils.shortToast(" 录像成功 $file")
  23. }
  24. override fun onError(
  25. videoCaptureError: Int,
  26. message: String,
  27. cause: Throwable?
  28. ) {
  29. //保存失败的回调,可能在开始或结束录制时被调用
  30. isRecordVideo = false
  31. LogUtils.e(TAG, "onError: $message")
  32. ToastUtils.shortToast(" 录像失败 $message")
  33. }
  34. })
  35. } catch (e: Exception) {
  36. e.printStackTrace()
  37. LogUtils.e(TAG, "===录像出错===${e.message}")
  38. }
  39. }

8.3 停止录像:

这里通过isRecordVideo是否正在录像进行录像和停止录像的操作

  1. btnVideo.setOnClickListener {
  2. if (!isRecordVideo) {
  3. takeVideo()
  4. isRecordVideo = true
  5. btnVideo.text = "停止录像"
  6. } else {
  7. isRecordVideo = false
  8. videoCapture?.stopRecording()//停止录制
  9. //preview?.clear()//清除预览
  10. btnVideo.text = "开始录像"
  11. }
  12. }

9.常量工具类:

  1. object Constants {
  2. const val REQUEST_CODE_PERMISSIONS = 101
  3. const val REQUEST_CODE_CAMERA = 102
  4. const val REQUEST_CODE_CROP = 103
  5. const val DATE_FORMAT = "yyyy-MM-dd HH-mm-ss"
  6. const val PHOTO_EXTENSION = ".jpg"
  7. val REQUIRED_PERMISSIONS = arrayOf(
  8. Manifest.permission.CAMERA,
  9. Manifest.permission.WRITE_EXTERNAL_STORAGE,
  10. Manifest.permission.READ_EXTERNAL_STORAGE,
  11. Manifest.permission.RECORD_AUDIO
  12. )
  13. }

10.ToastUtils:

  1. package com.example.cameraxdemo.utils
  2. import android.annotation.SuppressLint
  3. import android.app.Activity
  4. import android.content.Context
  5. import android.os.Handler
  6. import android.os.Looper
  7. import android.os.Message
  8. import android.util.Log
  9. import android.view.Gravity
  10. import android.widget.Toast
  11. import androidx.annotation.StringRes
  12. import com.example.cameraxdemo.app.CameraApp
  13. import java.lang.reflect.Field
  14. /**
  15. *@author: njb
  16. *@date: 2023/8/15 17:13
  17. *@desc:
  18. */
  19. object ToastUtils {
  20. private const val TAG = "ToastUtil"
  21. private var mToast: Toast? = null
  22. private var sField_TN: Field? = null
  23. private var sField_TN_Handler: Field? = null
  24. private var sIsHookFieldInit = false
  25. private const val FIELD_NAME_TN = "mTN"
  26. private const val FIELD_NAME_HANDLER = "mHandler"
  27. private fun showToast(
  28. context: Context, text: CharSequence,
  29. duration: Int, isShowCenterFlag: Boolean
  30. ) {
  31. val toastRunnable = ToastRunnable(context, text, duration, isShowCenterFlag)
  32. if (context is Activity) {
  33. if (!context.isFinishing) {
  34. context.runOnUiThread(toastRunnable)
  35. }
  36. } else {
  37. val handler = Handler(context.mainLooper)
  38. handler.post(toastRunnable)
  39. }
  40. }
  41. fun shortToast(context: Context, text: CharSequence) {
  42. showToast(context, text, Toast.LENGTH_SHORT, false)
  43. }
  44. fun longToast(context: Context, text: CharSequence) {
  45. showToast(context, text, Toast.LENGTH_LONG, false)
  46. }
  47. fun shortToast(msg: String) {
  48. showToast(CameraApp.mInstance, msg, Toast.LENGTH_SHORT, false)
  49. }
  50. fun shortToast(@StringRes resId: Int) {
  51. showToast(
  52. CameraApp.mInstance, CameraApp.mInstance.getText(resId),
  53. Toast.LENGTH_SHORT, false
  54. )
  55. }
  56. fun centerShortToast(msg: String) {
  57. showToast(CameraApp.mInstance, msg, Toast.LENGTH_SHORT, true)
  58. }
  59. fun centerShortToast(@StringRes resId: Int) {
  60. showToast(
  61. CameraApp.mInstance, CameraApp.mInstance.getText(resId),
  62. Toast.LENGTH_SHORT, true
  63. )
  64. }
  65. fun cancelToast() {
  66. val looper = Looper.getMainLooper()
  67. if (looper.thread === Thread.currentThread()) {
  68. mToast!!.cancel()
  69. } else {
  70. Handler(looper).post { mToast!!.cancel() }
  71. }
  72. }
  73. @SuppressLint("SoonBlockedPrivateApi")
  74. private fun hookToast(toast: Toast?) {
  75. try {
  76. if (!sIsHookFieldInit) {
  77. sField_TN = Toast::class.java.getDeclaredField(FIELD_NAME_TN)
  78. sField_TN?.run {
  79. isAccessible = true
  80. sField_TN_Handler = type.getDeclaredField(FIELD_NAME_HANDLER)
  81. }
  82. sField_TN_Handler?.isAccessible = true
  83. sIsHookFieldInit = true
  84. }
  85. val tn = sField_TN!![toast]
  86. val originHandler = sField_TN_Handler!![tn] as Handler
  87. sField_TN_Handler!![tn] = SafelyHandlerWrapper(originHandler)
  88. } catch (e: Exception) {
  89. Log.e(TAG, "Hook toast exception=$e")
  90. }
  91. }
  92. private class ToastRunnable(
  93. private val context: Context,
  94. private val text: CharSequence,
  95. private val duration: Int,
  96. private val isShowCenter: Boolean
  97. ) : Runnable {
  98. @SuppressLint("ShowToast")
  99. override fun run() {
  100. if (mToast == null) {
  101. mToast = Toast.makeText(context, text, duration)
  102. } else {
  103. mToast!!.setText(text)
  104. if (isShowCenter) {
  105. mToast!!.setGravity(Gravity.CENTER, 0, 0)
  106. }
  107. mToast!!.duration = duration
  108. }
  109. hookToast(mToast)
  110. mToast!!.show()
  111. }
  112. }
  113. private class SafelyHandlerWrapper(private val originHandler: Handler?) : Handler() {
  114. override fun dispatchMessage(msg: Message) {
  115. try {
  116. super.dispatchMessage(msg)
  117. } catch (e: Exception) {
  118. Log.e(TAG, "Catch system toast exception:$e")
  119. }
  120. }
  121. override fun handleMessage(msg: Message) {
  122. originHandler?.handleMessage(msg)
  123. }
  124. }
  125. }

11.FileManager:

  1. package com.example.cameraxdemo.utils;
  2. import android.app.Activity;
  3. import android.content.ContentResolver;
  4. import android.content.ContentUris;
  5. import android.content.ContentValues;
  6. import android.content.Context;
  7. import android.content.Intent;
  8. import android.database.Cursor;
  9. import android.net.Uri;
  10. import android.os.Build;
  11. import android.os.Environment;
  12. import android.provider.DocumentsContract;
  13. import android.provider.MediaStore;
  14. import android.text.TextUtils;
  15. import android.util.Log;
  16. import androidx.annotation.NonNull;
  17. import com.example.cameraxdemo.app.CameraApp;
  18. import java.io.File;
  19. /**
  20. * @author: njb
  21. * @date: 2023/8/15 17:13
  22. * @desc:
  23. */
  24. public class FileManager {
  25. // 媒体模块根目录
  26. private static final String SAVE_MEDIA_ROOT_DIR = Environment.DIRECTORY_DCIM;
  27. // 媒体模块存储路径
  28. private static final String SAVE_MEDIA_DIR = SAVE_MEDIA_ROOT_DIR + "/CameraXApp";
  29. private static final String AVATAR_DIR = "/avatar";
  30. private static final String SAVE_MEDIA_VIDEO_DIR = SAVE_MEDIA_DIR + "/video";
  31. private static final String SAVE_MEDIA_PHOTO_DIR = SAVE_MEDIA_DIR + "/photo";
  32. // JPG后缀
  33. public static final String JPG_SUFFIX = ".jpg";
  34. // PNG后缀
  35. public static final String PNG_SUFFIX = ".png";
  36. // MP4后缀
  37. public static final String MP4_SUFFIX = ".mp4";
  38. // YUV后缀
  39. public static final String YUV_SUFFIX = ".yuv";
  40. // h264后缀
  41. public static final String H264_SUFFIX = ".h264";
  42. /**
  43. * 保存图片到系统相册
  44. *
  45. * @param context
  46. * @param file
  47. */
  48. public static String saveImage(Context context, File file) {
  49. ContentResolver localContentResolver = context.getContentResolver();
  50. ContentValues localContentValues = getImageContentValues(context, file, System.currentTimeMillis());
  51. localContentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, localContentValues);
  52. Intent localIntent = new Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE");
  53. final Uri localUri = Uri.fromFile(file);
  54. localIntent.setData(localUri);
  55. context.sendBroadcast(localIntent);
  56. return file.getAbsolutePath();
  57. }
  58. public static ContentValues getImageContentValues(Context paramContext, File paramFile, long paramLong) {
  59. ContentValues localContentValues = new ContentValues();
  60. localContentValues.put("title", paramFile.getName());
  61. localContentValues.put("_display_name", paramFile.getName());
  62. localContentValues.put("mime_type", "image/jpeg");
  63. localContentValues.put("datetaken", Long.valueOf(paramLong));
  64. localContentValues.put("date_modified", Long.valueOf(paramLong));
  65. localContentValues.put("date_added", Long.valueOf(paramLong));
  66. localContentValues.put("orientation", Integer.valueOf(0));
  67. localContentValues.put("_data", paramFile.getAbsolutePath());
  68. localContentValues.put("_size", Long.valueOf(paramFile.length()));
  69. return localContentValues;
  70. }
  71. /**
  72. * 获取App存储根目录
  73. */
  74. public static String getAppRootDir() {
  75. String path = getStorageRootDir();
  76. FileUtil.createOrExistsDir(path);
  77. return path;
  78. }
  79. /**
  80. * 获取文件存储根目录
  81. */
  82. public static String getStorageRootDir() {
  83. File filePath = CameraApp.Companion.getMInstance().getExternalFilesDir("");
  84. String path;
  85. if (filePath != null) {
  86. path = filePath.getAbsolutePath();
  87. } else {
  88. path = CameraApp.Companion.getMInstance().getFilesDir().getAbsolutePath();
  89. }
  90. return path;
  91. }
  92. /**
  93. * 图片地址
  94. */
  95. public static String getCameraPhotoPath() {
  96. return getFolderDirPath(SAVE_MEDIA_PHOTO_DIR);
  97. }
  98. /**
  99. * 获取拍照普通图片文件
  100. */
  101. public static File getSavedPictureFile(long timeStamp) {
  102. String fileName = "image"+ "_"+ + timeStamp + JPG_SUFFIX;
  103. return new File(getCameraPhotoPath(), fileName);
  104. }
  105. /**
  106. * 头像地址
  107. */
  108. public static String getAvatarPath(String fileName) {
  109. String path;
  110. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
  111. path = getFolderDirPath(SAVE_MEDIA_DIR + AVATAR_DIR);
  112. } else {
  113. path = getSaveDir(AVATAR_DIR);
  114. }
  115. return path + File.separator + fileName;
  116. }
  117. /**
  118. * 视频地址
  119. */
  120. public static String getCameraVideoPath() {
  121. return getFolderDirPath(SAVE_MEDIA_VIDEO_DIR);
  122. }
  123. public static String getFolderDirPath(String dstDirPathToCreate) {
  124. File dstFileDir = new File(Environment.getExternalStorageDirectory(), dstDirPathToCreate);
  125. if (!dstFileDir.exists() && !dstFileDir.mkdirs()) {
  126. Log.e("Failed to create file", dstDirPathToCreate);
  127. return null;
  128. }
  129. return dstFileDir.getAbsolutePath();
  130. }
  131. /**
  132. * 获取具体模块存储目录
  133. */
  134. public static String getSaveDir(@NonNull String directory) {
  135. String path = "";
  136. if (TextUtils.isEmpty(directory) || "/".equals(directory)) {
  137. path = "";
  138. } else if (directory.startsWith("/")) {
  139. path = directory;
  140. } else {
  141. path = "/" + directory;
  142. }
  143. path = getAppRootDir() + path;
  144. FileUtil.createOrExistsDir(path);
  145. return path;
  146. }
  147. /**
  148. * 通过媒体文件Uri获取文件-Android 11兼容
  149. *
  150. * @param fileUri 文件Uri
  151. */
  152. public static File getMediaUri2File(Uri fileUri) {
  153. String[] projection = {MediaStore.Images.Media.DATA};
  154. Cursor cursor = CameraApp.Companion.getMInstance().getContentResolver().query(fileUri, projection,
  155. null, null, null);
  156. if (cursor != null) {
  157. if (cursor.moveToFirst()) {
  158. int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
  159. String path = cursor.getString(columnIndex);
  160. cursor.close();
  161. return new File(path);
  162. }
  163. }
  164. return null;
  165. }
  166. /**
  167. * 根据Uri获取图片绝对路径,解决Android4.4以上版本Uri转换
  168. *
  169. * @param context 上下文
  170. * @param imageUri 图片地址
  171. */
  172. public static String getImageAbsolutePath(Activity context, Uri imageUri) {
  173. if (context == null || imageUri == null)
  174. return null;
  175. if (DocumentsContract.isDocumentUri(context, imageUri)) {
  176. if (isExternalStorageDocument(imageUri)) {
  177. String docId = DocumentsContract.getDocumentId(imageUri);
  178. String[] split = docId.split(":");
  179. String type = split[0];
  180. if ("primary".equalsIgnoreCase(type)) {
  181. return Environment.getExternalStorageDirectory() + "/" + split[1];
  182. }
  183. } else if (isDownloadsDocument(imageUri)) {
  184. String id = DocumentsContract.getDocumentId(imageUri);
  185. Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.parseLong(id));
  186. return getDataColumn(context, contentUri, null, null);
  187. } else if (isMediaDocument(imageUri)) {
  188. String docId = DocumentsContract.getDocumentId(imageUri);
  189. String[] split = docId.split(":");
  190. String type = split[0];
  191. Uri contentUri = null;
  192. if ("image".equals(type)) {
  193. contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
  194. } else if ("video".equals(type)) {
  195. contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
  196. } else if ("audio".equals(type)) {
  197. contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
  198. }
  199. String selection = MediaStore.Images.Media._ID + "=?";
  200. String[] selectionArgs = new String[]{split[1]};
  201. return getDataColumn(context, contentUri, selection, selectionArgs);
  202. }
  203. } // MediaStore (and general)
  204. else if ("content".equalsIgnoreCase(imageUri.getScheme())) {
  205. // Return the remote address
  206. if (isGooglePhotosUri(imageUri))
  207. return imageUri.getLastPathSegment();
  208. return getDataColumn(context, imageUri, null, null);
  209. }
  210. // File
  211. else if ("file".equalsIgnoreCase(imageUri.getScheme())) {
  212. return imageUri.getPath();
  213. }
  214. return null;
  215. }
  216. /**
  217. * @param uri The Uri to checkRemote.
  218. * @return Whether the Uri authority is ExternalStorageProvider.
  219. */
  220. public static boolean isExternalStorageDocument(Uri uri) {
  221. return "com.android.externalstorage.documents".equals(uri.getAuthority());
  222. }
  223. /**
  224. * @param uri The Uri to checkRemote.
  225. * @return Whether the Uri authority is DownloadsProvider.
  226. */
  227. public static boolean isDownloadsDocument(Uri uri) {
  228. return "com.android.providers.downloads.documents".equals(uri.getAuthority());
  229. }
  230. /**
  231. * @param uri The Uri to checkRemote.
  232. * @return Whether the Uri authority is MediaProvider.
  233. */
  234. public static boolean isMediaDocument(Uri uri) {
  235. return "com.android.providers.media.documents".equals(uri.getAuthority());
  236. }
  237. /**
  238. * @param uri The Uri to checkRemote.
  239. * @return Whether the Uri authority is Google Photos.
  240. */
  241. public static boolean isGooglePhotosUri(Uri uri) {
  242. return "com.google.android.apps.photos.content".equals(uri.getAuthority());
  243. }
  244. public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
  245. String column = MediaStore.Images.Media.DATA;
  246. String[] projection = {column};
  247. try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) {
  248. if (cursor != null && cursor.moveToFirst()) {
  249. int index = cursor.getColumnIndexOrThrow(column);
  250. return cursor.getString(index);
  251. }
  252. }
  253. return null;
  254. }
  255. }

12.VideoFileUtils

  1. package com.example.cameraxdemo.utils
  2. import android.content.Context
  3. import android.os.Environment
  4. import com.example.cameraxdemo.R
  5. import java.io.File
  6. import java.text.SimpleDateFormat
  7. import java.util.Locale
  8. /**
  9. *@author: njb
  10. *@date: 2023/8/15 17:13
  11. *@desc:
  12. */
  13. object VideoFileUtils {
  14. /**
  15. * 获取视频文件路径
  16. */
  17. fun getVideoName(): String {
  18. val videoPath = Environment.getExternalStorageDirectory().toString() + "/CameraXApp"
  19. val dir = File(videoPath)
  20. if (!dir.exists() && !dir.mkdirs()) {
  21. ToastUtils.shortToast("文件不存在")
  22. }
  23. return videoPath
  24. }
  25. /**
  26. * 获取图片文件路径
  27. */
  28. fun getImageFileName(): String {
  29. val imagePath = Environment.getExternalStorageDirectory().toString() + "/images"
  30. val dir = File(imagePath)
  31. if (!dir.exists() && !dir.mkdirs()) {
  32. ToastUtils.shortToast("文件不存在")
  33. }
  34. return imagePath
  35. }
  36. /**
  37. * 拍照文件保存路径
  38. * @param context
  39. * @return
  40. */
  41. fun getPhotoDir(context: Context?): String? {
  42. return FileManager.getFolderDirPath(
  43. "DCIM/Camera/CameraXApp/photo"
  44. )
  45. }
  46. /**
  47. * 视频文件保存路径
  48. * @param context
  49. * @return
  50. */
  51. fun getVideoDir(): String? {
  52. return FileManager.getFolderDirPath(
  53. "DCIM/Camera/CameraXApp/video"
  54. )
  55. }
  56. /** Use external media if it is available, our app's file directory otherwise */
  57. fun getOutputDirectory(context: Context): File {
  58. val appContext = context.applicationContext
  59. val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
  60. File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() }
  61. }
  62. return if (mediaDir != null && mediaDir.exists())
  63. mediaDir else appContext.filesDir
  64. }
  65. fun createFile(baseFolder: File, format: String, extension: String) =
  66. File(
  67. baseFolder, SimpleDateFormat(format, Locale.US)
  68. .format(System.currentTimeMillis()) + extension
  69. )
  70. }

13.CameraApp:

  1. package com.example.cameraxdemo.app
  2. import android.app.Application
  3. /**
  4. *@author: njb
  5. *@date: 2023/8/15 17:07
  6. *@desc:
  7. */
  8. class CameraApp : Application() {
  9. override fun onCreate() {
  10. super.onCreate()
  11. mInstance = this
  12. }
  13. companion object {
  14. lateinit var mInstance: CameraApp
  15. private set
  16. }
  17. }

14.完整的示例代码如下:

  1. package com.example.cameraxdemo
  2. import android.Manifest
  3. import android.annotation.SuppressLint
  4. import android.content.Intent
  5. import android.content.pm.PackageManager
  6. import android.media.MediaScannerConnection
  7. import android.net.Uri
  8. import android.os.Build
  9. import android.os.Bundle
  10. import android.os.Environment
  11. import android.webkit.MimeTypeMap
  12. import android.widget.Button
  13. import androidx.appcompat.app.AppCompatActivity
  14. import androidx.camera.core.AspectRatio
  15. import androidx.camera.core.Camera
  16. import androidx.camera.core.CameraSelector
  17. import androidx.camera.core.ImageCapture
  18. import androidx.camera.core.ImageCaptureException
  19. import androidx.camera.core.Preview
  20. import androidx.camera.core.VideoCapture
  21. import androidx.camera.core.VideoCapture.OnVideoSavedCallback
  22. import androidx.camera.core.VideoCapture.OutputFileOptions
  23. import androidx.camera.lifecycle.ProcessCameraProvider
  24. import androidx.camera.view.PreviewView
  25. import androidx.core.app.ActivityCompat
  26. import androidx.core.content.ContextCompat
  27. import androidx.core.net.toFile
  28. import com.blankj.utilcode.util.LogUtils
  29. import com.example.cameraxdemo.activity.CameraActivity
  30. import com.example.cameraxdemo.utils.Constants
  31. import com.example.cameraxdemo.utils.Constants.Companion.DATE_FORMAT
  32. import com.example.cameraxdemo.utils.Constants.Companion.PHOTO_EXTENSION
  33. import com.example.cameraxdemo.utils.Constants.Companion.REQUIRED_PERMISSIONS
  34. import com.example.cameraxdemo.utils.FileManager
  35. import com.example.cameraxdemo.utils.ToastUtils
  36. import com.example.cameraxdemo.utils.VideoFileUtils.createFile
  37. import com.example.cameraxdemo.utils.VideoFileUtils.getOutputDirectory
  38. import java.io.File
  39. import java.text.SimpleDateFormat
  40. import java.util.*
  41. import java.util.concurrent.ExecutorService
  42. import java.util.concurrent.Executors
  43. class MainActivity : AppCompatActivity() {
  44. private var imageCamera: ImageCapture? = null
  45. private lateinit var cameraExecutor: ExecutorService
  46. private var videoCapture: VideoCapture? = null
  47. private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA//当前相机
  48. private var preview: Preview? = null//预览对象
  49. private var cameraProvider: ProcessCameraProvider? = null//相机信息
  50. private lateinit var camera: Camera //相机对象
  51. private var isRecordVideo: Boolean = false
  52. private val TAG = "CameraXApp"
  53. private lateinit var outputDirectory: File
  54. private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
  55. private val btnCameraCapture: Button by lazy { findViewById(R.id.btnCameraCapture) }
  56. private val btnVideo: Button by lazy { findViewById(R.id.btnVideo) }
  57. private val btnSwitch: Button by lazy { findViewById(R.id.btnSwitch) }
  58. private val btnOpenCamera: Button by lazy { findViewById(R.id.btnOpenCamera) }
  59. private val viewFinder: PreviewView by lazy { findViewById(R.id.mPreviewView) }
  60. override fun onCreate(savedInstanceState: Bundle?) {
  61. super.onCreate(savedInstanceState)
  62. setContentView(R.layout.activity_main)
  63. initPermission()
  64. initView()
  65. }
  66. private fun initView() {
  67. outputDirectory = getOutputDirectory(this)
  68. }
  69. @SuppressLint("RestrictedApi")
  70. private fun initListener() {
  71. btnCameraCapture.setOnClickListener {
  72. takePhoto()
  73. }
  74. btnVideo.setOnClickListener {
  75. if (!isRecordVideo) {
  76. takeVideo()
  77. isRecordVideo = true
  78. btnVideo.text = "停止录像"
  79. } else {
  80. isRecordVideo = false
  81. videoCapture?.stopRecording()//停止录制
  82. //preview?.clear()//清除预览
  83. btnVideo.text = "开始录像"
  84. }
  85. }
  86. btnSwitch.setOnClickListener {
  87. cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
  88. CameraSelector.DEFAULT_FRONT_CAMERA
  89. } else {
  90. CameraSelector.DEFAULT_BACK_CAMERA
  91. }
  92. if (!isRecordVideo) {
  93. startCamera()
  94. }
  95. }
  96. btnOpenCamera.setOnClickListener {
  97. val intent = Intent(this, CameraActivity::class.java)
  98. startActivity(intent)
  99. }
  100. }
  101. private fun initPermission() {
  102. if (checkPermissions()) {
  103. // ImageCapture
  104. startCamera()
  105. } else {
  106. requestPermission()
  107. }
  108. }
  109. private fun requestPermission() {
  110. when {
  111. Build.VERSION.SDK_INT >= 33 -> {
  112. ActivityCompat.requestPermissions(
  113. this,
  114. arrayOf(Manifest.permission.READ_MEDIA_IMAGES,Manifest.permission.READ_MEDIA_AUDIO,Manifest.permission.READ_MEDIA_VIDEO,Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO),
  115. Constants.REQUEST_CODE_PERMISSIONS
  116. )
  117. }
  118. else -> {
  119. ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, Constants.REQUEST_CODE_PERMISSIONS)
  120. }
  121. }
  122. }
  123. /**
  124. * 开始拍照
  125. */
  126. private fun takePhoto() {
  127. val imageCapture = imageCamera ?: return
  128. val photoFile = createFile(outputDirectory, DATE_FORMAT, PHOTO_EXTENSION)
  129. val metadata = ImageCapture.Metadata().apply {
  130. // Mirror image when using the front camera
  131. isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
  132. }
  133. val outputOptions =
  134. ImageCapture.OutputFileOptions.Builder(photoFile).setMetadata(metadata).build()
  135. imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this),
  136. object : ImageCapture.OnImageSavedCallback {
  137. override fun onError(exc: ImageCaptureException) {
  138. LogUtils.e(TAG, "Photo capture failed: ${exc.message}", exc)
  139. ToastUtils.shortToast(" 拍照失败 ${exc.message}")
  140. }
  141. override fun onImageSaved(output: ImageCapture.OutputFileResults) {
  142. val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
  143. ToastUtils.shortToast(" 拍照成功 $savedUri")
  144. LogUtils.e(TAG, savedUri.path.toString())
  145. val mimeType = MimeTypeMap.getSingleton()
  146. .getMimeTypeFromExtension(savedUri.toFile().extension)
  147. MediaScannerConnection.scanFile(
  148. this@MainActivity,
  149. arrayOf(savedUri.toFile().absolutePath),
  150. arrayOf(mimeType)
  151. ) { _, uri ->
  152. LogUtils.d(
  153. TAG,
  154. "Image capture scanned into media store: ${uri.path.toString()}"
  155. )
  156. }
  157. }
  158. })
  159. }
  160. /**
  161. * 开始录像
  162. */
  163. @SuppressLint("RestrictedApi", "ClickableViewAccessibility", "MissingPermission")
  164. private fun takeVideo() {
  165. //开始录像
  166. try {
  167. isRecordVideo = true
  168. val mFileDateFormat = SimpleDateFormat(DATE_FORMAT, Locale.US)
  169. //视频保存路径
  170. val file =
  171. File(FileManager.getCameraVideoPath(), mFileDateFormat.format(Date()) + ".mp4")
  172. val outputOptions = OutputFileOptions.Builder(file)
  173. videoCapture?.startRecording(
  174. outputOptions.build(),
  175. Executors.newSingleThreadExecutor(),
  176. object : OnVideoSavedCallback {
  177. override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) {
  178. isRecordVideo = false
  179. LogUtils.d(TAG, "===视频保存的地址为=== ${file.absolutePath}")
  180. //保存视频成功回调,会在停止录制时被调用
  181. ToastUtils.shortToast(" 录像成功 $file")
  182. }
  183. override fun onError(
  184. videoCaptureError: Int,
  185. message: String,
  186. cause: Throwable?
  187. ) {
  188. //保存失败的回调,可能在开始或结束录制时被调用
  189. isRecordVideo = false
  190. LogUtils.e(TAG, "onError: $message")
  191. ToastUtils.shortToast(" 录像失败 $message")
  192. }
  193. })
  194. } catch (e: Exception) {
  195. e.printStackTrace()
  196. LogUtils.e(TAG, "===录像出错===${e.message}")
  197. }
  198. }
  199. /**
  200. * 开始相机预览
  201. */
  202. @SuppressLint("RestrictedApi")
  203. private fun startCamera() {
  204. cameraExecutor = Executors.newSingleThreadExecutor()
  205. val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
  206. cameraProviderFuture.addListener(Runnable {
  207. cameraProvider = cameraProviderFuture.get()//获取相机信息
  208. //预览配置
  209. preview = Preview.Builder()
  210. .build()
  211. .also {
  212. it.setSurfaceProvider(viewFinder.surfaceProvider)
  213. }
  214. imageCamera = ImageCapture.Builder()
  215. .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
  216. .build()
  217. videoCapture = VideoCapture.Builder()//录像用例配置
  218. .setTargetAspectRatio(AspectRatio.RATIO_16_9) //设置高宽比
  219. //.setTargetRotation(viewFinder.display!!.rotation)//设置旋转角度
  220. .build()
  221. try {
  222. cameraProvider?.unbindAll()//先解绑所有用例
  223. camera = cameraProvider?.bindToLifecycle(
  224. this,
  225. cameraSelector,
  226. preview,
  227. imageCamera,
  228. videoCapture
  229. )!!//绑定用例
  230. } catch (e: Exception) {
  231. LogUtils.e(TAG, "Use case binding failed", e.message)
  232. }
  233. }, ContextCompat.getMainExecutor(this))
  234. initListener()
  235. }
  236. override fun onRequestPermissionsResult(
  237. requestCode: Int, permissions: Array<String>, grantResults:
  238. IntArray
  239. ) {
  240. super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  241. when (requestCode) {
  242. Constants.REQUEST_CODE_PERMISSIONS -> {
  243. var allPermissionsGranted = true
  244. for (result in grantResults) {
  245. if (result != PackageManager.PERMISSION_GRANTED) {
  246. allPermissionsGranted = false
  247. break
  248. }
  249. }
  250. when {
  251. allPermissionsGranted -> {
  252. // 权限已授予,执行文件读写操作
  253. startCamera()
  254. }
  255. else -> {
  256. // 权限被拒绝,处理权限请求失败的情况
  257. ToastUtils.shortToast("请您打开必要权限")
  258. requestPermission()
  259. }
  260. }
  261. }
  262. }
  263. }
  264. private fun checkPermissions(): Boolean {
  265. when {
  266. Build.VERSION.SDK_INT >= 33 -> {
  267. val permissions = arrayOf(
  268. Manifest.permission.READ_MEDIA_IMAGES,
  269. Manifest.permission.READ_MEDIA_AUDIO,
  270. Manifest.permission.READ_MEDIA_VIDEO,
  271. Manifest.permission.CAMERA,
  272. Manifest.permission.RECORD_AUDIO,
  273. )
  274. for (permission in permissions) {
  275. return Environment.isExternalStorageManager()
  276. }
  277. }
  278. else -> {
  279. for (permission in REQUIRED_PERMISSIONS) {
  280. if (ContextCompat.checkSelfPermission(
  281. this,
  282. permission
  283. ) != PackageManager.PERMISSION_GRANTED
  284. ) {
  285. return false
  286. }
  287. }
  288. }
  289. }
  290. return true
  291. }
  292. override fun onDestroy() {
  293. super.onDestroy()
  294. cameraExecutor.shutdown()
  295. }
  296. }

15.实现的效果如下:

53ff5b334f8df30196059d73ca67dc97.jpeg

93a8b297fbbbcc62f5cdb55246459ca7.jpeg

02a8454528f5dc468bf72146366e8b4b.jpeg

16.日志打印:

模拟器日志如下:

1e313b7a37a1095fe7226bee6fa3d13b.jpeg

c62360b172b95a86fea8482a01c86ac1.jpeg

867d60a1e9b69b62a5d86ae4e7dab9f3.jpeg

真机日志打印:

6e6e307f936d2e5db604596a85a8d960.jpeg

b1ce11167fb5ac8a23ad8aee86ae6b56.jpeg

17.选择相册的代码如下:

  1. package com.example.cameraxdemo.activity
  2. import androidx.appcompat.app.AppCompatActivity
  3. import android.content.ContentValues
  4. import android.content.Intent
  5. import android.graphics.Bitmap
  6. import android.net.Uri
  7. import android.os.Build
  8. import android.os.Bundle
  9. import android.provider.MediaStore
  10. import android.util.Log
  11. import android.widget.Button
  12. import android.widget.ImageView
  13. import com.blankj.utilcode.util.LogUtils
  14. import com.bumptech.glide.Glide
  15. import com.example.cameraxdemo.R
  16. import com.example.cameraxdemo.utils.Constants.Companion.REQUEST_CODE_CAMERA
  17. import com.example.cameraxdemo.utils.Constants.Companion.REQUEST_CODE_CROP
  18. import com.example.cameraxdemo.utils.FileManager
  19. import com.example.cameraxdemo.utils.FileUtil
  20. import java.io.File
  21. /**
  22. *@author: njb
  23. *@date: 2023/8/15 17:20
  24. *@desc:
  25. */
  26. class CameraActivity :AppCompatActivity(){
  27. private var mUploadImageUri: Uri? = null
  28. private var mUploadImageFile: File? = null
  29. private var photoUri: Uri? = null
  30. private val btnCamera:Button by lazy { findViewById(R.id.btnCamera) }
  31. private val ivAvatar:ImageView by lazy { findViewById(R.id.iv_avatar) }
  32. private val TAG = CameraActivity::class.java.name
  33. override fun onCreate(savedInstanceState: Bundle?) {
  34. super.onCreate(savedInstanceState)
  35. setContentView(R.layout.activity_camera)
  36. initView()
  37. }
  38. private fun initView() {
  39. btnCamera.setOnClickListener {
  40. startSystemCamera()
  41. }
  42. }
  43. /**
  44. * 调起系统相机拍照
  45. */
  46. private fun startSystemCamera() {
  47. val takeIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
  48. val values = ContentValues()
  49. //根据uri查询图片地址
  50. photoUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
  51. LogUtils.d(TAG, "photoUri:" + photoUri?.authority + ",photoUri:" + photoUri?.path)
  52. //放入拍照后的地址
  53. takeIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
  54. //调起拍照
  55. startActivityForResult(
  56. takeIntent,
  57. REQUEST_CODE_CAMERA
  58. )
  59. }
  60. /**
  61. * 设置用户头像
  62. */
  63. private fun setAvatar() {
  64. val file: File? = if (mUploadImageUri != null) {
  65. FileManager.getMediaUri2File(mUploadImageUri)
  66. } else {
  67. mUploadImageFile
  68. }
  69. Glide.with(this).load(file).into(ivAvatar)
  70. LogUtils.d(TAG,"filepath"+ file!!.absolutePath)
  71. }
  72. /**
  73. * 系统裁剪方法
  74. */
  75. private fun workCropFun(imgPathUri: Uri?) {
  76. mUploadImageUri = null
  77. mUploadImageFile = null
  78. if (imgPathUri != null) {
  79. val imageObject: Any = FileUtil.getHeadJpgFile()
  80. if (imageObject is Uri) {
  81. mUploadImageUri = imageObject
  82. }
  83. if (imageObject is File) {
  84. mUploadImageFile = imageObject
  85. }
  86. val intent = Intent("com.android.camera.action.CROP")
  87. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  88. intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
  89. }
  90. intent.run {
  91. setDataAndType(imgPathUri, "image/*")// 图片资源
  92. putExtra("crop", "true") // 裁剪
  93. putExtra("aspectX", 1) // 宽度比
  94. putExtra("aspectY", 1) // 高度比
  95. putExtra("outputX", 150) // 裁剪框宽度
  96. putExtra("outputY", 150) // 裁剪框高度
  97. putExtra("scale", true) // 缩放
  98. putExtra("return-data", false) // true-返回缩略图-data,false-不返回-需要通过Uri
  99. putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) // 保存的图片格式
  100. putExtra("noFaceDetection", true) // 取消人脸识别
  101. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
  102. putExtra(MediaStore.EXTRA_OUTPUT, mUploadImageUri)
  103. } else {
  104. val imgCropUri = Uri.fromFile(mUploadImageFile)
  105. putExtra(MediaStore.EXTRA_OUTPUT, imgCropUri)
  106. }
  107. }
  108. startActivityForResult(
  109. intent, REQUEST_CODE_CROP
  110. )
  111. }
  112. }
  113. override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  114. super.onActivityResult(requestCode, resultCode, data)
  115. if (resultCode == RESULT_OK) {
  116. if (requestCode == REQUEST_CODE_CAMERA) {//拍照回调
  117. workCropFun(photoUri)
  118. } else if (requestCode == REQUEST_CODE_CROP) {//裁剪回调
  119. setAvatar()
  120. }
  121. }
  122. }
  123. }

18.选择相册后的效果如下:

6f638fe4c6ef2c2353bde1995a0d8d6a.jpeg

b0505ae189ea7a1c16d57be6bc554914.jpeg

19.总结:

今天虽然遇到了不少问题,但是出现问题后调试的过程很安逸,基本找出原因和解决花费了将近3个小时才把完整的demo整理出来,花费这么久的时间有三点原因:

1.Android13适配的规则没有搞清楚就直接升级更换了新版本,导致请求和拒绝后一直出错。

2.CameraX新的api和录像权限没看完整导致这里来回折腾了很久,最后找到了官网api才解决。

3.对于AGP8.1.0使用不熟悉,导致刚开始配置依赖也浪费了一点时间。

“路漫漫其修远兮,吾将上下而求索”,后面会把整理出来的完整适配Android13的例子也放出来,还有关于AGP8.1.0配置依赖方式变更的也会整理,遇到问题进行求索的过程比结果更重要,只要有一颗战胜困难的心,相信问题最终都会解决,如果感兴趣的可以尝试一下,若有其他问题可以提出来一起讨论解决,共同学习,一起成长.

20.demo源码地址如下:

https://gitee.com/jackning_admin/camera-xdemo

关注我获取更多知识或者投稿

ed243a80812fdd485ddcca9bffdfe007.jpeg

7efc6b52e91eb5c475ed854785400383.jpeg

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/935152
推荐阅读
相关标签
  

闽ICP备14008679号