
三三要成为安卓糕手
零:总体代码
1:UI设计
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CameraVideoRecordActivity">
<androidx.camera.view.PreviewView
android:id="@+id/preview_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_record"
android:layout_width="66dp"
android:layout_height="66dp"
android:src="@mipmap/icon_record"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.95" />
<ImageView
android:id="@+id/iv_switch"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_margin="30dp"
android:src="@mipmap/icon_switch_camera"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

2:Activity代码
java
public class CameraVideoRecordActivity extends AppCompatActivity {
private static final String TAG = "CameraVideoRecordActivity";
private PreviewView previewView;
private ImageView ivRecord;
private ImageView ivSwitch;
private boolean isRecording;
private VideoCapture<Recorder> videoCapture;
private ExecutorService executorService;
private Recording recording;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera_video_record);
previewView = findViewById(R.id.preview_view);
ivRecord = findViewById(R.id.iv_record);
ivSwitch = findViewById(R.id.iv_switch);
ivRecord.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isRecording) {
stopRecording();
} else {
startRecording();
}
}
});
startCamera();
//创建一个子线程,用于处理录制回调事件,避免阻塞主线程的进行
executorService = Executors.newSingleThreadExecutor();
requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO},100);
}
private void startCamera() {
ListenableFuture<ProcessCameraProvider> providerListenableFuture = ProcessCameraProvider.getInstance(this);
providerListenableFuture.addListener(new Runnable() {
@Override
public void run() {
try {
ProcessCameraProvider cameraProvide = providerListenableFuture.get();
//后置摄像头
CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
//创建预览实例,并把预览实例与previewView绑定
Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(previewView.getSurfaceProvider());
//视频录制的实例
Recorder recorder = new Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HD))
.build();
videoCapture = VideoCapture.withOutput(recorder);
//相机绑定生命周期
cameraProvide.unbindAll();
cameraProvide.bindToLifecycle(CameraVideoRecordActivity.this,
cameraSelector, preview,videoCapture);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}, ContextCompat.getMainExecutor(this));
}
/**
* 开始录制
*/
private void startRecording() {
if(isRecording){
Toast.makeText(this,"正在录制中",Toast.LENGTH_SHORT).show();
return;
}
//在contentValues中指定文件名和类型
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME,
"video_" + System.currentTimeMillis() + ".mp4");
contentValues.put(MediaStore.MediaColumns.MIME_TYPE,"video/mp4");
//指定文件输出位置
MediaStoreOutputOptions.Builder builder =
new MediaStoreOutputOptions.Builder(getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues);
MediaStoreOutputOptions build = builder.build();
//检查录音权限申请
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "清先获取录音的权限", Toast.LENGTH_SHORT).show();
return;
}
PendingRecording recording = videoCapture.getOutput().prepareRecording(this, build)
.withAudioEnabled();
//启动录制
this.recording = recording.start(executorService, new Consumer<VideoRecordEvent>() {
@Override
public void accept(VideoRecordEvent videoRecordEvent) {
if (videoRecordEvent instanceof VideoRecordEvent.Start) {
Log.i(TAG, "accept: 开始录制");
isRecording = true;
ivRecord.setImageResource(R.mipmap.icon_record);
} else if(videoRecordEvent instanceof VideoRecordEvent.Finalize){
Log.i(TAG, "accept: 录制结束");
isRecording = false;
ivRecord.setImageResource(R.mipmap.icon_stop_record);
}
}
});
}
private void stopRecording() {
if(recording != null && isRecording){
recording.stop();//停止
recording = null;//释放资源
}
}
@Override
protected void onDestroy() {
super.onDestroy();
executorService.shutdown();
}
}
一:startCamera启动相机
1:创建视频录制实例
这里是跟拍照功能那有一些不一样的地方;这里使用构建设计模式,设置默认录制视频的清晰度
java
//视频录制的实例
Recorder recorder = new Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HD))
.build();
videoCapture = VideoCapture.withOutput(recorder);
二:开始录制功能代码分析
1:视频文件的输出配置

这段代码的主要目的是创建一个视频文件的输出配置,告诉系统视频文件应该存储在哪里、以什么信息保存(文件名、格式等),以便后续视频录制完成后能正确写入到系统中。

