Camera1, Camera2 and CameraX are three apis, each of which has different usage and methods. If you have done camera development, you will feel very worried about the adaptation of these three apis in different Android phones. As part of the work of the current App involves this part, it is summarized from the basic to the in-depth.

I. introduction :(the official introduction is as follows)

CameraX is a Jetpack support library designed to help you simplify camera application development. It provides a consistent and easy-to-use API interface that works on most Android devices and is backward compatible up to Android 5.0 (API level 21).

For details, please refer to the official website. The website address is:

CameraX overview | | Android Developers Android Developers

Ii. Advantages :(refer to the official website)

Ease of use

Figure 1. CameraX is targeted at Android 5.0 (API level 21) and higher, covering most Android devices

CameraX introduces multiple use cases, allowing you to focus on what needs to be done without spending time dealing with the nuances of different devices. Some basic uses are as follows:

  • Preview: Displays an image on the screen
  • Image analysis: Seamlessly accessing images in the buffer for use in algorithms, such as passing them into MLKit
  • Photo shooting: Save quality pictures

These use cases work with all devices running Android 5.0 (API level 21) or higher, ensuring that the same code works for most devices in the market.

Iii. The actual code is as follows:

1. The dependencies of CameraX introduced in the project are as follows:

Import the following configuration in your project’s build.gradle:

Implementation "Androidx. camera: CamerA2:1.0.0-beta07" // CameraView can be used Implementation "Androidx. camera:camera-view:1.0.0-alpha14 Camerax lifecycle library implementation "Androidx. camera: Camera-Extensions :1.0.0-alpha14" // Camerax lifecycle library implementation "Androidx. Camera: the camera - lifecycle: 1.0.0 - beta07"Copy the code

2. Project Application:

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

3.MainActivity code is as follows:

There are three main functional methods of the project:

3.1 photographing method :startCamera()

/ * * * took pictures * / private fun startCamera () {cameraExecutor = Executors. NewSingleThreadExecutor () val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener(Runnable { cameraProvider = Cameraproviderfuture.get () cameraproviderFuture.get () cameraproviderFuture.get () cameraProviderFuture.get() cameraProviderFuture.get() it.setSurfaceProvider(viewFinder.createSurfaceProvider()) } imageCamera = ImageCapture.Builder() .setCaptuRemode (ImageCapture.capture_mode_minimize_latency).build() videoCapture = videocapture.builder ()// Video case configuration // SetTargetAspectRatio (AspectRatio. RATIO_16_9) / / set the height to width ratio / /. SetTargetRotation viewFinder. Display. (rotation) / / / / set the rotation Angle .setaudiorecordsource (audiosource.mic)// set AudioSource microphone. Build () try {cameraProvider? .unbindall ()// unbindAll cases camera = cameraProvider? .bindtolifecycle (this, cameraSelector, preview, imageCamera, videoCapture)} catch (exc: Exception) { Log.e(TAG, "Use case binding failed", exc) } }, ContextCompat.getMainExecutor(this)) }Copy the code

3.2 video recording method: takeVideo()

/** * Start recording */ @suppressLint ("RestrictedApi", "ClickableViewAccessibility") private fun takeVideo() { val mDateFormat = SimpleDateFormat(FILENAME_FORMAT, Locale.us) val file = file (fileutils.getVideoname (), mdateformat.format (Date()) + ".mp4") // Start videoCapture? .startRecording( file, Executors.newSingleThreadExecutor(), object : OnVideoSavedCallback { override fun onVideoSaved(@NonNull file: File) {// Save video callback successfully, Override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {// Save the failed callback, which may be called at the beginning or end of recording log.e ("", "onError: $message ") ToastUtils. ShortToast (" video failure $message ")}}) btnVideo. SetOnClickListener {videoCapture? .stop recording ()// stopRecording // . The clear () / / clear preview btnVideo. Text = "Start Video" btnVideo. SetOnClickListener {btnVideo. Text = "Stop Video" takeVideo ()} Log.d("path", file.path) } }Copy the code

3.3 Switching front and rear cameras:

btnVideo.setOnClickListener { videoCapture? .stop recording ()// stopRecording // . The clear () / / clear preview btnVideo. Text = "Start Video" btnVideo. SetOnClickListener {btnVideo. Text = "Stop Video" takeVideo ()} Log.d("path", file.path)Copy the code

}

3.4 The complete code is as follows:

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
        )
    }

}
Copy the code

4. File tool classes encapsulated by the project:

/** * @auth: NJB * @date: 2021/10/20 17:47 * @desc: File tool class */ object FileUtils {/** * getVideoName(): String { val videoPath = Environment.getExternalStorageDirectory().toString() + "/CameraX" val dir = File(videoPath) if (! dir.exists() && ! Dir.mkdirs ()) {toastutils.shorttoast ("Trip")} return videoPath} String { val imagePath = Environment.getExternalStorageDirectory().toString() + "/images" val dir = File(imagePath) if (! dir.exists() && ! dir.mkdirs()) { ToastUtils.shortToast("Trip") } return imagePath } }Copy the code

5. ToastUtils utility class code of the project:

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; /** * 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); }}}}Copy the code

6. The Manifest code for the project is as follows:

<? The 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" /> <! - recording audio permissions - > < USES - permission android: name = "android. Permission. RECORD_AUDIO" / > < application android: name = ". The app. The 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>Copy the code

Iv. Problems encountered are as follows:

1. The photo is taken successfully, but the background log file fails to be written.

2. On Android 10 or later, a message is displayed indicating that reading or writing files fails.

3. The video preview fails because the screen goes black.

V. Solutions are as follows:

1. The photo is taken successfully, but the image file fails to be written. Based on the experience of previous projects, the FileProvider is not configured.

2. Configure File_paths in the res directory of the project

The file_paths code is as follows:

<external-path name=”external_storage_root”

path=”.” />

3. Configure FileProvider in the manifest as follows:

<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>​
Copy the code

4.Android10 External storage file permission adaptation:

In the application of AndroidManifest set android: requestLegacyExternalStorage = “true”.

5. Solution to black screen and preview failure after recording: I actively called the method of clearing preview after recording success, so the black screen resulted in preview failure. You can log out of this method.

btnVideo.setOnClickListener {

videoCapture? .stoprecording ()// stopRecording

//preview? .clear()// Clear preview

Btnvideo.text = “Start recording”

btnVideo.setOnClickListener {

Btnvideo.text = “Stop recording”

takeVideo()

}

Log.d(“path”, file.path)}​

6. The above is the use of CameraXApi today, tested xiaomi, Huawei, Samsung, Google, Oppo, Vivo and other mainstream models, Android 9, Android 10, Android11 system, but Android11 phone all file permissions have also been adapted. However, all the applications for file permissions have been removed since the listing was blocked back. For the reason, you can go to the official website to see the specific adaptation and update of Android11. The main logic is all using Kotlin, which realizes the functions of preview, photo taking, video recording, camera before and after switching, etc. Of course, this article does not explain the difference between Camera1 and Camera2 in detail, because there is a lot of content, so there is time to sort out the difference later. There are still many deficiencies in this article, please understand, if you have any questions, please timely submit. Learn and progress together.

Finally, the source code for the project is as follows:

CameraXApp: An example of the Android CameraX CAMERA Api