☰
HarmonyOS拍照与相册选图:免权限Picker与CameraKit实战
2026/10/19 9:33:27 网站建设 项目流程

在HarmonyOS上做“拍照+相册选图”这个功能,估计是绝大多数应用都会遇到的需求。我最早接触这套系统时,也是先找两个Ability,点一下拉起系统相机拍照,再点一下进相册选图。结果发现鸿蒙(尤其是HarmonyOS NEXT之后)的思路和Android完全不同——它把媒体访问权限收得很紧,但同时又给了两个非常轻量的Picker组件,让“不申请权限就拿到底图”成了默认路径。

这篇文章记录的是我在HarmonyOS6环境下一个实战案例的完整实现过程,覆盖三条路径:应用内自定义相机、CameraPicker拉起系统相机拍照、PhotoViewPicker相册选图,外加图片保存和压缩处理。如果你正要开发鸿蒙应用里和图片采集相关的功能,这篇内容可以直接照着抄,也能帮你避开我踩过的那些坑。

1. 先想清楚用哪条路:应用内相机、系统拍照Picker、相册选图三选一

很多新手上来就写权限申请,恨不得一口气把相机、相册、存储三个权限全都要了。但鸿蒙的权限模型和安卓不一样,它对敏感权限的审核很严格,尤其相机和媒体库读取权限,如果应用场景说不清楚,上架审核也可能被卡。所以第一步应该是想清楚:你的业务真的需要这些权限吗?

我在这个实战案例里,把需求拆成了三个场景,对应三条完全不同的技术路径:

业务场景推荐路径是否需要相机权限是否需要相册读取权限
应用内拍照,需要自定义UI和连续拍照CameraKit自建相机需要不需要
直接拉起系统相机,拍完返回图片CameraPicker不需要不需要
从系统相册选图PhotoViewPicker不需要不需要

这里最关键的一个认知转变是:鸿蒙把“拍照”和“选图”本身做成了系统级的Picker能力,应用只要调起Picker,照片的拍摄、保存、授权全部由系统搞定,应用只能拿到一个URI(资源标识符),根本碰不到用户的完整相册。只有当你需要自定义相机界面、做实时滤镜、连续帧处理时,才必须要CameraKit。

我最初也陷入过一个误区:以为不申请权限就没法拍照。实测下来完全不是这样。CameraPicker拉起的是系统相机界面,拍摄过程在独立的系统进程里完成,拍完直接返回结果。对绝大多数“提交一张照片”的场景来说,这条路最省心,代码最少,隐私合规还干净。

不过也要明白它们的区别:CameraPicker返回的是一张图片的URI,你没机会做取景框叠加、人脸框、自定义快门动效这些操作;而CameraKit虽然开发量大,但取景流每一帧你都能拿到,适合做AR、扫码增强、检测框等功能。这次实战案例里,我两条路都做了,下面分开讲。

2. 应用内自建相机:权限只是开始,会话配置才是重头戏

如果你确定要自建相机,权限申请只是万里长征第一步。鸿蒙的CameraKit框架和安卓的Camera2类似,基于“会话(Session)”的抽象,整个流程可以归纳成四步:准备权限、初始化相机管理器、配置会话、拍照取帧。

2.1 权限申请:别忽略“还需要用户主动调用”

使用CameraKit必须声明ohos.permission.CAMERA权限。但有一个细节很容易被忽略:这个权限属于用户授权类(user_grant),不仅要在module.json5里声明,你还需要在代码里使用abilityAccessCtrl(权限访问控制模块)发起动态授权请求。另外,实际评测中发现,即使已经授权了相机权限,如果页面尚未获得焦点(比如权限弹窗刚弹出还没消失),此时立即初始化相机有一定概率失败,所以建议在权限弹窗回调成功且页面处于onWindowStageShow后再开始创建会话。

我整理了一份权限申请的工具代码,可以直接复制:

