android学习

Android中CameraX的基本使用(Kotlin实现)

2021-10-21  本文已影响0人  淡然灬一笑

 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的区别,因为这块内容很多,所以后面有时间整理一下,本文还有很多不足之处,望大家谅解,有问题及时提出,共同学习进步。

最后,项目的源码如下:

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

上一篇下一篇

猜你喜欢

热点阅读