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
最后,項目的源碼如下:

















