HarmonyOS APP实战-基于Image Kit的图像处理APP - 第2篇:图片选择与预览
1. 开篇
在上一篇中,我们完成了环境搭建与项目创建,配置了DevEco Studio开发环境,创建了名为ImageProcessor的HarmonyOS工程,并在module.json5中预留了文件读写所需的权限声明。项目骨架已经搭好,源代码中包含了基础的EntryAbility入口和空白的首页页面。
从本篇开始,我们将进入真正的功能开发。本篇要实现的是整个APP的入口操作——让用户从系统相册中选择图片,并将选中的图片以缩略图网格的形式呈现在屏幕上。这不仅是视觉交互的第一步,更是后续所有图片处理操作的数据来源基础。我们将封装三个核心模块:PhotoPickerService(选择图片服务)、ImageSourceUtils(图片解码工具)和ImageGridPage(图片网格页面),它们将协同完成从选择到展示的完整闭环。
2. 核心实现
2.1 基础配置:导入模块与权限声明
在开始编码前,我们需要确认项目已经添加了必要的依赖。在module.json5的requestPermissions中新增相册访问权限(PhotoViewPicker在API 11+无需单独权限,但为了兼容旧设备和数据访问,建议声明读取媒体文件权限)。同时,在代码中导入所需的系统模块。
src/main/module.json5权限配置片段:
{"module":{"name":"entry","type":"entry","requestPermissions":[{"name":"ohos.permission.READ_MEDIA"}]}}关键点说明:
- 尽管
PhotoViewPicker自带用户授权弹窗,但声明ohos.permission.READ_MEDIA可以确保后续需要直接读取文件时权限就绪。 ohos.permission.READ_MEDIA属于user_grant类型,需要动态弹窗申请。本APP仅在选择图片时触发,我们会在选择前动态请求。
导入依赖模块(在需要使用的地方添加):
import{photoAccessHelper}from'@kit.MediaLibraryKit';// 仅作参考,实际使用PhotoViewPickerimport{photoViewPicker}from'@kit.MediaLibraryKit';// PhotoViewPicker 所在模块import{image}from'@kit.ImageKit';// Image Kit 核心模块import{fileIo}from'@kit.CoreFileKit';// 文件操作import{common}from'@kit.AbilityKit';实际使用的API路径(基于官方文档):
PhotoViewPicker来自@ohos.multimedia.picker(API10+)或@kit.MediaLibraryKit(API11+)。image.createImageSource来自@ohos.multimedia.image或@kit.ImageKit。fileIo.openSync来自@ohos.file.fs。
2.2 核心逻辑:PhotoPickerService 与 ImageSourceUtils
我们首先封装一个服务类,负责调用系统选择器并返回选中文件的URI列表;然后编写一个工具类,将URI解码为像素图(PixelMap)缩略图。
src/main/ets/services/PhotoPickerService.ets:
import{photoViewPicker}from'@kit.MediaLibraryKit';import{common}from'@kit.AbilityKit';import{BusinessError}from'@kit.BasicServicesKit';/** * 图片选择服务,封装 PhotoViewPicker 调用 */exportclassPhotoPickerService{privatepicker:photoViewPicker.PhotoViewPicker;constructor(){this.picker=newphotoViewPicker.PhotoViewPicker();}/** * 打开系统相册选择图片,返回选中文件的URI数组 * @param context UIAbility上下文,用于申请权限 * @returns 返回选中的图片URI列表,如果用户取消则返回空数组 */asyncpickPhotos(context:common.UIAbilityContext):Promise<string[]>{try{// 构建选择选项:限制最多选择50张,只显示图片类型constoptions:photoViewPicker.PhotoSelectOptions={MIMEType:photoViewPicker.PhotoViewMIMETypes.IMAGE_TYPE,// 只选择图片maxSelectNumber:50,// 最大50张isOriginal:false// 不选择原始图,提升速度};// 调用选择器,返回结果constresult:photoViewPicker.PhotoSelectResult=awaitthis.picker.select(options,context);// 取出选中的URI列表consturis:string[]=result.photoUris;console.info('PhotoPickerService: selected uris count = '+uris.length);returnuris;}catch(error){consterr:BusinessError=errorasBusinessError;console.error('PhotoPickerService: pick failed, code = '+err.code+', msg = '+err.message);return[];}}}关键点说明:
photoViewPicker.PhotoViewPicker是系统提供的图片选择器API,无需额外权限,系统会弹出用户授权界面。PhotoSelectOptions中的MIMEType设置为PhotoViewMIMETypes.IMAGE_TYPE只显示图片文件。isOriginal: false表示返回的是压缩后的缩略图URI,选择速度快,适合预览场景。如需原始大图,可设为true,但后续解码时需注意内存。
src/main/ets/utils/ImageSourceUtils.ets:
import{image}from'@kit.ImageKit';import{fileIo}from'@kit.CoreFileKit';/** * 图片解码工具类,提供URI转像素图(缩略图)功能 */exportclassImageSourceUtils{/** * 根据文件URI创建解码后的缩略图 PixelMap * @param fileUri 文件URI,例如从 PhotoPickerService 得到 * @param width 期望的缩略图宽度(px) * @param height 期望的缩略图高度(px) * @returns 解码后的 PixelMap,失败返回 undefined */staticasyncdecodeThumbnail(fileUri:string,width:number=256,height:number=256):Promise<image.PixelMap|undefined>{letfile:fileIo.File|undefined;try{// 打开文件获取文件描述符file=fileIo.openSync(fileUri,fileIo.OpenMode.READ_ONLY);constfd:number=file.fd;// 创建图片源对象constimageSource:image.ImageSource=image.createImageSource(fd);// 设置解码参数:输出缩略图尺寸constdecodingOptions:image.DecodingOptions={desiredSize:{width:width,height:height},// 指定期望的宽高editable:true// 允许后续编辑(如裁剪)};// 同步解码得到 PixelMapconstpixelMap:image.PixelMap=imageSource.createPixelMapSync(decodingOptions);// 释放图片源资源imageSource.release();returnpixelMap;}catch(error){console.error('ImageSourceUtils decode failed: '+JSON.stringify(error));returnundefined;}finally{// 关闭文件描述符if(file){fileIo.closeSync(file);}}}}关键点说明:
- 使用
fileIo.openSync将URI转换为文件描述符,因为image.createImageSource接受文件描述符(fd)。fileIo.openSync的OpenMode.READ_ONLY表示只读打开。 image.createImageSource返回ImageSource对象,它是解码图片的入口。DecodingOptions中的desiredSize属性可以指定输出像素图的尺寸,这相当于“缩略图采样”,比解码再缩放更高效,且节省内存。createPixelMapSync是同步接口,不会阻塞主线程太久(小图)。也可以使用异步接口createPixelMap,但示例采用同步更简洁。
2.3 完整模块:ImageGridPage 页面
现在我们将选择服务与解码工具组合到页面中,实现点击按钮选择图片,并显示在Grid布局中。
src/main/ets/pages/ImageGridPage.ets:
import{PhotoPickerService}from'../services/PhotoPickerService';import{ImageSourceUtils}from'../utils/ImageSourceUtils';import{image}from'@kit.ImageKit';import{common}from'@kit.AbilityKit';@Componentexportstruct ImageGridPage{// 声明图片列表状态,每次选择后追加新图的PixelMap@StateimageList:image.PixelMap[]=[];// 页面标题privatetitle:string='图片预览';// 常量:网格列数privatereadonlyGRID_COLUMNS:number=3;build(){Column(){// 顶部操作栏Row(){Text(this.title).fontSize(24).fontWeight(FontWeight.Bold).padding({left:16});Blank();Button('选择图片').onClick(()=>this.pickAndAddImages()).margin({right:16})}.height(60).width('100%').backgroundColor('#F5F5F5')// 图片网格区域if(this.imageList.length===0){// 没有图片时显示提示Column(){Text('点击上方按钮从相册选择图片').fontSize(16).fontColor('#999999')}.layoutWeight(1).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).width('100%')}else{Grid(){ForEach(this.imageList,(item:image.PixelMap,index:number)=>{GridItem(){Image(item).width('100%').aspectRatio(1)// 正方形显示.objectFit(ImageFit.Cover).borderRadius(8)}.margin(4)},(item:image.PixelMap,index:number)=>index.toString())}.columnsTemplate('1fr '.repeat(this.GRID_COLUMNS)).rowsTemplate('1fr '.repeat(this.GRID_COLUMNS)).columnsGap(4).rowsGap(4).padding(4).layoutWeight(1).width('100%')}}.width('100%').height('100%')}/** * 选择图片并添加到列表 */privateasyncpickAndAddImages(){// 获取UIAbility上下文(在页面中通过getContext获取)constcontext:common.UIAbilityContext=getContext(this)ascommon.UIAbilityContext;constservice=newPhotoPickerService();consturis:string[]=awaitservice.pickPhotos(context);if(uris.length===0){// 用户取消或选择失败return;}// 遍历URI,解码缩略图并添加到列表for(consturiofuris){constthumb:image.PixelMap|undefined=awaitImageSourceUtils.decodeThumbnail(uri,256,256);if(thumb){// 追加到状态数组,触发UI刷新this.imageList=[...this.imageList,thumb];}}}}关键点说明:
- 网格使用
Grid容器,columnsTemplate设置为'1fr 1fr 1fr'(3列),用字符串重复生成。 ForEach遍历imageList,每个GridItem内使用Image(pixelMap)显示像素图。objectFit(ImageFit.Cover)确保图片填满正方形区域,可能会被裁剪。- 每次选择后,
pickAndAddImages方法异步解码所有选定图片,新生成的PixelMap追加到imageList中。为了简单,这里使用...展开运算符创建新数组触发刷新。如果图片数量较多,建议使用@Observed配合LazyForEach实现懒加载。 getContext(this)获取当前组件的上下文,传递给PhotoPickerService。
首页引用该页面:
修改EntryAbility.ets或Index.ets中的build()直接加载ImageGridPage组件,例如:
@Entry@Componentstruct Index{build(){ImageGridPage()}}3. 运行验证
完成上述代码后,运行项目(通过模拟器或真机)。预期表现如下:
- 应用启动后,显示一个空白页面,顶部标题“图片预览”,右侧有“选择图片”按钮。
- 点击“选择图片”按钮,系统弹出相册选择界面,用户可以单选或多选图片。
- 选择后返回APP,选中的图片以3列网格的形式显示为小方块(256x256像素缩略图)。
- 连续多次选择,新图片会被追加到网格末尾,之前的图片保留。
如果出现图片无法显示,请检查:
- 真机/模拟器是否已登录华为账号并授予相册权限?
- 测试图片是否损坏或格式不被支持(建议使用JPEG、PNG格式)?
- 控制台是否有文件打开或解码失败的日志?
4. 小结与预告
本篇我们完成了APP的第一个核心功能模块:通过PhotoViewPicker选择多张图片,使用image.createImageSource配合DecodingOptions高效解码出缩略图,并以Grid网格形式预览。我们封装的PhotoPickerService和ImageSourceUtils将成为后续所有编辑操作的基石——所有图片处理都将基于PixelMap进行。
下一篇,我们将深入图片解码环节,学习如何获取图片的原始信息(宽度、高度、方向、EXIF元数据等),并在此基础上实现图片的基本信息展示面板。届时,你的图片处理APP将拥有“看懂图片”的能力。