Andoird中拍照、錄像是很常見的功能,但是系統相機的Api目前發生了很大的變化,有Camera1、Camera2、CameraX三個api,每個api的使用和方法都不一樣,如果做過相機開發的小伙伴應該會很頭疼這三個api在不同安卓系統手機的適配,由於目前的App有一部分工作涉及到這部分,所以總結了一下,目前由基礎到深入慢慢總結.
一.簡介:(官方介紹如下)
CameraX 是一個 Jetpack 支持庫,旨在幫助您簡化相機應用的開發工作。它提供一致且易用的 API 接口,適用於大多數 Android 設備,並可向后兼容至 Android 5.0(API 級別 21)。
具體內容可以參考官網介紹,網站地址為:
二.優勢:(參考官網)
易用性
圖 1. CameraX 以 Android 5.0(API 級別 21)及更高版本為目標平台,涵蓋了大多數 Android 設備
CameraX 引入了多個用例,使您可以專注於需要完成的任務,而無需花時間處理不同設備之間的細微差別。一些基本用例如下所示:
這些用例適用於搭載 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
最后,項目的源碼如下: