安卓CameraX的使用
如果要在安卓应用中操作相机,有三个库可以选:
- Camera(已废弃):Camera是安卓最早的包,目前已废弃,在Android 5.0(API 级别 21)的设备上操作相机可以选择该包,保证兼容性;
- Camera2:Camera2在Android 5.0(API 级别 21)开始提供,用于替代Camera,相比Camera,在性能、灵活性等方面有优势,但是使用起来更复杂;
- CameraX:CameraX是对Camera2的封装,是Jetpack的一个库,支持Android 5.0(API 级别 21)及更高版本。CameraX降低了操作相机的难度,大多数情况下,使用CameraX已足以完成需求,建议使用CameraX。
注:Jetpack是一个由多个库组成的套件,可帮助开发者遵循最佳做法、减少样板代码并编写可在各种Android版本和设备中一致运行的代码,让开发者可将精力集中于真正重要的编码工作。
谷歌的文档只提供了Kotlin的代码示例,根据Kotlin的代码示例和文档,本文提供Java的实现。
1. CameraX常见用例
您可以使用CameraX,借助名为“用例”的抽象概念与设备的相机进行交互。提供的用例如下:
- 预览(Preview):在屏幕上查看相机画面;
- 图片分析(ImageAnalysis):逐帧处理相机捕获到的每一帧画面;
- 图片拍摄(ImageCapture):拍照;
- 视频拍摄(VideoCapture):拍视频(和音频)。
CameraX允许同时使用Preview、VideoCapture、ImageAnalysis和ImageCapture各一个实例。此外:
- 每个用例都可以单独使用。例如,应用可以在不使用预览的情况下录制视频;
- 启用扩展后,只能保证能够使用ImageCapture和Preview的组合。根据OEM实现情况,可能无法同时添加ImageAnalysis;无法为VideoCapture用例启用扩展。如需了解详情,请参阅扩展参考文档;
- 对于某些相机而言,在较低分辨率模式下可以支持的组合,在较高的分辨率下将无法支持,这具体取决于相机的功能;
- 在相机硬件级别为FULL或更低的设备上,组合使用Preview、VideoCapture和ImageCapture或ImageAnalysis可能会迫使CameraX为Preview和VideoCapture复制相机的PRIV数据流。这种重复(称为数据流共享)可让您同时使用这些功能,但代价是增加了处理需求。因此,您可能会遇到略长的延迟时间和缩短的电池续航时间。
2. 代码实现
在build.gradle中添加相关依赖:
// camerax
def camerax_version = "1.3.0"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
// 扩展,本文代码未使用
// implementation "androidx.camera:camera-extensions:${camerax_version}"
在AndroidManifest.xml添加相关权限:
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
添加布局文件activity_camerax_preview.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<androidx.camera.view.PreviewView
android:id="@+id/camerax_preview_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<Button
android:id="@+id/camerax_flash"
android:layout_width="110dp"
android:layout_height="70dp"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="5dp"
android:text="开灯" />
<Button
android:id="@+id/camerax_switch_camera"
android:layout_width="110dp"
android:layout_height="70dp"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="5dp"
android:text="切换摄像头" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<Button
android:id="@+id/camerax_image_capture_button"
android:layout_width="110dp"
android:layout_height="70dp"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="5dp"
android:text="拍照" />
<Button
android:id="@+id/camerax_video_capture_button"
android:layout_width="110dp"
android:layout_height="70dp"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="5dp"
android:text="开始录制" />
<Button
android:id="@+id/camerax_video_capture_pause_button"
android:layout_width="110dp"
android:layout_height="70dp"
android:layout_marginStart="10dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="5dp"
android:text="暂停录制"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
activity类:
package org.tao.hetools.activities;
import android.Manifest;
import android.content.ContentValues;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Size;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Preview;
import androidx.camera.core.TorchState;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.video.MediaStoreOutputOptions;
import androidx.camera.video.Quality;
import androidx.camera.video.QualitySelector;
import androidx.camera.video.Recorder;
import androidx.camera.video.Recording;
import androidx.camera.video.VideoCapture;
import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;
import com.google.common.util.concurrent.ListenableFuture;
import org.tao.hetools.R;
import org.tao.hetools.utils.PermissionUtils;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
public class CameraxPreviewActivity extends ComponentActivity {
private static final String TAG = "CameraxPreviewActivity";
private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmssSSS");
private static final Size SIZE = new Size(1080, 1920);
private static final List<CameraSelector> CAMERAS = Arrays.asList(CameraSelector.DEFAULT_BACK_CAMERA,
CameraSelector.DEFAULT_FRONT_CAMERA);
private static final String START_RECORD = "开始录制";
private static final String STOP_RECORD = "结束录制";
private static final String PAUSE_RECORD = "暂停录制";
private static final String RESUME_RECORD = "恢复录制";
private PreviewView previewView;
private Executor executor;
private Preview preview;
private ImageAnalysis imageAnalysis;
private ImageCapture imageCapture;
private VideoCapture<Recorder> videoCapture;
private Recording videoRecording;
private ProcessCameraProvider cameraProvider;
private Camera camera;
private boolean isRecordingStart = false;
private boolean isRecordingPause = false;
private int cameraIndex = 0;
private Button flashButton;
private Button pauseResumeVideoButton;
private Button startShopVideoButton;
private Button switchCameraButton;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_camerax_preview);
PermissionUtils.checkPermission(this, Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE);
previewView = findViewById(R.id.camerax_preview_view);
executor = ContextCompat.getMainExecutor(this);
initCamerax();
initButton();
}
private void initCamerax() {
// 可以将相机生命周期绑定到activity,从而免去打开、关闭相机的任务
ListenableFuture<ProcessCameraProvider> cameraProviderListenableFuture = ProcessCameraProvider.getInstance(this);
cameraProviderListenableFuture.addListener(() -> {
try {
cameraProvider = cameraProviderListenableFuture.get();
// 预览
preview = new Preview.Builder().build();
preview.setSurfaceProvider(previewView.getSurfaceProvider());
// 图片分析
imageAnalysis = new ImageAnalysis.Builder()
// 设置输出图片格式
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
// 非阻塞模式,只取最新的图片。若需要每帧图片都处理,使用阻塞模式 ImageAnalysis.STRATEGY_BLOCK_PRODUCER
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetResolution(SIZE)
.build();
imageAnalysis.setAnalyzer(executor, (imageProxy) -> {
// close后才认为处理完成当前帧,所以放到try-with-resources语句中
try (imageProxy) {
// 在此处处理图片,如对图片进行人脸、条码识别等
Log.i(TAG, "接收到一帧图片" + SIMPLE_DATE_FORMAT.format(new Date()));
} catch (Exception exception) {
Log.w(TAG, exception.getMessage());
}
});
// 拍照
imageCapture = new ImageCapture.Builder()
.setFlashMode(ImageCapture.FLASH_MODE_AUTO)
.setTargetResolution(SIZE)
.build();
// 录制
Recorder recorder = new Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.FHD))
.build();
videoCapture = VideoCapture.withOutput(recorder);
cameraProvider.unbindAll();
camera = cameraProvider.bindToLifecycle(this, CAMERAS.get(cameraIndex), preview,
imageCapture, videoCapture);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, executor);
}
private void initButton() {
initFlashButton();
initSwitchButton();
initImageCaptureButton();
initVideoCaptureButtons();
}
/**
* 视频操作按钮
*/
private void initVideoCaptureButtons() {
startShopVideoButton = findViewById(R.id.camerax_video_capture_button);
pauseResumeVideoButton = findViewById(R.id.camerax_video_capture_pause_button);
// 开始/停止录制视频
startShopVideoButton.setOnClickListener(view -> {
if (videoCapture == null || executor == null) {
return;
}
if (isRecordingStart) {
videoRecording.stop();
videoRecording = null;
isRecordingStart = false;
startShopVideoButton.setText(START_RECORD);
// 视频录制停止后不显示"暂停/恢复录制"按钮
pauseResumeVideoButton.setVisibility(View.GONE);
// 录制视频完成后显示切换摄像头按钮
switchCameraButton.setVisibility(View.VISIBLE);
return;
}
PermissionUtils.checkPermission(this, Manifest.permission.RECORD_AUDIO);
videoRecording = videoCapture.getOutput()
.prepareRecording(this, getMediaStoreOutputOptions())
.withAudioEnabled()
.start(executor, videoRecordEvent -> {
});
isRecordingStart = true;
startShopVideoButton.setText(STOP_RECORD);
pauseResumeVideoButton.setText(PAUSE_RECORD);
// 视频录制开始后显示"暂停/恢复录制"按钮
pauseResumeVideoButton.setVisibility(View.VISIBLE);
// 录制视频期间不允许切换摄像头,切换摄像头会终止录制
switchCameraButton.setVisibility(View.GONE);
});
// 暂停/恢复录制视频
pauseResumeVideoButton.setOnClickListener(view -> {
if (videoCapture == null || executor == null || videoRecording == null || !isRecordingStart) {
return;
}
if (isRecordingPause) {
isRecordingPause = false;
videoRecording.resume();
pauseResumeVideoButton.setText(PAUSE_RECORD);
} else {
videoRecording.pause();
isRecordingPause = true;
pauseResumeVideoButton.setText(RESUME_RECORD);
}
});
}
/**
* 拍照按钮
*/
private void initImageCaptureButton() {
findViewById(R.id.camerax_image_capture_button).setOnClickListener(view -> {
if (imageCapture == null || executor == null) {
return;
}
imageCapture.takePicture(getImageOutputFileOptions(), executor,
new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
Log.i(TAG, "save picture success");
Toast.makeText(CameraxPreviewActivity.this, "success", Toast.LENGTH_SHORT).show();
}
@Override
public void onError(@NonNull ImageCaptureException exception) {
Log.w(TAG, "error" + exception.getMessage());
Toast.makeText(CameraxPreviewActivity.this, "fail", Toast.LENGTH_SHORT).show();
}
});
});
}
/**
* 切换摄像头按钮
*/
private void initSwitchButton() {
switchCameraButton = findViewById(R.id.camerax_switch_camera);
switchCameraButton.setOnClickListener((view -> {
if (cameraProvider == null) {
return;
}
if (++cameraIndex >= CAMERAS.size()) {
cameraIndex = 0;
}
cameraProvider.unbindAll();
cameraProvider.bindToLifecycle(this, CAMERAS.get(cameraIndex), preview, imageAnalysis,
imageCapture, videoCapture);
resetButtonStatus(cameraIndex);
}));
}
/**
* 闪光灯按钮
*/
private void initFlashButton() {
flashButton = findViewById(R.id.camerax_flash);
flashButton.setOnClickListener(view -> {
if (camera == null) {
return;
}
boolean isTorchOff = camera.getCameraInfo().getTorchState().getValue() == TorchState.OFF;
camera.getCameraControl().enableTorch(isTorchOff);
flashButton.setText(camera.getCameraInfo().getTorchState().getValue() == TorchState.ON ? "关灯" : "开灯");
});
}
/**
* 重设按钮状态
*/
private void resetButtonStatus(int cameraIndex) {
flashButton.setText("开灯");
flashButton.setVisibility(cameraIndex == 0 ? View.VISIBLE : View.GONE);
startShopVideoButton.setText(START_RECORD);
pauseResumeVideoButton.setText(PAUSE_RECORD);
pauseResumeVideoButton.setVisibility(View.GONE);
}
private ImageCapture.OutputFileOptions getImageOutputFileOptions() {
return new ImageCapture
.OutputFileOptions.Builder(getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
getContentValues("image/png"))
.build();
}
private MediaStoreOutputOptions getMediaStoreOutputOptions() {
return new MediaStoreOutputOptions.Builder(getContentResolver(),
MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(getContentValues("video/mp4"))
.build();
}
private ContentValues getContentValues(String mimeType) {
String name = SIMPLE_DATE_FORMAT.format(new Date());
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/HeTools");
}
return contentValues;
}
}
参考文章
- 开始使用Android相机