Android 官方关于相机的介绍如下:
https://developer.android.google.cn/media/camera/get-started-with-camera?hl=zh_cn
一、开始使用 Android 相机
Android相机一般包含前置摄像头和后置摄像头,使用相机可以开发一系列激动人心的应用,例如拍摄视频和图片以便与社交媒体分享,以及创建文档和二维码扫描等实用程序。
二、通过相机 intent实现拍照、录视频
如果只是使用相机进行拍照或录制视频等基本相机操作,则无需使用CameraX或Camera2,可以直接使用 Intent 方式。
2.1 使用相机应用拍照
通过调用Intent来拍照:
static final int REQUEST_IMAGE_CAPTURE = 1;private void dispatchTakePictureIntent() {Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);try {startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);} catch (ActivityNotFoundException e) {// display error state to the user}
}
2.2 使用相机应用录制视频
通过调用Intent来录制视频:
static final int REQUEST_VIDEO_CAPTURE = 1;private void dispatchTakeVideoIntent() {Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);if (takeVideoIntent.resolveActivity(getPackageManager()) != null) {startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE);}else {//display error state to the user}
}
清单文件中添加:
<queries><intent><action android:name="android.media.action.IMAGE_CAPTURE" /></intent><intent><action android:name="android.media.action.VIDEO_CAPTURE" /></intent></queries>
使用Intent.resolveActivity方法时,需要传入一个PackageManager对象。如果Intent可以解析为一个Activity,该方法会返回一个ComponentName;否则返回null。
通过resolveActivity方法,可以检查视频录制页面是否存在,确保应用不会崩溃。
注:resolveActivity方法会提示在清单文件中添加标签,可以查看下面的文章:
Android <queries>声明的作用和配置方法
2.3 使用建议
对于基本的相机操作,请使用 Intent方式。否则,建议您使用 Camera2 和 CameraX 库来处理比基本图片或视频拍摄更复杂的任务。
如果要向 Android 应用中添加相机功能,您有以下三个主要选项:
- CameraX
- Camera2
- Camera(已废弃)
对于大多数开发者,建议使用 CameraX。CameraX 是一个 Jetpack 库,支持绝大多数 Android 设备(Android 5.0 及更高版本),并提供围绕常见用例设计的一致高级别 API。CameraX 会为您解决设备兼容性问题,因此您无需向应用添加针对特定设备的代码。
CameraX 基于 Camera2 软件包构建而成。如果您需要低级别的相机控件来支持复杂的用例,Camera2 是一个不错的选择,但 API 比 CameraX 更复杂。您需要管理设备专属配置。与 CameraX 一样,Camera2 适用于 Android 5.0(API 级别 21)及更高版本。
原始 Android Camera 类已废弃。新应用应使用 CameraX(推荐)或 Camera2,而现有应用应进行迁移,以充分利用新功能,并避免不再兼容未来设备。
三、通过CameraX实现拍照、录视频
CameraX 是一个 Jetpack 库,旨在帮助您更轻松地开发相机应用。
如果您要开发新应用,我们建议您从 CameraX 开始。它提供了一个一致且易于使用的 API,该 API 适用于绝大多数 Android 设备,并向后兼容 Android 5.0(API 级别 21)。
CameraX官方接入步骤:
https://developer.android.google.cn/codelabs/camerax-getting-started?hl=zh-cn#2
3.1 请求必要的权限
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"android:maxSdkVersion="28" />
//https://github.com/Blankj/AndroidUtilCode 工具类implementation 'com.blankj:utilcodex:1.31.1'
PermissionUtils.permission(Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO).callback(new PermissionUtils.SimpleCallback() {@Overridepublic void onGranted() {startActivity(new Intent(mContext, CameraXActivity.class));}@Overridepublic void onDenied() {ToastUtils.showShort("请开启相应权限,否则无法拍摄视频!");}}).request();
3.2 实现摄像头 Preview 预览
private VideoCapture<Recorder> videoCapture;private ImageCapture imageCapture;private Recording recording;/*** 开启摄像头*/public void startCamera() {//创建 ProcessCameraProvider 的实例。这用于将相机的生命周期绑定到生命周期所有者。这消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力。ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(mContext);//向 cameraProviderFuture 添加监听器。添加 Runnable 作为一个参数。添加 ContextCompat.getMainExecutor() 作为第二个参数。这将返回一个在主线程上运行的 Executor。cameraProviderFuture.addListener(new Runnable() {@Overridepublic void run() {// 在 Runnable 中,添加 ProcessCameraProvider。它用于将相机的生命周期绑定到应用进程中的 LifecycleOwnertry {ProcessCameraProvider cameraProvider = cameraProviderFuture.get();// 初始化 Preview 对象,在其上调用 build,从取景器中获取 Surface 提供程序,然后在预览上进行设置。Preview preview = new Preview.Builder().build();preview.setSurfaceProvider(mViewBinding.viewFinder.getSurfaceProvider());//创建 ImageCapture 用例imageCapture = new ImageCapture.Builder().build();//创建 VideoCapture 用例Recorder recorder = new Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.HIGHEST,FallbackStrategy.higherQualityOrLowerThan(Quality.SD))).build();videoCapture = VideoCapture.withOutput(recorder);// 创建 CameraSelector 对象,然后选择 DEFAULT_BACK_CAMERA(后置摄像头)CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;try {//创建一个 try 代码块。在此块内,确保没有任何内容绑定到 cameraProvidercameraProvider.unbindAll();// 然后将 cameraSelector 和预览对象绑定到 cameraProvidercameraProvider.bindToLifecycle(CameraXActivity.this, cameraSelector, preview, imageCapture, videoCapture);} catch (Exception e) {LogUtils.e("Use case binding failed", e.getMessage());}} catch (Exception e) {LogUtils.e(e.getMessage());}}}, ContextCompat.getMainExecutor(mContext));}
3.3 实现拍照
/*** 拍照*/private void takePhoto() {// 首先,获取对 ImageCapture 用例的引用。如果用例为 null,请退出函数。如果在设置图片拍摄之前点按“photo”按钮,它将为 null。如果没有 return 语句,应用会在该用例为 null 时崩溃。if (imageCapture == null) {return;}//接下来,创建用于保存图片的 MediaStore 内容值。请使用时间戳,确保 MediaStore 中的显示名是唯一的。String name = TimeUtil.getCurrentTimeName();ContentValues contentValues = new ContentValues();contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image");}// 创建一个 OutputFileOptions 对象。在该对象中,您可以指定所需的输出内容。我们希望将输出保存在 MediaStore 中,以便其他应用可以显示它,因此,请添加我们的 MediaStore 条目。ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(getContentResolver(),MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues).build();// 对 imageCapture 对象调用 takePicture()。传入 outputOptions、执行器和保存图片时使用的回调。imageCapture.takePicture(outputOptions,ContextCompat.getMainExecutor(this),new ImageCapture.OnImageSavedCallback() {@Overridepublic void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {//照片拍摄成功!将照片保存到我们之前创建的文件中,显示消息框,让用户知道照片已拍摄成功,并输出日志语句。String msg = "Photo capture succeeded: " + outputFileResults.getSavedUri();ToastUtils.showShort(name + "\n" + msg);LogUtils.i(msg);}@Overridepublic void onError(@NonNull ImageCaptureException exception) {//如果图片拍摄失败或保存图片失败,请添加错误情况以记录失败。LogUtils.e("Photo capture failed:" + exception.getMessage(), exception);}});}
3.4 实现录制视频
/*** 录制视频*/private void captureVideo() {//检查是否已创建 VideoCapture 用例:如果尚未创建,则不执行任何操作。if (videoCapture == null) {return;}// 在 CameraX 完成请求操作之前,停用按钮;在后续步骤中,它会在我们的已注册的 VideoRecordListener 内重新启用。mViewBinding.videoCaptureButton.setEnabled(false);//如果有正在进行的录制操作,请将其停止并释放当前的 recording。当所捕获的视频文件可供我们的应用使用时,我们会收到通知。Recording curRecording = recording;if (curRecording != null) {curRecording.stop();recording = null;return;}//为了开始录制,我们会创建一个新的录制会话。首先,我们创建预定的 MediaStore 视频内容对象,将系统时间戳作为显示名(以便我们可以捕获多个视频)。String name = TimeUtil.getCurrentTimeName();ContentValues contentValues = new ContentValues();contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");//使用外部内容选项创建 MediaStoreOutputOptions.Builder。MediaStoreOutputOptions mediaStoreOutputOptions = new MediaStoreOutputOptions.Builder(getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI).setContentValues(contentValues).build();//检测录音权限if (ActivityCompat.checkSelfPermission(this,Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {return;}//将输出选项配置为 VideoCapture<Recorder> 的 Recorder 并启用录音:recording = videoCapture.getOutput().prepareRecording(mContext, mediaStoreOutputOptions).withAudioEnabled().start(ContextCompat.getMainExecutor(this),new Consumer<VideoRecordEvent>() {@Overridepublic void accept(VideoRecordEvent videoRecordEvent) {if (videoRecordEvent instanceof VideoRecordEvent.Start) {//当相机设备开始请求录制时mViewBinding.videoCaptureButton.setText(R.string.stop_capture);mViewBinding.videoCaptureButton.setEnabled(true);} else if (videoRecordEvent instanceof VideoRecordEvent.Finalize) {//完成录制后,用消息框通知用户,并将“Stop Capture”按钮切换回“Start Capture”,然后重新启用它:if (!((VideoRecordEvent.Finalize) videoRecordEvent).hasError()) {String msg = "Video capture succeeded: " + ((VideoRecordEvent.Finalize) videoRecordEvent).getOutputResults().getOutputUri();ToastUtils.showShort(msg);LogUtils.e(msg);} else {if (recording != null) {recording.close();recording = null;LogUtils.e("Video capture ends with error: " + ((VideoRecordEvent.Finalize) videoRecordEvent).getError());}}mViewBinding.videoCaptureButton.setText(R.string.start_capture);mViewBinding.videoCaptureButton.setEnabled(true);}}});}
3.5 完整代码
添加 CameraX 依赖项:
dependencies {def camerax_version = "1.5.0-alpha01"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-video:${camerax_version}"implementation "androidx.camera:camera-view:${camerax_version}"implementation "androidx.camera:camera-extensions:${camerax_version}"
}
CameraX 需要一些属于 Java 8 的方法,因此我们需要相应地设置编译选项。在 android 代码块的末尾,紧跟在 buildTypes 之后,添加以下代码:
compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {viewBinding = truebuildConfig = true}
<!--CameraX--><string name="take_photo">拍照</string><string name="start_capture">开始录制</string><string name="stop_capture">结束录制</string><string name="camerax_tip">请开启相应权限,否则无法拍摄视频!</string>
activity_camera_x.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"><androidx.camera.view.PreviewViewandroid:id="@+id/viewFinder"android:layout_width="match_parent"android:layout_height="match_parent" /><Buttonandroid:id="@+id/image_capture_button"android:layout_width="110dp"android:layout_height="110dp"android:layout_marginBottom="50dp"android:layout_marginEnd="50dp"android:elevation="2dp"android:text="@string/take_photo"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintEnd_toStartOf="@id/vertical_centerline" /><Buttonandroid:id="@+id/video_capture_button"android:layout_width="110dp"android:layout_height="110dp"android:layout_marginBottom="50dp"android:layout_marginStart="50dp"android:elevation="2dp"android:text="@string/start_capture"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toEndOf="@id/vertical_centerline" /><androidx.constraintlayout.widget.Guidelineandroid:id="@+id/vertical_centerline"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"app:layout_constraintGuide_percent=".50" /></androidx.constraintlayout.widget.ConstraintLayout>
public class CameraXActivity extends BaseVBActivity<ActivityCameraXBinding> {private VideoCapture<Recorder> videoCapture;private ImageCapture imageCapture;private Recording recording;@Overrideprotected void initView() {startCamera();}/*** 开启摄像头*/public void startCamera() {//创建 ProcessCameraProvider 的实例。这用于将相机的生命周期绑定到生命周期所有者。这消除了打开和关闭相机的任务,因为 CameraX 具有生命周期感知能力。ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(mContext);//向 cameraProviderFuture 添加监听器。添加 Runnable 作为一个参数。添加 ContextCompat.getMainExecutor() 作为第二个参数。这将返回一个在主线程上运行的 Executor。cameraProviderFuture.addListener(new Runnable() {@Overridepublic void run() {// 在 Runnable 中,添加 ProcessCameraProvider。它用于将相机的生命周期绑定到应用进程中的 LifecycleOwnertry {ProcessCameraProvider cameraProvider = cameraProviderFuture.get();// 初始化 Preview 对象,在其上调用 build,从取景器中获取 Surface 提供程序,然后在预览上进行设置。Preview preview = new Preview.Builder().build();preview.setSurfaceProvider(mViewBinding.viewFinder.getSurfaceProvider());//创建 ImageCapture 用例imageCapture = new ImageCapture.Builder().build();//创建 VideoCapture 用例Recorder recorder = new Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.HIGHEST,FallbackStrategy.higherQualityOrLowerThan(Quality.SD))).build();videoCapture = VideoCapture.withOutput(recorder);// 创建 CameraSelector 对象,然后选择 DEFAULT_BACK_CAMERA(后置摄像头)CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;try {//创建一个 try 代码块。在此块内,确保没有任何内容绑定到 cameraProvidercameraProvider.unbindAll();// 然后将 cameraSelector 和预览对象绑定到 cameraProvidercameraProvider.bindToLifecycle(CameraXActivity.this, cameraSelector, preview, imageCapture, videoCapture);} catch (Exception e) {LogUtils.e("Use case binding failed", e.getMessage());}} catch (Exception e) {LogUtils.e(e.getMessage());}}}, ContextCompat.getMainExecutor(mContext));}/*** 设置点击事件*/@Overrideprotected void setOnClick() {mViewBinding.imageCaptureButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {ToastUtils.showShort("拍照");takePhoto();}});mViewBinding.videoCaptureButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {ToastUtils.showShort("录制视频");captureVideo();}});}/*** 拍照*/private void takePhoto() {// 首先,获取对 ImageCapture 用例的引用。如果用例为 null,请退出函数。如果在设置图片拍摄之前点按“photo”按钮,它将为 null。如果没有 return 语句,应用会在该用例为 null 时崩溃。if (imageCapture == null) {return;}//接下来,创建用于保存图片的 MediaStore 内容值。请使用时间戳,确保 MediaStore 中的显示名是唯一的。String name = TimeUtil.getCurrentTimeName();ContentValues contentValues = new ContentValues();contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image");}// 创建一个 OutputFileOptions 对象。在该对象中,您可以指定所需的输出内容。我们希望将输出保存在 MediaStore 中,以便其他应用可以显示它,因此,请添加我们的 MediaStore 条目。ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(getContentResolver(),MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues).build();// 对 imageCapture 对象调用 takePicture()。传入 outputOptions、执行器和保存图片时使用的回调。imageCapture.takePicture(outputOptions,ContextCompat.getMainExecutor(this),new ImageCapture.OnImageSavedCallback() {@Overridepublic void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {//照片拍摄成功!将照片保存到我们之前创建的文件中,显示消息框,让用户知道照片已拍摄成功,并输出日志语句。String msg = "Photo capture succeeded: " + outputFileResults.getSavedUri();ToastUtils.showShort(name + "\n" + msg);LogUtils.i(msg);}@Overridepublic void onError(@NonNull ImageCaptureException exception) {//如果图片拍摄失败或保存图片失败,请添加错误情况以记录失败。LogUtils.e("Photo capture failed:" + exception.getMessage(), exception);}});}/*** 录制视频*/private void captureVideo() {//检查是否已创建 VideoCapture 用例:如果尚未创建,则不执行任何操作。if (videoCapture == null) {return;}// 在 CameraX 完成请求操作之前,停用按钮;在后续步骤中,它会在我们的已注册的 VideoRecordListener 内重新启用。mViewBinding.videoCaptureButton.setEnabled(false);//如果有正在进行的录制操作,请将其停止并释放当前的 recording。当所捕获的视频文件可供我们的应用使用时,我们会收到通知。Recording curRecording = recording;if (curRecording != null) {curRecording.stop();recording = null;return;}//为了开始录制,我们会创建一个新的录制会话。首先,我们创建预定的 MediaStore 视频内容对象,将系统时间戳作为显示名(以便我们可以捕获多个视频)。String name = TimeUtil.getCurrentTimeName();ContentValues contentValues = new ContentValues();contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");//使用外部内容选项创建 MediaStoreOutputOptions.Builder。MediaStoreOutputOptions mediaStoreOutputOptions = new MediaStoreOutputOptions.Builder(getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI).setContentValues(contentValues).build();//检测录音权限if (ActivityCompat.checkSelfPermission(this,Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {return;}//将输出选项配置为 VideoCapture<Recorder> 的 Recorder 并启用录音:recording = videoCapture.getOutput().prepareRecording(mContext, mediaStoreOutputOptions).withAudioEnabled().start(ContextCompat.getMainExecutor(this),new Consumer<VideoRecordEvent>() {@Overridepublic void accept(VideoRecordEvent videoRecordEvent) {if (videoRecordEvent instanceof VideoRecordEvent.Start) {//当相机设备开始请求录制时mViewBinding.videoCaptureButton.setText(R.string.stop_capture);mViewBinding.videoCaptureButton.setEnabled(true);} else if (videoRecordEvent instanceof VideoRecordEvent.Finalize) {//完成录制后,用消息框通知用户,并将“Stop Capture”按钮切换回“Start Capture”,然后重新启用它:if (!((VideoRecordEvent.Finalize) videoRecordEvent).hasError()) {String msg = "Video capture succeeded: " + ((VideoRecordEvent.Finalize) videoRecordEvent).getOutputResults().getOutputUri();ToastUtils.showShort(msg);LogUtils.e(msg);} else {if (recording != null) {recording.close();recording = null;LogUtils.e("Video capture ends with error: " + ((VideoRecordEvent.Finalize) videoRecordEvent).getError());}}mViewBinding.videoCaptureButton.setText(R.string.start_capture);mViewBinding.videoCaptureButton.setEnabled(true);}}});}
}