Android CameraX は Android13 の落とし穴に適応します

6b6156f7632fd2a64bfb9141a3d61455.jpeg

序文:

最近 AGP プラグインを 8.1.0 にアップグレードしました。新しいプロジェクトを作成したとき、ターゲット バージョンとコンパイル済みバージョンは両方とも 33 でした。以前のデモでは Camerax を使用して写真やビデオを撮影できなかったことが判明したので、公式を確認してみましたウェブサイトやさまざまな情報を調べたところ、Android13. 適応計画が見つかりました。

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

動作の変更: Android 13 以降をターゲットとするアプリ

以前のリリースと同様、Android 13 にはアプリに影響を与える可能性のある動作変更がいくつか含まれています。次の動作の変更は、Android 13 以降をターゲットとするアプリにのみ影響します。アプリが Android 13 以降をターゲットにしている場合は、該当する場合、これらの動作を適切にサポートするようにアプリを変更する必要があります。

また、Android 13 で実行されているすべてのアプリに影響する動作変更のリストも必ずご確認ください。

1. 洗練されたメディア権限

d6c2106584ba6fea9bd8229d46d2a06b.jpeg

このダイアログ ボックスの 2 つのボタンは、上から下に「許可」と「許可しない」です図 1.権限を要求するREAD_MEDIA_AUDIOときにユーザーに表示されるシステム権限ダイアログ ボックス。

アプリが Android 13 以降をターゲットにしており、他のアプリによって作成されたメディア ファイルにアクセスする必要がある場合は、 アクセス許可 の代わりに、次の詳細なメディア アクセス許可の 1 つ以上をリクエストする必要があります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 依存関係を追加します。

[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 アプリの 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)
}

2050e5d372a0a8e07f79e9e29f3c905f.jpeg

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 では以前のバージョンと比べて大幅な変更が加えられており、パーミッションリクエストが洗練されています。詳細については、上記の公式 Web サイトの情報を参照し、コードに直接アクセスしてください。

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 を使用します。

<!--存储图像或者视频权限-->
<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.トーストユーティリティ:

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.ファイルマネージャー:

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.ビデオファイルユーティリティ

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.カメラアプリ:

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. 得られる効果は次のとおりです。

53ff5b334f8df30196059d73ca67dc97.jpeg

93a8b297fbbbcc62f5cdb55246459ca7.jpeg

02a8454528f5dc468bf72146366e8b4b.jpeg

16. ログの印刷:

シミュレータのログは次のとおりです。

1e313b7a37a1095fe7226bee6fa3d13b.jpeg

c62360b172b95a86fea8482a01c86ac1.jpeg

867d60a1e9b69b62a5d86ae4e7dab9f3.jpeg

実機ログの印刷:

6e6e307f936d2e5db604596a85a8d960.jpeg

b1ce11167fb5ac8a23ad8aee86ae6b56.jpeg

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. アルバムを選択した後の効果は次のとおりです。

6f638fe4c6ef2c2353bde1995a0d8d6a.jpeg

b0505ae189ea7a1c16d57be6bc554914.jpeg

19. 要約:

今日はトラブルが多かったですが、トラブル発生後のデバッグ作業は非常に快適でした デモ全体をまとめるまでに、基本的に原因究明と解決に3時間近くかかりました 時間がかかった理由は3つあります:

1. Android 13 の適応ルールが明確にされないまま新しいバージョンに直接アップグレードされたため、要求および拒否後にエラーが継続的に発生しました。

2. 新しい API とカメラの録画権限がよくわかりませんでした

3. AGP8.1.0 の使用に慣れていないため、最初に依存関係を構成するのに時間が無駄になりました。

「道は長くて険しい。上から下まで探っていきます。」 後日、Android 13への完全適応例も公開する予定です また、AGP8.1.0の構成依存方式の変更点も整理します何か問題に遭遇したら、私はやります 結果よりも求める過程が大切です 困難を乗り越える心さえあれば、問題はいつか解決すると信じています 興味のある方はぜひ他に困ったことがあれば、一緒に話し合って解決し、共に学び、成長していきましょう。

20.デモのソースコードのアドレスは次のとおりです。

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

より多くの知識を得る、または記事に貢献するには私をフォローしてください

ed243a80812fdd485ddcca9bffdfe007.jpeg

7efc6b52e91eb5c475ed854785400383.jpeg

おすすめ

転載: blog.csdn.net/c6E5UlI1N/article/details/132419145