import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { common } from '@kit.AbilityKit'; let atManager = abilityAccessCtrl.createAtManager(); let permissionList: Array<Permissions> = ['ohos.permission.CAMERA']; let context = getContext(this) as common.UIAbilityContext; atManager.requestPermissionsFromUser(context, permissionList) .then((result) => { let grantStatus = result.authResults[0]; if (grantStatus === 0) { // 0 表示已授权,可以开始初始化相机 initCamera(); } else { // 用户拒绝,做降级处理,比如提示后跳转设置页 } }) .catch((err: BusinessError) => { console.error(`权限请求失败: ${JSON.stringify(err)}`); });

这段代码本身不难,但要注意:requestPermissionsFromUser是异步弹窗,用户处理过程可能很慢,甚至超时。所以建议在按钮点击事件里再触发权限请求,不要冷启动就弹窗,体验会好很多。

2.2 初始化CameraManager和获取相机设备

权限到位后,就可以通过@kit.CameraKit创建相机管理器了。需要说明的是,不同机型对相机的能力支持有差异,所以必须走“查询能力、再创建会话”的路线,而不是硬编码某个摄像头ID。

下面是我在实战案例里的初始化代码:

import { camera } from '@kit.CameraKit'; import { BusinessError } from '@kit.BasicServicesKit'; let cameraManager: camera.CameraManager = camera.getCameraManager(context); // 获取设备列表(后置、前置等) let cameraArray: Array<camera.CameraDevice> = cameraManager.getSupportedCameras(); if (cameraArray.length <= 0) { console.error('当前设备没有可用相机'); return; } let targetCamera: camera.CameraDevice = cameraArray[0]; // 如果存在后置摄像头则优先后置 for (let cam of cameraArray) { if (cam.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK) { targetCamera = cam; break; } } // 查询该相机支持的输出能力 let outputCapability: camera.CameraOutputCapability = cameraManager.getSupportedOutputCapability(targetCamera); let previewProfile: camera.Profile = outputCapability.previewProfiles[0]; let photoProfile: camera.Profile = outputCapability.photoProfiles[0];

getSupportedOutputCapability返回的是该设备支持的预览流和拍照流能力列表,里面包含分辨率、帧率、像素格式等。我建议不要直接选最大分辨率,因为高分辨率预览流对GPU、带宽的占用非常高,如果应用同时还在跑人脸检测,很容易掉帧。一般的做法是选一个预览流分辨率适配屏幕比例、拍照流分辨率满足业务输出需求的组合。

2.3 创建PreviewOutput和PhotoOutput

有了Profile还是不能直接用,需要通过createPreviewOutput和createPhotoOutput来创建真正可用的输出对象。同时你必须创建一个CameraSession,把它俩加进去,提交配置后这个会话才真正开始工作。

这里有一个非常容易踩的坑:预览表面必须在创建会话前准备好。HarmonyOS的预览输出支持两种方式:一种是Surface,用于极低延迟的相机预览(配合XComponent使用);另一种是基于ImageReceiver,每一帧都会回调到应用侧做分析。如果一开始就选错了模式,后面接预览画面时又要重建整个会话,非常浪费。

我的做法是,通过createPhotoOutput(photoProfile)创建拍照输出,通过cameraManager.createPreviewOutput(previewProfile, surfaceId)创建预览输出。其中surfaceId来自原生组件XComponent,这是一个用于承载Native图形surface的容器。

// 假设已经通过XComponent拿到了 surfaceId let previewOutput: camera.PreviewOutput = await cameraManager.createPreviewOutput(previewProfile, surfaceId); let photoOutput: camera.PhotoOutput = await cameraManager.createPhotoOutput(photoProfile); let session: camera.CameraSession = cameraManager.createSession(camera.CameraSessionType.NORMAL); session.addOutput(previewOutput); session.addOutput(photoOutput); await session.commitConfig(); await session.start();

提交会话成功后,预览流会立刻输出到XComponent绑定的surface上,用户就能看到取景画面了。这里我还想多提一句:commitConfig()之后的任何output变更都是不允许的,只有先stop()会话、修改配置、再次commitConfig()才行。这个约束和Android的Camera2很像,但在鸿蒙上表现得尤其严格,稍不注意就抛异常。

2.4 拍照回调与照片旋转角处理

拍照本身很简单,调用photoOutput.capture()即可。难的是你对照片后续要做什么。这次案例的业务是上传头像,所以我关注的是三点:回调够不够快、照片方向是否正确、JPEG质量是否可控。

let photoSettings = { quality: 95, rotation: camera.ImageRotation.ROTATION_0, location: { latitude: 0, longitude: 0 }, mirror: false }; await photoOutput.capture(photoSettings);

注意上面的rotation,这是个很容易出问题的点。不同手机的后置摄像头传感器安装方向不同,JPEG文件如果没有写入正确的EXIF旋转角,拿到其他设备上就是歪的。我建议拍照完成后,一律通过photoOutput.on('photoAssetReady')拿到拍照结果对象,再从结果里读取rotation字段,之后在处理图片时按这个值纠正方向。

photoOutput.on('photoAssetReady', (err: BusinessError, photoAsset: camera.PhotoAsset) => { let imageRotation: number = photoAsset.rotation; // 后续用image组件处理时传入rotation,或者解码时做旋转 });

这里还藏着一个坑:photoAssetReady回调拿到的临时照片默认保存在应用的缓存目录,不是相册。如果你希望拍完直接进系统相册,需要调用保存接口把图片写入媒体库,具体我会在后面的保存章节专门讲。

3. 轻量方案实测:用CameraPicker免权限拉起系统相机

自建相机虽然灵活,但开发成本高、摄像头权限也比较敏感。所以实战里我推荐第二个方案:CameraPicker,它最大的优点就是应用不需要申请相机权限,系统会在独立的Picker进程里完成拍照授权、拍摄、临时存储全流程,拍完把成果返回给你。

3.1 为什么推荐优先用CameraPicker

从用户角度来说,系统相机的拍照体验是最稳定的,变焦、夜景、HDR等能力手机厂商已经优化得很好,你用CameraKit自建相机,这些算法能力基本都享受不到。从开发者角度,CameraPicker代码量非常少,只需要设置拍摄结果保存参数,然后等着回调就行。

官方把它封装在@kit.CameraKit里,使用方式是这样的:

import { camera } from '@kit.CameraKit'; interface PickResult { resultCode: number; resultUri: string; } let pickerProfile: camera.CameraPickerProfile = { cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK, saveUri: undefined // 不指定,系统会保存到媒体库 }; let pickerResult: PickResult = await camera.picker.pick(context, [pickerProfile]);

picker.pick返回的resultUri指向一张保存在媒体库里的图片资源,你可以直接用这个URI交给后续的上传组件。整条链路里系统会自己弹权限弹窗、自己调起相机界面,应用全程无感。

3.2 用CameraPicker时的注意点

这套方案省事,但也不是没有约束,我自己实测时总结了三个注意事项:

第一,saveUri参数如果不传,系统默认把照片保存到公共相册。如果你的业务是“随手拍一张,不上传原图”,建议在saveUri中传入应用沙箱的URI,避免污染用户相册。具体通过photoAccessHelper创建临时文件再传进去。

第二,camera.picker.pick只能一次拍一张。如果业务需要一次拍多张,目前CameraPicker没有批量模式,只能循环调用。但连续调起系统相机的体验并不好,这种场景我还是建议走自建相机。

第三,回调的resultCode要判断。用户可能取消拍照,或者中途拍照失败返回错误码,不能默认拿到的就是成功照片。我在代码里专门做了失败分支,给出提示并允许用户返回重新选择。这一点看似简单,但很多初学者只跑通成功路径,上线后取消操作直接把空URI传给了后端,服务器自然解析失败。

从权限合规的角度,我特别推荐那些“头像拍摄、证件照拍摄、随手打卡拍照”这类App把默认路径切成CameraPicker,这样应用商店审核时,权限声明里没有相机权限,审核说服成本一下子就低了。

4. 相册选图:PhotoViewPicker免权限读取,权限方案做兜底

相册选图是另一个高频场景。HarmonyOS的媒体库访问策略比Android更严格,ohos.permission.READ_IMAGEVIDEO权限属于用户授权类,申请后的审核说明要写得非常具体。但好消息是,和CameraPicker对应的,系统也提供了一套免权限的PhotoViewPicker。

4.1 PhotoViewPicker的基本用法

PhotoViewPicker的使用就三步:构造选图参数、拉起Picker、处理返回的URI列表。

import { photoAccessHelper } from '@kit.MediaLibraryKit'; import { BusinessError } from '@kit.BasicServicesKit'; let options = new photoAccessHelper.PhotoSelectOptions(); options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; options.maxSelectNumber = 1; // 按需指定是否可预览,默认均能预览 let photoPicker = new photoAccessHelper.PhotoViewPicker(); photoPicker.select(options) .then((selectResult: photoAccessHelper.PhotoSelectResult) => { let uriList: Array<string> = selectResult.photoUris; if (uriList.length > 0) { // 使用第一个URI加载图片 loadImage(uriList[0]); } }) .catch((err: BusinessError) => { console.error(`PhotoViewPicker调用失败: ${JSON.stringify(err)}`); });

这段代码跑通很容易,但这里有几个容易忽略的细节,我逐个说一下:

  • PhotoSelectOptions支持MIMEType过滤,只选图片、只选视频或图文混合都可以。
  • maxSelectNumber不传默认是1,但如果设置为大于1,Picker界面会进入多选模式。
  • 返回的photoUris是媒体库的URI,它并不是直接的文件路径。后续读取时需要使用photoAccessHelper或fs的文件描述符接口打开,这一点和Android老版本的content://逻辑是类似的。

4.2 免权限Picker和申请权限读相册到底什么区别

我知道很多人会疑惑:既然能免权限拿照片,为什么还要学READ_IMAGEVIDEO权限的读法?

区别在于应用场景。PhotoViewPicker适合“用户主动选一张图上传”这类交互;但如果你的应用要做“图片缓存管理、AIGC图片批量导入、相册自动备份、需要遍历相册内容做分类统计”这类功能,就必须要申请媒体库读取权限,因为Picker是一次性交互,拿完就走,你拿不到持续的相册访问能力。

我把两套逻辑做了一个对比:

能力PhotoViewPickerREAD_IMAGEVIDEO权限
权限申请无需申请需要动态申请并填写权限理由
访问范围用户本次选择的内容整个媒体库
与相册交互只能选,不能遍历可以查询、遍历、订阅变更
典型场景上传头像、发布图片相册管理、批量备份

在项目实践中,我采用了一个混合策略:默认使用Picker;在“批量导入相册图片”模块里,如果用户主动开启了“允许访问所有照片”开关,再走权限通道,这样大部分用户全程不用看到权限弹窗,体验和隐私权衡都能兼顾。

4.3 读取照片内容:从URI到图像数据

拿到URI只是起点。开发时最容易出错的地方是直接用文件路径拼接,结果发现不存在的路径。正确做法是用媒体库或文件接口通过URI去打开文件描述符。

import { fileIo as fs } from '@kit.CoreFileKit'; import { photoAccessHelper } from '@kit.MediaLibraryKit'; let file: fs.File = fs.openSync(uri, fs.OpenMode.READ_ONLY); let stat = fs.statSync(file.fd); let buffer = new ArrayBuffer(stat.size); fs.readSync(file.fd, buffer); fs.closeSync(file.fd);

这里我想强调fs.openSync的第二个参数。打开URI对应的资源时,必须用READ_ONLY,因为媒体库文件很多在只读区域,用READ_WRITE很可能会抛出Error: Operation not permitted。另外,如果后面要做图片压缩,ArrayBuffer太大时建议用流式读取分块消费,避免内存峰值过高。

5. 图片保存、压缩与展示:让照片真正“落地”

拍摄和选图只是前半段,后半段是拿到图片后如何保存、压缩和展示。这次实战案例里,我发现很多同事把照片原图直接端到端传到服务器,流量开销大、上传慢,而且服务器侧往往只需要头像、缩略图这种小尺寸资源,所以本地处理必不可少。

5.1 保存图片到系统相册的三种姿势

如果你拍的照片来自CameraPicker,前面讲过系统默认会存到公共相册;但如果来自CameraKit自建相机,照片默认在应用的沙盒缓存,不会出现在相册里。把临时图片保存到相册,有三种做法:

第一种:直接复制到媒体库,这是常规做法。HarmonyOS的媒体库接口里提供了创建Asset的方式,可以把应用沙盒文件写入相册。

import { photoAccessHelper } from '@kit.MediaLibraryKit'; async function saveImageToGallery(fileUri: string) { let helper = photoAccessHelper.getPhotoAccessHelper(context); let createOptions: photoAccessHelper.CreateOptions = { title: `IMG_${Date.now()}`, fileNameExtension: 'jpg', MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE }; let photoAsset = await helper.createAsset(createOptions); // 打开该Asset的写通道,将图片数据写入 let fd = await photoAsset.open('rw'); // 写入图片数据后关闭fd await photoAsset.close(fd); }

第二种:如果图片本身就在系统相册(比如CameraPicker返回的URI就是相册里的),其实什么都不用做,它已经在相册里了。这一点要特别说明,因为在调试时很多人会以为自己还得“复制一份”,结果平白多出一张重复的图片。

第三种:临时展示而不落盘。如果只是想用image组件看看,直接把URI传给<Image>组件即可,它内部能处理media库URI。

5.2 使用ImagePacker做本地压缩

照片直接上传通常会有2~5MB的体积,这对移动网络非常不友好。鸿蒙提供了@kit.ImageKit的ImagePacker,可以按质量或按尺寸压缩图片。

我做了一个通用的压缩函数:

import { image } from '@kit.ImageKit'; async function compressImage(uri: string, maxWidth: number, quality: number): Promise<string> { // 1. 按URI解码图片源 let source = image.createImageSource(uri); // 2. 获取原图尺寸,按比例计算目标尺寸 let imageInfo = await source.getImageInfo(); let scale = Math.min(1, maxWidth / imageInfo.size.width); let targetWidth = Math.floor(imageInfo.size.width * scale); let targetHeight = Math.floor(imageInfo.size.height * scale); // 3. 创建PixelMap(解码到指定尺寸) let pixelMap = await source.createPixelMap({ desiredPixelFormat: image.PixelMapFormat.RGBA_8888, desiredSize: { width: targetWidth, height: targetHeight }, editable: false }); // 4. 使用ImagePacker压缩成JPEG let packer = image.createImagePacker(); let packedData = await packer.packing(pixelMap, { format: 'image/jpeg', quality: quality }); // 5. 清理资源,避免内存泄漏 pixelMap.release(); source.release(); return packedData; }

这个函数里,quality我建议值取在70~85之间,肉眼几乎看不出质量差异,体积却能压缩50%以上。另外,desiredPixelFormat选RGBA_8888是为了兼容大多数图片解码器;如果你处理的是不含透明通道的JPEG,也可以选RGB_565,内存占用会更低。

5.3 压缩完的字节数组怎么展示和上传

packing返回的是ArrayBuffer,你可以直接把它转成Base64或者写入临时文件,然后传给上传模块。项目里我是这样处理的:

// 临时文件路径,后续直接上传该文件 let tempFile = `${getContext(this).cacheDir}/upload_${Date.now()}.jpg`; let file = fs.openSync(tempFile, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); fs.writeSync(file.fd, packedData); fs.closeSync(file.fd);

这里有一个建议:压缩后的图片不要直接放在内存里做网络请求,先落盘。因为ArrayBuffer在异步上传过程中可能被GC回收,一旦网络层跨线程读取,会出现ArrayBuffer is detached的崩溃。落到磁盘后用文件流上传,内存占用更稳定,也方便日志记录。

6. 排查链路实录:三个最折磨人的问题

开发这套功能的过程中,我遇到并解决的问题远不止基础流程,有几个损耗了大量时间的问题值得单独说一说。如果你恰好也遇到了,可以参考我的排查顺序。

6.1 照片拍下来了,相册却看不到

这个问题特别典型:用CameraKit拍照,预览界面正常,拍照回调也执行了,但在系统相册里找不到照片。

我最初的排查链路是先确认拍照回调里的photoAsset是否有效,结果有效;然后再怀疑是不是保存目录不对,结果也没问题;最后发现原因在于:相机会话输出的照片默认写在应用缓存目录的临时隐藏路径里,系统相册根本不会扫描这块区域。要想进相册,必须手动通过媒体库接口把文件复制或写入到公共目录。

想明白这一点后,我再也不纠结“为什么相册不显示”了。正确流程就是五步:回调拿到asset → 读取原始数据 → 调用媒体库createAsset→ 写入数据 → 关闭资源。另外,如果拍摄前设置了saveUri为沙箱URI,也要走一遍同样的落地逻辑。

6.2 相册选择返回的URI图片加载失败

PhotoViewPicker返回值用Image组件直接渲染是OK的,但如果用fs.openSync按路径方式打开,则会抛“No such file or directory”。这也是典型的“路径直接拼接”的坑。

正确逻辑是:媒体库URI(形如file://media/Photo/xxxx)必须先通过媒体库的getAssetByUri拿到媒体资源对象,再从对象上获取或打开文件描述符,不能用路径方式硬拼。

let photoAccessHelperInstance = photoAccessHelper.getPhotoAccessHelper(context); let asset = await photoAccessHelperInstance.getAssetByUri(uri); let fd = await asset.open('r');

打开后读取数据再关闭fd。这个细节我在代码评审时看到很多人写错了,因为看起来fs.openSync也能打开一些以file://开头的URI,但遇到媒体库URI时一定会失败。所有涉及媒体资源的操作,都要严格遵守“URI → Asset → fd”的路径。

6.3 预览画面方向不对、取景框拉伸

自建相机预览时,手机竖直拿但预览画面横过来,或者画面被拉伸,这类问题的根因是预览流的尺寸没有适配屏幕方向。

HarmonyOS的相机预览输出遵循传感器画面的原始方向,所以当设备方向变化时,你需要监听on('deviceOrientationChange')事件(或组件旋转事件),动态调整XComponent的宽高比。我用的基准方法是:拿出当前预览Profile的分辨率,结合屏幕宽高比算出一个适配矩形的缩放,再通过XComponent容器的aspectRatio属性约束。

// 计算预览流宽高比,和屏幕宽高比比较后决定容器比例 let previewWidth = previewProfile.size.width; let previewHeight = previewProfile.size.height; let previewAspect = previewHeight / previewWidth; // 竖屏使用 // 将XComponent父容器设置为这个比例即可

另外还有一个小技巧:如果发现预览图像上下左右颠倒(不同机型传感器朝向差异导致),需要在XComponent上应用对应的坐标变换或设置镜像。不同厂商的设备会有细微差别,建议在真机自测列表里增加“前后摄像头×横竖屏”的组合用例。

7. 从Demo到产品化:几个容易被忽略的收尾细节

如果你顺利跑通了上文所有代码,恭喜,Demo已经完成。但从能跑到好用,中间还有一段路。我在这次实战案例里做了不少产品化收尾工作,挑几个性价比最高的分享一下。

第一,权限被拒后的降级体验。如果用户第一次拒绝相机权限,再次点击拍摄按钮时最好能引导去设置页开启,而不是反复弹系统权限框。我的做法是检测到拒绝后,先弹一个自定义对话框解释用途,并提供“去设置”按钮跳转。

第二,拍照等待态的UI反馈。相机拍照快门到回调之间往往有几百毫秒延迟,如果不加等待动画,用户会觉得没拍上,反复点击快门,结果拍出好几张。正确做法是拍照后立即禁用快门按钮,直到photoAssetReady回调或失败回调恢复。

第三,图片尺寸上限与上传超时。CameraKit拍出的照片如果在某些机型上特别大,服务器接收容易超时,建议压缩后大小控制在500KB以内,这样弱网环境下也相对可靠。我在压缩函数里默认做了“最长边不超过1280px、质量75%”的兜底。

第四,多设备适配。鸿蒙的特点是“一次开发,多端部署”,手机、平板、折叠屏的屏幕比例差异巨大。相机预览界面的XComponent容器比例如果写死,在折叠屏上会变形。建议用好组件的百分比布局和比例约束,永远不要固定宽高的像素值。

第五,内存释放。ImageSource、PixelMap、ImagePacker这些都是重量级对象,用完不释放,连续拍照几十次后App会明显卡顿甚至闪退。我习惯在每次处理的finally块里显式release,不要指望GC兜底,因为这几类Native资源走的是独立内存池。


这次的实战案例整体走下来,我最大的感受是:HarmonyOS在媒体能力上的设计目标非常一致——把权限边界划清楚,把高频的拍照、选图交给系统Picker,把低频但重度的自定义相机开放给CameraKit,并且所有图片都通过URI和媒体库打通。开发者的核心任务不是从零去实现一套相机,而是理解这三者的边界,在合适的业务场景选合适的能力。

最后再分享一个我个人的操作习惯:每接一种Picker或权限方案,都单独写一个小的验证Demo,把成功、取消、失败、旋转、压缩这几个分支全部跑一遍再进业务代码。因为相机和相册功能非常依赖设备和系统版本,真机上的表现可能和模拟器完全不同。别问我是怎么知道的,都是调试到深夜换来的经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询