都 2025 年了,你的鸿蒙相机还停留在“示例 Demo 水平”?
看了这么多流程,如果 UI 还停留在“一个 XComponent + 三个 Button”的 Demo 水平,那就太可惜了。我们来设计一个更像产品的页面:上方:相机预览中间:辅助信息层(比如对焦框、提示文字)左:缩略图(最近一张照片)中:拍照 / 录像按钮右:切换前后摄像头如果你看完这篇,还能在脑子里清楚画出这条线:相机权限 ✅ → 初始化 CameraManager & Device ✅ → 建
大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
一、CameraX 思路:我们要的是“用例驱动”的相机,而不是一坨状态机
先说清楚一点:
CameraX 是 Android 上的一个高级相机库,它的设计思路非常值得借鉴:
- 以 UseCase 为中心(Preview / ImageCapture / VideoCapture)
- 自动帮你处理生命周期、线程、Camera 选择等复杂细节
- 使用方式非常简洁
在鸿蒙上,我们完全可以 模仿 CameraX 的思想:
→ “我不想每次都直接跟底层 camera session 互怼,
→ 我只想写:我要预览 / 我要拍照 / 我要录像。”
所以我们可以做一层自己的 “类 CameraX 封装”:
-
CameraController:- 管理 Camera 设备、生命周期、状态
- 负责打开 / 关闭 / 切换前后摄像头
-
PreviewUseCase:- 负责预览画面绑定到 UI(XComponent / Surface 等)
-
PhotoUseCase:- 负责拍照、输出图片数据
-
VideoUseCase:- 负责开启 / 停止录像,并输出文件路径
你没必要真的叫这些名字,但“按职责拆分”这个思路一定要有,不然代码会拧成一团。
二、相机权限:不把权限搞清楚,你连第一帧预览都看不到
说实话,相机相关问题,有相当一部分是权限没配对。
我们先把权限这块讲清楚,再谈相机本身。
2.1 需要哪些权限?
典型拍照 / 录像 + 写入相册场景,基本要这些:
ohos.permission.CAMERA—— 相机采集ohos.permission.MICROPHONE—— 录像录音ohos.permission.READ_MEDIA—— 读取媒体(有时用于相册读取)ohos.permission.WRITE_MEDIA—— 写入媒体(保存照片 / 视频)
有的版本会把读写合并为媒体访问相关权限,整体方向一样:
相机 + 麦克风 + 媒体存储 这三件事。
2.2 在 module.json5 中静态声明
{
"module": {
// ...
"requestPermissions": [
{
"name": "ohos.permission.CAMERA"
},
{
"name": "ohos.permission.MICROPHONE"
},
{
"name": "ohos.permission.READ_MEDIA"
},
{
"name": "ohos.permission.WRITE_MEDIA"
}
]
}
}
这一步是“跟系统打招呼”:没有声明,很多相机 / 媒体 API 直接不给用。
2.3 动态申请(运行时让用户点“允许”)
相机 & 麦克风一般属于受控权限,需要运行时弹窗申请:
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
async function ensurePermission(context, permission: string): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
const status = await atManager.checkAccessToken(context.tokenId, permission);
if (status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return true;
}
const result = await atManager.requestPermissionsFromUser(context, [permission]);
return result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}
export async function ensureCameraPermissions(context): Promise<boolean> {
const perms = [
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE',
'ohos.permission.READ_MEDIA',
'ohos.permission.WRITE_MEDIA'
];
for (const p of perms) {
const ok = await ensurePermission(context, p);
if (!ok) {
console.error(`【权限被拒绝】${p}`);
return false;
}
}
return true;
}
📌 建议:
- 不要一进 App 就弹,
- 在用户点击“拍照/录像”入口的时候再申请,顺便配一个简单说明:“为了拍照 / 录像,需要相机和麦克风权限”。
三、预览 + 拍照 + 录像:把相机能力拆成几条清晰的链路
大部分相机功能,可以抽象成三条链:
- 预览:Camera → Surface → UI
- 拍照:Camera → Image → 保存文件
- 录像:Camera → VideoStream → 封装为媒体文件
以鸿蒙多媒体 camera 能力为例,大致流程如下(伪代码+思路):
3.1 初始化 Camera 管理器 & 选择设备
import camera from '@ohos.multimedia.camera';
class CameraController {
private cameraManager: camera.CameraManager | null = null;
private cameraDevice: camera.CameraDevice | null = null;
private session: camera.Session | null = null;
private previewOutput: camera.PreviewOutput | null = null;
private photoOutput: camera.PhotoOutput | null = null;
private videoOutput: camera.VideoOutput | null = null;
constructor(private context) {}
async init() {
this.cameraManager = camera.getCameraManager(this.context);
const cameras = this.cameraManager.getSupportedCameras();
// 简单选一个后置摄像头
this.cameraDevice = cameras.find(c => c.position === camera.CameraPosition.REAR) ?? cameras[0];
}
}
实战建议:
- 把“选择前/后摄像头”封装成单独方法
switchCamera()- 对“没有前摄像头”这种情况给出兼容处理
3.2 绑定预览 Surface(XComponent)
在 ArkUI 里面,要把相机画面渲染出来,通常会用一个 XComponent 作为渲染目标(Surface)。
UI 侧:
import camera from '@ohos.multimedia.camera';
@Entry
@Component
struct CameraPage {
private controller: CameraController = new CameraController(getContext(this));
private surfaceId: string = '';
async aboutToAppear() {
const ok = await ensureCameraPermissions(getContext(this));
if (!ok) {
// TODO: 打提示
return;
}
await this.controller.init();
}
onPreviewReady(surfaceId: string) {
this.surfaceId = surfaceId;
this.controller.startPreview(surfaceId);
}
build() {
Column() {
XComponent({
id: 'cameraPreview',
type: 'surface',
controller: new XComponentController()
})
.onLoad((component) => {
const surfaceId = component.getSurfaceId();
this.onPreviewReady(surfaceId);
})
.width('100%')
.height('70%')
// 下方按钮
Row() {
Button('拍照')
.onClick(() => this.controller.takePhoto());
Button('开始录像')
.onClick(() => this.controller.startRecord());
Button('停止录像')
.onClick(() => this.controller.stopRecord());
}.justifyContent(FlexAlign.Center)
}
}
}
控制器侧:
async startPreview(surfaceId: string) {
if (!this.cameraManager || !this.cameraDevice) return;
const previewProfile = this.cameraManager.getSupportedOutputCapability(this.cameraDevice).previewProfiles[0];
const surface = camera.createSurfaceFromId(surfaceId); // 示意
this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, surface);
this.session = this.cameraManager.createSession();
this.session.beginConfig();
this.session.addOutput(this.previewOutput);
this.session.commitConfig();
this.session.start();
}
这块具体 API 根据版本略有差异,但思路都是:
拿到 Camera → 选 profile → 创建 Output(预览)→ 建 session → 绑定 surface → start()
3.3 拍照:触发 PhotoOutput 的 capture
接下来看拍照链路。
- 配置
PhotoOutput - 调用
capture() - 拿到图片数据 → 落盘 → 写入相册
简化示例(控制器内部):
async preparePhotoOutput() {
const cap = this.cameraManager.getSupportedOutputCapability(this.cameraDevice);
const photoProfile = cap.photoProfiles[0];
// 创建一个 photoOutput(一般基于某种 image 或 buffer)
this.photoOutput = this.cameraManager.createPhotoOutput(photoProfile);
this.photoOutput.on('photoAvailable', async (photo: camera.Photo) => {
// 拿到图片数据
const buffer = photo.main; // ArrayBuffer
await this.savePhotoToGallery(buffer);
});
this.session.beginConfig();
this.session.addOutput(this.photoOutput);
this.session.commitConfig();
}
async takePhoto() {
if (!this.photoOutput) {
await this.preparePhotoOutput();
}
this.photoOutput.capture();
}
这里我们先不纠结具体类型,重点放在“拍照 → 回调拿数据 → 保存”这一整条链。
3.4 录像:VideoOutput + 媒体录制
录像比拍照稍微复杂一点,因为要 持续向一个媒体文件写入视频流。
一般流程是:
- 配置
VideoOutput,指定编码参数 - 创建
Recorder或类似对象 - session 中加入 videoOutput
- startRecord() / stop()
简化版伪代码(思路):
async prepareVideoOutput(filePath: string) {
const cap = this.cameraManager.getSupportedOutputCapability(this.cameraDevice);
const videoProfile = cap.videoProfiles[0];
this.videoOutput = this.cameraManager.createVideoOutput(videoProfile);
// 将 videoOutput 与 recorder 关联(不同版本写法不同)
this.session.beginConfig();
this.session.addOutput(this.videoOutput);
this.session.commitConfig();
// 同时初始化 recorder 写入 filePath
}
async startRecord() {
// 启动相机 session(若未启动)
this.session.start();
// 启动 recorder 或 videoOutput
// this.videoOutput.start(); 等
}
async stopRecord() {
// 停止 recorder
// this.videoOutput.stop();
// 停止 session 或保持 preview
}
真实项目中这块要对照 SDK 示例,但结构是一样的:相机出视频流 → 媒体录制器按编码配置写成文件。
四、相册写入:拍完不入库,用户就永远找不到
很多人拍照写完文件就结束了,结果用户在相册找不到刚刚拍的照片,以为你没保存。
要让媒体真正出现在系统图库里,得走一把 媒体库(MediaLibrary)。
4.1 使用 MediaLibrary 写入图片 / 视频
典型写入流程(示意):
import mediaLibrary from '@ohos.multimedia.mediaLibrary';
async savePhotoToGallery(buffer: ArrayBuffer): Promise<string> {
const context = this.context;
const ml = mediaLibrary.getMediaLibrary(context);
const fileName = `IMG_${Date.now()}.jpg`;
const mediaType = mediaLibrary.MediaType.IMAGE;
const asset = await ml.createAsset(mediaType, fileName, 'Pictures/Camera');
const fd = await asset.open('rw');
// 写数据
const fileIo = await fs.open(fd); // 伪代码视 fs 模块具体用法
await fileIo.write(buffer);
await fileIo.close();
// 通知媒体库刷新
await ml.close();
return asset.uri; // 或者返回 path
}
视频类似,只是媒体类型改成 VIDEO,目录改成 Movies/Camera 之类。
确保:
- 目录使用系统建议的相册路径(例如 Camera)
- 写完后关闭文件描述符
- 必要时触发媒体扫描(一般 MediaLibrary API 会处理)
五、自定义相机 UI 实战:自己做一个“像样的相机页面”
看了这么多流程,如果 UI 还停留在“一个 XComponent + 三个 Button”的 Demo 水平,那就太可惜了。
我们来设计一个更像产品的页面:
-
上方:相机预览
-
中间:辅助信息层(比如对焦框、提示文字)
-
底部:
- 左:缩略图(最近一张照片)
- 中:拍照 / 录像按钮
- 右:切换前后摄像头
5.1 UI 结构
@Entry
@Component
struct CustomCameraPage {
private cameraCtrl: CameraController = new CameraController(getContext(this));
private xController: XComponentController = new XComponentController();
@State isRecording: boolean = false;
@State usingFront: boolean = false;
async aboutToAppear() {
const ok = await ensureCameraPermissions(getContext(this));
if (!ok) return;
await this.cameraCtrl.init();
}
onPreviewReady() {
const surfaceId = this.xController.getSurfaceId();
this.cameraCtrl.startPreview(surfaceId);
}
build() {
Column() {
// 预览区域
XComponent({
id: 'cameraPreview',
type: 'surface',
controller: this.xController
})
.onLoad(() => this.onPreviewReady())
.width('100%')
.height('70%')
// 可叠加一些 overlay,比如对焦框、提示等,这里先略
// 底部控制条
Row() {
// 左侧缩略图位占位
Circle()
.width(40)
.height(40)
.backgroundColor(0x333333)
// 中间大按钮
Circle()
.width(70)
.height(70)
.backgroundColor(this.isRecording ? 0xff0000 : 0xffffff)
.margin({ left: 50, right: 50 })
.onClick(async () => {
if (this.isRecording) {
await this.cameraCtrl.stopRecord();
this.isRecording = false;
} else {
// 拍照模式 / 录像模式可根据需求切换
await this.cameraCtrl.takePhoto();
}
})
// 右侧切换摄像头
Button(this.usingFront ? '前置' : '后置')
.onClick(async () => {
this.usingFront = !this.usingFront;
await this.cameraCtrl.switchCamera(this.usingFront);
const surfaceId = this.xController.getSurfaceId();
this.cameraCtrl.startPreview(surfaceId);
})
}
.height('30%')
.justifyContent(FlexAlign.Center)
}
.backgroundColor(0x000000)
.width('100%')
.height('100%')
}
}
这个 UI 示例的重点,是告诉你:
- 预览与控制可以完全按产品要求排版
- 相机只是一块“画面源”,UI 完全由你决定
- 拍照、录像、切换摄像头、显示缩略图都可以组合
5.2 可扩展的交互细节
你可以继续往里加很多“产品级体验”:
- 点击预览区域对焦 / 显示对焦动画
- 滑动调整缩放(Pinch 手势 → 调整 Zoom)
- 长按拍照按钮 → 切换成连拍模式
- 左滑打开相册
- 录像时显示录制时长 / 剩余空间提示
在代码层面,都可以围绕 CameraController 增加对应接口,比如:
setZoom(level: number) {}
setFlashMode(mode: 'auto' | 'on' | 'off') {}
lockFocusAt(x: number, y: number) {}
UI 这块完全可以做得很“产品化”,不要满足于 SDK Demo 级别。
六、常见坑 & 心理安慰小合集 😂
最后简单列几个你几乎一定会遇到的坑(至少我都挨过):
-
预览是横的,UI 是竖的
- 记得处理旋转 / 映射,或者使用 SDK 提供的 orientation 控制
-
照片方向颠倒 / 旋转 90°
- EXIF 信息 / orientation 没处理
-
录像没声音
- MIC 权限 / 音频编码配置缺失
-
拍完照片在相册里找不到
- 媒体库没正确写入 / 目录不对 / 没触发扫描
-
后台切换回来相机崩 / 黑屏
- 生命周期没处理:onBackground 里该停的停、该释放的释放
- 回到前台时重新创建 session & preview
写相机真的很少“一把过”,但你把本文这些流程和结构吃透之后,再遇到 bug,至少心里有根线:
——问题一定在 权限 / 会话 / 输出配置 / 媒体库 这几块。
最后一点小结 💬
如果你看完这篇,还能在脑子里清楚画出这条线:
相机权限 ✅ → 初始化 CameraManager & Device ✅ → 建 Session + PreviewOutput ✅
→ 自定义 UI 绑定 Surface ✅ → 拍照链路:PhotoOutput + 保存到相册 ✅
→ 录像链路:VideoOutput + 录制文件 ✅ → 媒体库写入 & 相册可见 ✅
那你已经告别“只能跑 Demo”的阶段了,开始摸到 “可上线产品级相机” 的门槛了。
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐


所有评论(0)