(1)MediaStoreOutputOptions.Builder
构建器
java
MediaStoreOutputOptions.Builder builder =
new MediaStoreOutputOptions.Builder(getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues);
-
构造参数 1:
getContentResolver()
获取内容解析器(
ContentResolver
),用于访问 Android 系统的媒体库(如照片、视频库)。 -
构造参数 2:
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
指定视频文件的存储位置 ------外部存储的系统视频媒体库(即系统默认的 "视频" 文件夹),之前用过的
-
.setContentValues(contentValues)
将之前创建的
contentValues
(包含文件名、MIME 类型等信息)设置到构建器中,这些信息会被用于创建视频文件。
(2)MediaStoreOutputOptions
MediaStoreOutputOptions
:CameraX 库中用于配置媒体文件(视频 / 图片)输出位置和属性的类,确保文件能正确写入系统媒体库。- 为什么用这种方式存储?
直接通过MediaStore
存储视频,会让文件自动出现在系统相册 / 视频库中,无需手动刷新媒体库;同时符合 Android 10 + 的存储权限规范(无需申请WRITE_EXTERNAL_STORAGE
权限)。
2:录音权限检查


在启动带音频的视频录制前,检查应用是否已获得录音权限 (RECORD_AUDIO
),这是 Android 权限管理的强制要求(属于 "危险权限",必须明确申请)。
java
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "请先获取录音的权限", Toast.LENGTH_SHORT).show();
return;
}
3:创建 "待启动" 的录制对象。

-
videoCapture.getOutput()
:
videoCapture
是 CameraX 库中负责视频捕获的核心对象,在startCamera方法中已经创建好了,并且提取了成员变量,getOutput()
用于获取其输出控制器,后续通过它配置录制参数。 -
.prepareRecording(this, build)
:准备录制会话,参数说明:
build
:前面创建的MediaStoreOutputOptions
对象(包含视频存储路径、文件名等配置)
这一步会根据配置初始化录制环境,确定视频文件的存储位置和格式。
-
.withAudioEnabled()
:启用音频录制功能(默认可能只录视频不含声音),使最终生成的视频包含音频轨道;所以这里需要检查录音权限
-
PendingRecording recording
:接收返回的
PendingRecording
对象,它表示 "已准备好但尚未启动" 的录制任务,后续通过它的start()
方法真正开始录制。
4:开始录制
通过调用start()
方法正式启动视频录制,并设置一个回调监听器,实时接收录制过程中的各种状态事件(如开始、结束等),进而更新应用的状态标记和 UI 显示。

(1)启动录制并绑定回调
-
start(...)
:用于正式启动录制,需要传入两个参数:executorService
:一个线程池(ExecutorService
),指定回调事件的处理线程(避免在主线程处理耗时操作)Consumer<VideoRecordEvent>
接口实现,用于接收录制过程中的各种事件(回调函数)。
-
this.recording
:将启动后的录制对象保存到成员变量中,方便后续控制资源释放(如调用stop()
停止录制)。
(2)回调接口Consumer<VideoRecordEvent>
VideoRecordEvent
:CameraX 定义的录制事件基类,包含多种子类,分别表示不同的录制状态(如开始、暂停、结束等)。
java
//回调中的videoRecordEvent会有下面几种状态:
//VideoRecordEvent.Start:录制开始。
//VideoRecordEvent.Pause:录制暂停。
//VideoRecordEvent.Resume:录制恢复。
//VideoRecordEvent.Finalize:录制完成(停止或失败)。
//VideoRecordEvent.Status:录制状态更新(持续获取统计信息)。
accept()
方法:当录制状态发生变化时,系统会自动调用该方法,并传入对应的事件对象recordEvent
。
(3)关键技术点

线程切换 :executorService
指定了回调的执行线程,通常会在回调中通过runOnUiThread()
切换到主线程更新 UI,setImageResource()
属于 UI 操作,需确保在主线程执行
异步回调机制: 录制过程是异步的(在后台执行),通过回调通知 UI 线程状态变化,避免阻塞主线程。
状态管理 :isRecording
变量用于全局跟踪录制状态,防止重复操作(如多次点击开始录制)。
(4)可能的扩展
- 可以增加对其他事件的处理,如
VideoRecordEvent.Pause
(暂停)、VideoRecordEvent.Resume
(恢复)等。 - 可在
Finalize
事件中添加视频保存成功 / 失败的判断(通过recordEvent.getError()
检查是否有错误)。 - 可以在回调中实时更新录制时长(通过
recordEvent.getRecordingStats().getRecordedDurationMs()
获取已录制时长)。
5:效果