• Android中使用CameraX实现拍照和录像(Kotlin实现)


     Andoird中拍照、录像是很常见的功能,但是系统相机的Api目前发生了很大的变化,有Camera1、Camera2、CameraX三个api,每个api的使用和方法都不一样,如果做过相机开发的小伙伴应该会很头疼这三个api在不同安卓系统手机的适配,由于目前的App有一部分工作涉及到这部分,所以总结了一下,目前由基础到深入慢慢总结.

    一.简介:(官方介绍如下)

    CameraX 是一个 Jetpack 支持库,旨在帮助您简化相机应用的开发工作。它提供一致且易用的 API 接口,适用于大多数 Android 设备,并可向后兼容至 Android 5.0(API 级别 21)。

    具体内容可以参考官网介绍,网站地址为:

    CameraX 概览  |  Android 开发者  |  Android Developers

    二.优势:(参考官网)

    易用性

    图 1. CameraX 以 Android 5.0(API 级别 21)及更高版本为目标平台,涵盖了大多数 Android 设备

    CameraX 引入了多个用例,使您可以专注于需要完成的任务,而无需花时间处理不同设备之间的细微差别。一些基本用例如下所示:

    • 预览:在屏幕上显示图像
    • 图像分析:无缝访问缓冲区中的图像以便在算法中使用,例如将其传入 MLKit
    • 图片拍摄:保存优质图片

    这些用例适用于搭载 Android 5.0(API 级别 21)或更高版本的所有设备,从而确保了同样的代码适用于市场中的大多数设备。

     三.实战代码如下:

    1.项目引入CameraX的依赖如下:

    在项目的build.gradle导入如下配置:

    // CameraX 核心库使用 camera2 实现
    implementation "androidx.camera:camera-camera2:1.0.0-beta07"
    // 可以使用CameraView
    implementation "androidx.camera:camera-view:1.0.0-alpha14"
    // 可以使用供应商扩展
    implementation "androidx.camera:camera-extensions:1.0.0-alpha14"
    //camerax的生命周期库
    implementation "androidx.camera:camera-lifecycle:1.0.0-beta07"

    2.项目的Application:

    /**
     * @auth: njb
     * @date: 2021/10/20 16:19
     * @desc: 描述
     */
    public class MyApp extends Application {
        public  static MyApp app = null;
    
        @Override
        public void onCreate() {
            super.onCreate();
            app = this;
        }
    
        public static MyApp getInstance(){
            return app;
        }
    }
    

    3.MainActivity代码如下:

    项目的主要3个功能方法:

    3.1、拍照方法:startCamera() 

        /**
         * 开始拍照
         */
        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.createSurfaceProvider())
                    }
    
                imageCamera = ImageCapture.Builder()
                    .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                    .build()
    
                videoCapture = VideoCapture.Builder()//录像用例配置
    //                .setTargetAspectRatio(AspectRatio.RATIO_16_9) //设置高宽比
    //                .setTargetRotation(viewFinder.display.rotation)//设置旋转角度
    //                .setAudioRecordSource(AudioSource.MIC)//设置音频源麦克风
                    .build()
    
                try {
                    cameraProvider?.unbindAll()//先解绑所有用例
                    camera = cameraProvider?.bindToLifecycle(
                        this,
                        cameraSelector,
                        preview,
                        imageCamera,
                        videoCapture
                    )//绑定用例
                } catch (exc: Exception) {
                    Log.e(TAG, "Use case binding failed", exc)
                }
    
            }, ContextCompat.getMainExecutor(this))
        }

    3.2、录像方法:takeVideo()

    /**
     * 开始录像
     */
    @SuppressLint("RestrictedApi", "ClickableViewAccessibility")
    private fun takeVideo() {
        val mDateFormat = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
        //视频保存路径
        val file = File(FileUtils.getVideoName(), mDateFormat.format(Date()) + ".mp4")
        //开始录像
        videoCapture?.startRecording(
            file,
            Executors.newSingleThreadExecutor(),
            object : OnVideoSavedCallback {
                override fun onVideoSaved(@NonNull file: File) {
                    //保存视频成功回调,会在停止录制时被调用
                    ToastUtils.shortToast(" 录像成功 $file.absolutePath")
                }
    
                override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
                    //保存失败的回调,可能在开始或结束录制时被调用
                    Log.e("", "onError: $message")
                    ToastUtils.shortToast(" 录像失败 $message")
                }
            })
    
        btnVideo.setOnClickListener {
            videoCapture?.stopRecording()//停止录制
            //preview?.clear()//清除预览
            btnVideo.text = "Start Video"
            btnVideo.setOnClickListener {
                btnVideo.text = "Stop Video"
                takeVideo()
            }
            Log.d("path", file.path)
        }
    }

    3.3、切换前后置摄像头方法:

    3.4、完整代码如下:

    package com.example.cameraxapp
    
    import android.Manifest
    import android.annotation.SuppressLint
    import android.content.pm.PackageManager
    import android.net.Uri
    import android.os.Bundle
    import android.util.Log
    import android.widget.Toast
    import androidx.annotation.NonNull
    import androidx.appcompat.app.AppCompatActivity
    import androidx.camera.core.*
    import androidx.camera.core.VideoCapture.OnVideoSavedCallback
    import androidx.camera.lifecycle.ProcessCameraProvider
    import androidx.core.app.ActivityCompat
    import androidx.core.content.ContextCompat
    import com.example.cameraxapp.utils.FileUtils
    import com.example.cameraxapp.utils.ToastUtils
    import kotlinx.android.synthetic.main.activity_main.*
    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
        var videoCapture: VideoCapture? = null//录像用例
        var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA//当前相机
        var preview: Preview? = null//预览对象
        var cameraProvider: ProcessCameraProvider? = null//相机信息
        var camera: Camera? = null//相机对象
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            initPermission()
        }
    
        private fun initPermission() {
            if (allPermissionsGranted()) {
                // ImageCapture
                startCamera()
            } else {
                ActivityCompat.requestPermissions(
                    this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
                )
            }
            btnCameraCapture.setOnClickListener {
                takePhoto()
            }
            btnVideo.setOnClickListener {
                btnVideo.text = "Stop Video"
                takeVideo()
            }
            btnSwitch.setOnClickListener {
                cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) {
                    CameraSelector.DEFAULT_FRONT_CAMERA
                } else {
                    CameraSelector.DEFAULT_BACK_CAMERA
                }
                startCamera()
            }
        }
    
    
        private fun takePhoto() {
            val imageCapture = imageCamera ?: return
            val mDateFormat = SimpleDateFormat("yyyyMMddHHmmss", Locale.US)
            val file =
                File(FileUtils.getImageFileName(), mDateFormat.format(Date()).toString() + ".jpg")
    
            val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build()
    
            imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this),
                object : ImageCapture.OnImageSavedCallback {
                    override fun onError(exc: ImageCaptureException) {
                        Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
                        ToastUtils.shortToast(" 拍照失败 ${exc.message}")
                    }
    
                    override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                        val savedUri = Uri.fromFile(file)
                        val msg = "Photo capture succeeded: $savedUri"
                        ToastUtils.shortToast(" 拍照成功 $savedUri")
                        Log.d(TAG, msg)
                    }
                })
        }
    
        /**
         * 开始录像
         */
        @SuppressLint("RestrictedApi", "ClickableViewAccessibility")
        private fun takeVideo() {
            val mDateFormat = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
            //视频保存路径
            val file = File(FileUtils.getVideoName(), mDateFormat.format(Date()) + ".mp4")
            //开始录像
            videoCapture?.startRecording(
                file,
                Executors.newSingleThreadExecutor(),
                object : OnVideoSavedCallback {
                    override fun onVideoSaved(@NonNull file: File) {
                        //保存视频成功回调,会在停止录制时被调用
                        ToastUtils.shortToast(" 录像成功 $file.absolutePath")
                    }
    
                    override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
                        //保存失败的回调,可能在开始或结束录制时被调用
                        Log.e("", "onError: $message")
                        ToastUtils.shortToast(" 录像失败 $message")
                    }
                })
    
            btnVideo.setOnClickListener {
                videoCapture?.stopRecording()//停止录制
                //preview?.clear()//清除预览
                btnVideo.text = "Start Video"
                btnVideo.setOnClickListener {
                    btnVideo.text = "Stop Video"
                    takeVideo()
                }
                Log.d("path", file.path)
            }
        }
    
        /**
         * 开始拍照
         */
        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.createSurfaceProvider())
                    }
    
                imageCamera = ImageCapture.Builder()
                    .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                    .build()
    
                videoCapture = VideoCapture.Builder()//录像用例配置
    //                .setTargetAspectRatio(AspectRatio.RATIO_16_9) //设置高宽比
    //                .setTargetRotation(viewFinder.display.rotation)//设置旋转角度
    //                .setAudioRecordSource(AudioSource.MIC)//设置音频源麦克风
                    .build()
    
                try {
                    cameraProvider?.unbindAll()//先解绑所有用例
                    camera = cameraProvider?.bindToLifecycle(
                        this,
                        cameraSelector,
                        preview,
                        imageCamera,
                        videoCapture
                    )//绑定用例
                } catch (exc: Exception) {
                    Log.e(TAG, "Use case binding failed", exc)
                }
    
            }, ContextCompat.getMainExecutor(this))
        }
    
        override fun onRequestPermissionsResult(
            requestCode: Int, permissions: Array<String>, grantResults:
            IntArray
        ) {
            if (requestCode == REQUEST_CODE_PERMISSIONS) {
                if (allPermissionsGranted()) {
                    startCamera()
                } else {
                    Toast.makeText(
                        this,
                        "Permissions not granted by the user.",
                        Toast.LENGTH_SHORT
                    ).show()
                    finish()
                }
            }
        }
    
        private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
            ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
        }
    
        override fun onDestroy() {
            super.onDestroy()
            cameraExecutor.shutdown()
        }
    
        companion object {
            private const val TAG = "CameraXBasic"
            private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
            private const val REQUEST_CODE_PERMISSIONS = 10
            private val REQUIRED_PERMISSIONS = arrayOf(
                Manifest.permission.CAMERA,
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.RECORD_AUDIO
            )
        }
    
    }

    4.项目封装的文件工具类:

    /**
     * @auth: njb
     * @date: 2021/10/20 17:47
     * @desc: 文件工具类
     */
    object FileUtils {
        /**
         * 获取视频文件路径
         */
        fun getVideoName(): String {
            val videoPath = Environment.getExternalStorageDirectory().toString() + "/CameraX"
            val dir = File(videoPath)
            if (!dir.exists() && !dir.mkdirs()) {
                ToastUtils.shortToast("Trip")
            }
            return videoPath
        }
    
        /**
         * 获取图片文件路径
         */
        fun getImageFileName(): String {
            val imagePath = Environment.getExternalStorageDirectory().toString() + "/images"
            val dir = File(imagePath)
            if (!dir.exists() && !dir.mkdirs()) {
                ToastUtils.shortToast("Trip")
            }
            return imagePath
        }
    }

    5.项目的ToastUtils工具类代码:

    package com.example.cameraxapp.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.NonNull;
    import androidx.annotation.StringRes;
    
    
    import com.example.cameraxapp.app.MyApp;
    
    import org.jetbrains.annotations.NotNull;
    
    import java.lang.reflect.Field;
    
    /**
     * toast工具类
     */
    public final class ToastUtils {
        private static final String TAG = "ToastUtil";
        private static Toast mToast;
        private static Field sField_TN;
        private static Field sField_TN_Handler;
        private static boolean sIsHookFieldInit = false;
        private static final String FIELD_NAME_TN = "mTN";
        private static final String FIELD_NAME_HANDLER = "mHandler";
    
        private static void showToast(final Context context, final CharSequence text,
                                      final int duration, final boolean isShowCenterFlag) {
            ToastRunnable toastRunnable = new ToastRunnable(context, text, duration, isShowCenterFlag);
            if (context instanceof Activity) {
                final Activity activity = (Activity) context;
                if (!activity.isFinishing()) {
                    activity.runOnUiThread(toastRunnable);
                }
            } else {
                Handler handler = new Handler(context.getMainLooper());
                handler.post(toastRunnable);
            }
        }
    
        public static void shortToast(Context context, CharSequence text) {
            showToast(context, text, Toast.LENGTH_SHORT, false);
        }
    
        public static void longToast(Context context, CharSequence text) {
            showToast(context, text, Toast.LENGTH_LONG, false);
        }
    
        public static void shortToast(String msg) {
            showToast(MyApp.getInstance(), msg, Toast.LENGTH_SHORT, false);
        }
    
        public static void shortToast(@StringRes int resId) {
            showToast(MyApp.getInstance(), MyApp.getInstance().getText(resId),
                    Toast.LENGTH_SHORT, false);
        }
    
        public static void centerShortToast(@NonNull String msg) {
            showToast(MyApp.getInstance(), msg, Toast.LENGTH_SHORT, true);
        }
    
        public static void centerShortToast(@StringRes int resId) {
            showToast(MyApp.getInstance(), MyApp.getInstance().getText(resId),
                    Toast.LENGTH_SHORT, true);
        }
    
        public static void cancelToast() {
            Looper looper = Looper.getMainLooper();
            if (looper.getThread() == Thread.currentThread()) {
                mToast.cancel();
            } else {
                new Handler(looper).post(() -> mToast.cancel());
            }
        }
    
        private static void hookToast(Toast toast) {
            try {
                if (!sIsHookFieldInit) {
                    sField_TN = Toast.class.getDeclaredField(FIELD_NAME_TN);
                    sField_TN.setAccessible(true);
                    sField_TN_Handler = sField_TN.getType().getDeclaredField(FIELD_NAME_HANDLER);
                    sField_TN_Handler.setAccessible(true);
                    sIsHookFieldInit = true;
                }
                Object tn = sField_TN.get(toast);
                Handler originHandler = (Handler) sField_TN_Handler.get(tn);
                sField_TN_Handler.set(tn, new SafelyHandlerWrapper(originHandler));
            } catch (Exception e) {
                Log.e(TAG, "Hook toast exception=" + e);
            }
        }
    
        private static class ToastRunnable implements Runnable {
            private Context context;
            private CharSequence text;
            private int duration;
            private boolean isShowCenter;
    
            public ToastRunnable(Context context, CharSequence text, int duration, boolean isShowCenter) {
                this.context = context;
                this.text = text;
                this.duration = duration;
                this.isShowCenter = isShowCenter;
            }
    
            @Override
            @SuppressLint("ShowToast")
            public void run() {
                if (mToast == null) {
                    mToast = Toast.makeText(context, text, duration);
                } else {
                    mToast.setText(text);
                    if (isShowCenter) {
                        mToast.setGravity(Gravity.CENTER, 0, 0);
                    }
                    mToast.setDuration(duration);
                }
                hookToast(mToast);
                mToast.show();
            }
        }
    
        private static class SafelyHandlerWrapper extends Handler {
            private Handler originHandler;
    
            public SafelyHandlerWrapper(Handler originHandler) {
                this.originHandler = originHandler;
            }
    
            @Override
            public void dispatchMessage(@NotNull Message msg) {
                try {
                    super.dispatchMessage(msg);
                } catch (Exception e) {
                    Log.e(TAG, "Catch system toast exception:" + e);
                }
            }
    
            @Override
            public void handleMessage(@NotNull Message msg) {
                if (originHandler != null) {
                    originHandler.handleMessage(msg);
                }
            }
        }
    }
    

    6.项目的Manifest代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        package="com.example.cameraxapp">
        <uses-feature android:name="android.hardware.camera.any" />
        <uses-permission android:name="android.permission.CAMERA"/>
        <!--存储图像或者视频权限-->
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
        <!--录制音频权限-->
        <uses-permission android:name="android.permission.RECORD_AUDIO" />
    
        <application
            android:name=".app.MyApp"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:requestLegacyExternalStorage="true"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity android:name=".MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    
            <provider
                android:name="androidx.core.content.FileProvider"
                android:authorities="${applicationId}.fileprovider"
                android:exported="false"
                android:grantUriPermissions="true"
                tools:replace="android:authorities">
                <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/file_paths" />
            </provider>
        </application>
    
    </manifest>

    7.运行效果如下图:可以看到拍照、录像,切换摄像头都是正常的

    四、遇到的问题如下: 

    1.拍照成功但后台打印日志图片文件写入失败。

    2.在Android 10及以上系统提示读写文件失败。

    3.录像后屏幕黑屏,预览失败。

    五、解决方法如下:

    1.拍照成功,图片文件写入失败,根据以前项目的经验没有配置FileProvider。

    2.在项目的res目录下配置file_paths

     file_paths代码如下:

     3.在manifest配置FileProvider,代码如下:

    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="${applicationId}.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true"
        tools:replace="android:authorities">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>

    4.Android10读写文件权限适配如下:

    在AndroidManifest的application中设置android:requestLegacyExternalStorage="true"。

     

    5.解决录像后屏幕黑屏,预览失败的方法:由于我在录像成功后主动调用了清除预览的方法,所以导致黑屏,预览失败,注销此方法即可。

     6.以上就是今天的CameraXApi的使用,测试了小米、华为、三星、google、oppo、vivo等几款主流机型,Android 9、Android 10的系统,后面有机型会适配Android 11,主逻辑全部使用的是kotlin,实现了预览、拍照、录像、切换前后置摄像头等功能,当然本文没有仔细展开讲解和Camera1、Camera2的区别,因为这块内容很多,所以后面有时间整理一下,本文还有很多不足之处,望大家谅解,有问题及时提出,共同学习进步。

    本人的csdn地址:https://blog.csdn.net/u012556114

    简书:https://www.jianshu.com/u/f0761210f810

    最后,项目的源码如下:

    CameraXApp: Android CameraX相机Api的使用实例

  • 相关阅读:
    尚硅谷《全套Java、Android、HTML5前端视频》
    OCM 学习练习题目
    本文转自 MyEclipse 2015反编译插件安装
    关闭VirtualBox虚拟机的时钟同步
    Oracle数据库的状态查询
    ORA-00845 MEMORY_TARGET not supported on this system 的解决
    Ehcache
    Oracle 列转行函数 Listagg()
    oracle数据库定时任务dbms_job的用法详解
    MySQL 处理海量数据时的一些优化查询速度方法
  • 原文地址:https://www.cnblogs.com/ning137/p/15438018.html
Copyright © 2020-2023  润新知