?Andoird中拍照衣厘、錄像是很常見的功能,但是系統(tǒng)相機(jī)的Api目前發(fā)生了很大的變化择镇,有Camera1挡逼、Camera2、CameraX三個(gè)api,每個(gè)api的使用和方法都不一樣腻豌,如果做過相機(jī)開發(fā)的小伙伴應(yīng)該會(huì)很頭疼這三個(gè)api在不同安卓系統(tǒng)手機(jī)的適配家坎,由于目前的App有一部分工作涉及到這部分嘱能,所以總結(jié)了一下,目前由基礎(chǔ)到深入慢慢總結(jié).
一.簡(jiǎn)介:(官方介紹如下)
CameraX 是一個(gè) Jetpack 支持庫虱疏,旨在幫助您簡(jiǎn)化相機(jī)應(yīng)用的開發(fā)工作惹骂。它提供一致且易用的 API 接口,適用于大多數(shù) Android 設(shè)備做瞪,并可向后兼容至 Android 5.0(API 級(jí)別 21)析苫。
具體內(nèi)容可以參考官網(wǎng)介紹,網(wǎng)站地址為:
CameraX 概覽 ?|? Android 開發(fā)者 ?|? Android Developers
二.優(yōu)勢(shì):(參考官網(wǎng))
易用性
圖 1.CameraX 以 Android 5.0(API 級(jí)別 21)及更高版本為目標(biāo)平臺(tái)穿扳,涵蓋了大多數(shù) Android 設(shè)備
CameraX 引入了多個(gè)用例衩侥,使您可以專注于需要完成的任務(wù),而無需花時(shí)間處理不同設(shè)備之間的細(xì)微差別矛物。一些基本用例如下所示:
預(yù)覽:在屏幕上顯示圖像
圖像分析:無縫訪問緩沖區(qū)中的圖像以便在算法中使用茫死,例如將其傳入 MLKit
圖片拍攝:保存優(yōu)質(zhì)圖片
這些用例適用于搭載 Android 5.0(API 級(jí)別 21)或更高版本的所有設(shè)備,從而確保了同樣的代碼適用于市場(chǎng)中的大多數(shù)設(shè)備履羞。
?三.實(shí)戰(zhàn)代碼如下:
1.項(xiàng)目引入CameraX的依賴如下:
在項(xiàng)目的build.gradle導(dǎo)入如下配置:
// CameraX 核心庫使用 camera2 實(shí)現(xiàn)
implementation "androidx.camera:camera-camera2:1.0.0-beta07"
// 可以使用CameraView
implementation "androidx.camera:camera-view:1.0.0-alpha14"
// 可以使用供應(yīng)商擴(kuò)展
implementation "androidx.camera:camera-extensions:1.0.0-alpha14"
//camerax的生命周期庫
implementation "androidx.camera:camera-lifecycle:1.0.0-beta07"
2.項(xiàng)目的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代碼如下:
項(xiàng)目的主要3個(gè)功能方法:
3.1峦萎、拍照方法:startCamera()?
? ? /**
? ? * 開始拍照
? ? */
? ? private fun startCamera() {
? ? ? ? cameraExecutor = Executors.newSingleThreadExecutor()
? ? ? ? val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
? ? ? ? cameraProviderFuture.addListener(Runnable {
? ? ? ? ? ? cameraProvider = cameraProviderFuture.get()//獲取相機(jī)信息
? ? ? ? ? ? //預(yù)覽配置
? ? ? ? ? ? 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) //設(shè)置高寬比
//? ? ? ? ? ? ? ? .setTargetRotation(viewFinder.display.rotation)//設(shè)置旋轉(zhuǎn)角度
//? ? ? ? ? ? ? ? .setAudioRecordSource(AudioSource.MIC)//設(shè)置音頻源麥克風(fēng)
? ? ? ? ? ? ? ? .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) {
? ? ? ? ? ? ? ? //保存視頻成功回調(diào)忆首,會(huì)在停止錄制時(shí)被調(diào)用
? ? ? ? ? ? ? ? ToastUtils.shortToast(" 錄像成功 $file.absolutePath")
? ? ? ? ? ? }
? ? ? ? ? ? override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
? ? ? ? ? ? ? ? //保存失敗的回調(diào)爱榔,可能在開始或結(jié)束錄制時(shí)被調(diào)用
? ? ? ? ? ? ? ? Log.e("", "onError: $message")
? ? ? ? ? ? ? ? ToastUtils.shortToast(" 錄像失敗 $message")
? ? ? ? ? ? }
? ? ? ? })
? ? btnVideo.setOnClickListener {
? ? ? ? videoCapture?.stopRecording()//停止錄制
? ? ? ? //preview?.clear()//清除預(yù)覽
? ? ? ? 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//當(dāng)前相機(jī)
? ? var preview: Preview? = null//預(yù)覽對(duì)象
? ? var cameraProvider: ProcessCameraProvider? = null//相機(jī)信息
? ? var camera: Camera? = null//相機(jī)對(duì)象
? ? 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) {
? ? ? ? ? ? ? ? ? ? //保存視頻成功回調(diào)详幽,會(huì)在停止錄制時(shí)被調(diào)用
? ? ? ? ? ? ? ? ? ? ToastUtils.shortToast(" 錄像成功 $file.absolutePath")
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) {
? ? ? ? ? ? ? ? ? ? //保存失敗的回調(diào),可能在開始或結(jié)束錄制時(shí)被調(diào)用
? ? ? ? ? ? ? ? ? ? Log.e("", "onError: $message")
? ? ? ? ? ? ? ? ? ? ToastUtils.shortToast(" 錄像失敗 $message")
? ? ? ? ? ? ? ? }
? ? ? ? ? ? })
? ? ? ? btnVideo.setOnClickListener {
? ? ? ? ? ? videoCapture?.stopRecording()//停止錄制
? ? ? ? ? ? //preview?.clear()//清除預(yù)覽
? ? ? ? ? ? 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()//獲取相機(jī)信息
? ? ? ? ? ? //預(yù)覽配置
? ? ? ? ? ? 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) //設(shè)置高寬比
//? ? ? ? ? ? ? ? .setTargetRotation(viewFinder.display.rotation)//設(shè)置旋轉(zhuǎn)角度
//? ? ? ? ? ? ? ? .setAudioRecordSource(AudioSource.MIC)//設(shè)置音頻源麥克風(fēng)
? ? ? ? ? ? ? ? .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.項(xiàng)目封裝的文件工具類:
/**
* @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.項(xiàng)目的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.項(xiàng)目的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"/>
? ? <!--存儲(chǔ)圖像或者視頻權(quán)限-->
? ? <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
? ? <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
? ? <!--錄制音頻權(quán)限-->
? ? <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.運(yùn)行效果如下圖:可以看到拍照浸锨、錄像唇聘,切換攝像頭都是正常的
四、遇到的問題如下:?
1.拍照成功但后臺(tái)打印日志圖片文件寫入失敗柱搜。
2.在Android 10及以上系統(tǒng)提示讀寫文件失敗迟郎。
3.錄像后屏幕黑屏,預(yù)覽失敗聪蘸。
五宪肖、解決方法如下:
1.拍照成功,圖片文件寫入失敗健爬,根據(jù)以前項(xiàng)目的經(jīng)驗(yàn)沒有配置FileProvider控乾。
2.在項(xiàng)目的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讀寫文件權(quán)限適配如下:
在AndroidManifest的application中設(shè)置android:requestLegacyExternalStorage="true"浑劳。
5.解決錄像后屏幕黑屏阱持,預(yù)覽失敗的方法:由于我在錄像成功后主動(dòng)調(diào)用了清除預(yù)覽的方法夭拌,所以導(dǎo)致黑屏魔熏,預(yù)覽失敗衷咽,注銷此方法即可。
?6.以上就是今天的CameraXApi的使用蒜绽,測(cè)試了小米镶骗、華為、三星躲雅、google鼎姊、oppo、vivo等幾款主流機(jī)型相赁,Android 9相寇、Android 10的系統(tǒng),后面有機(jī)型會(huì)適配Android 11钮科,主邏輯全部使用的是kotlin,實(shí)現(xiàn)了預(yù)覽唤衫、拍照、錄像绵脯、切換前后置攝像頭等功能佳励,當(dāng)然本文沒有仔細(xì)展開講解和Camera1、Camera2的區(qū)別蛆挫,因?yàn)檫@塊內(nèi)容很多赃承,所以后面有時(shí)間整理一下,本文還有很多不足之處悴侵,望大家諒解瞧剖,有問題及時(shí)提出,共同學(xué)習(xí)進(jìn)步可免。
最后筒繁,項(xiàng)目的源碼如下: