HarmonyOS 读取系统相册图片并预览
2025-08-06 14:19:00
108次阅读
0个评论
HarmonyOS 读取系统相册图片并预览
概述
本文将详细介绍如何在 HarmonyOS 应用中实现读取用户相册图片并进行预览的功能。通过使用 HarmonyOS 提供的媒体库 API 和权限管理机制,我们可以安全、高效地访问用户的图片资源。
效果图
核心 API 介绍
1. PhotoAccessHelper API
PhotoAccessHelper
是 HarmonyOS 提供的媒体库访问助手,用于管理和访问用户的图片、视频等媒体资源。
import { photoAccessHelper } from '@kit.MediaLibraryKit';
// 获取 PhotoAccessHelper 实例
this.phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context);
主要功能:
- 获取媒体资源列表
- 支持条件查询和排序
- 获取媒体文件的缩略图
- 访问媒体文件的元数据
2. 权限管理 API
访问用户相册需要申请 ohos.permission.READ_IMAGEVIDEO
权限。
import { abilityAccessCtrl, PermissionRequestResult } from '@kit.AbilityKit';
// 检查和请求权限
const atManager = abilityAccessCtrl.createAtManager();
const permission = 'ohos.permission.READ_IMAGEVIDEO';
3. 数据查询 API
使用 DataSharePredicates
进行数据查询和排序。
import { dataSharePredicates } from '@kit.ArkData';
// 创建查询条件
const fetchOptions: photoAccessHelper.FetchOptions = {
fetchColumns: [
photoAccessHelper.PhotoKeys.URI,
photoAccessHelper.PhotoKeys.DISPLAY_NAME,
photoAccessHelper.PhotoKeys.SIZE,
photoAccessHelper.PhotoKeys.DATE_ADDED
],
predicates: new dataSharePredicates.DataSharePredicates()
};
4. 文件系统 API
获取文件详细信息,如文件大小。
import { fileIo as fs } from '@kit.CoreFileKit';
// 获取文件统计信息
const file = fs.openSync(asset.uri, fs.OpenMode.READ_ONLY);
const stat = await fs.stat(file.fd);
const fileSize = stat.size;
核心组件介绍
1. 数据模型组件
ImageItem 类
图片项数据模型,使用 V2 状态管理。
@ObservedV2
class ImageItem {
@Trace id: string = ''; // 图片唯一标识
@Trace name: string = ''; // 图片名称
@Trace size: number = 0; // 文件大小
@Trace createTime: string = ''; // 创建时间
@Trace thumbnail: image.PixelMap | null = null; // 缩略图
@Trace photoAsset: photoAccessHelper.PhotoAsset | null = null; // 原始资源
}
GalleryModel 类
图片浏览页面的状态管理模型。
@ObservedV2
class GalleryModel {
@Trace images: ImageItem[] = []; // 所有图片列表
@Trace filteredImages: ImageItem[] = []; // 过滤后的图片列表
@Trace isLoading: boolean = false; // 加载状态
@Trace hasPermission: boolean = false; // 权限状态
@Trace isSearchMode: boolean = false; // 搜索模式
}
2. UI 组件
主页面组件
使用 V2 组件架构,支持响应式状态管理。
@Entry
@ComponentV2
export struct Page01 {
@Local localModel: GalleryModel = new GalleryModel();
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
private phAccessHelper: photoAccessHelper.PhotoAccessHelper | null = null;
}
图片网格项组件
自定义 Builder 函数,用于构建单个图片项的 UI。
@Builder
buildImageItem(image: ImageItem) {
Stack({ alignContent: Alignment.TopEnd }) {
Column() {
// 图片缩略图显示
if (image.thumbnail) {
Image(image.thumbnail)
.width('100%')
.height(120)
.objectFit(ImageFit.Cover)
} else {
// 默认占位图
Image($r('app.media.startIcon'))
.backgroundColor('#F0F0F0')
}
// 图片信息显示
Column() {
Text(image.name) // 图片名称
Row() {
Text(this.formatDateTime(image.createTime)) // 创建时间
Text(this.formatFileSize(image.size)) // 文件大小
}
}
}
}
}
主要实现步骤
步骤 1:声明权限
在modules.json5中声明权限
requestPermissions: [
{
name: "ohos.permission.READ_IMAGEVIDEO",
reason: "$string:EntryAbility_label",
usedScene: { abilities: [] },
},
],
步骤 2:权限申请
在访问用户相册之前,必须先申请相应的权限。
private async requestPermission(): Promise<boolean> {
try {
const atManager = abilityAccessCtrl.createAtManager();
const permission = 'ohos.permission.READ_IMAGEVIDEO';
// 检查权限状态
const grantStatus = await atManager.checkAccessToken(
this.context.applicationInfo.accessTokenId,
permission
);
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return true;
}
// 请求权限
const requestResult: PermissionRequestResult =
await atManager.requestPermissionsFromUser(this.context, [permission]);
return requestResult.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch (error) {
console.error('权限请求失败:', error);
return false;
}
}
步骤 3:初始化 PhotoAccessHelper
获取权限后,初始化媒体库访问助手。
private async initializeGallery(): Promise<void> {
try {
this.localModel.isLoading = true;
// 请求权限
const hasPermission = await this.requestPermission();
if (!hasPermission) {
this.localModel.hasPermission = false;
return;
}
this.localModel.hasPermission = true;
// 初始化 PhotoAccessHelper
this.phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context);
// 加载图片
await this.loadImagesFromAlbum();
} catch (error) {
console.error('初始化图库失败:', error);
} finally {
this.localModel.isLoading = false;
}
}
步骤 4:查询图片资源
配置查询选项,获取用户相册中的图片。
private async loadImagesFromAlbum(): Promise<void> {
if (!this.phAccessHelper) {
console.error('PhotoAccessHelper未初始化');
return;
}
try {
// 创建获取选项
const fetchOptions: photoAccessHelper.FetchOptions = {
fetchColumns: [
photoAccessHelper.PhotoKeys.URI,
photoAccessHelper.PhotoKeys.DISPLAY_NAME,
photoAccessHelper.PhotoKeys.SIZE,
photoAccessHelper.PhotoKeys.DATE_ADDED
],
predicates: new dataSharePredicates.DataSharePredicates()
};
// 按时间倒序排列
fetchOptions.predicates?.orderByDesc(photoAccessHelper.PhotoKeys.DATE_ADDED);
// 获取图片资源
const fetchResult = await this.phAccessHelper.getAssets(fetchOptions);
const photoAssets = await fetchResult.getAllObjects();
// 处理图片数据...
} catch (error) {
console.error('加载相册图片失败:', error);
}
}
步骤 5:处理图片数据
将获取的图片资源转换为应用内的数据模型。
// 转换为 ImageItem 数组
const imageItems: ImageItem[] = [];
for (let i = 0; i < Math.min(photoAssets.length, 50); i++) {
const asset = photoAssets[i];
try {
// 获取缩略图
const thumbnail = await asset.getThumbnail({ width: 300, height: 300 });
// 获取文件大小
let fileSize = 0;
try {
const file = fs.openSync(asset.uri, fs.OpenMode.READ_ONLY);
const stat = await fs.stat(file.fd);
fileSize = stat.size;
fs.closeSync(file.fd);
} catch (sizeError) {
console.error(`获取文件大小失败:`, sizeError);
fileSize = 0;
}
// 获取创建时间
const dateAdded = asset.get(photoAccessHelper.PhotoKeys.DATE_ADDED) as number;
const createTime = new Date(dateAdded * 1000).toISOString();
// 创建 ImageItem 实例
const imageItem = new ImageItem(
asset.uri,
asset.displayName || `图片_${i + 1}`,
fileSize,
createTime,
thumbnail,
asset
);
imageItems.push(imageItem);
} catch (error) {
console.error(`处理图片失败:`, error);
}
}
步骤 6:UI 状态管理
根据不同状态显示相应的 UI 界面。
build() {
Column() {
if (this.localModel.isLoading) {
// 加载状态
Column() {
LoadingProgress()
Text('正在加载图片...')
}
} else if (!this.localModel.hasPermission) {
// 权限提示
Column() {
Image($r('app.media.lightbulb'))
Text('需要访问相册权限')
Button('重新授权')
.onClick(() => {
this.initializeGallery();
})
}
} else if (this.localModel.images.length === 0) {
// 空状态
Column() {
Image($r('app.media.grid_horizontal'))
Text('暂无图片')
}
} else {
// 图片网格
Scroll() {
Grid() {
ForEach(this.localModel.images, (image: ImageItem) => {
GridItem() {
this.buildImageItem(image)
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(12)
.columnsGap(12)
}
}
}
}
完整代码
import { dataSharePredicates } from '@kit.ArkData';
import { image } from '@kit.ImageKit';
import { abilityAccessCtrl, common, PermissionRequestResult } from '@kit.AbilityKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo as fs } from '@kit.CoreFileKit';
// 图片项数据模型
@ObservedV2
class ImageItem {
@Trace id: string = '';
@Trace name: string = '';
@Trace size: number = 0;
@Trace createTime: string = '';
@Trace thumbnail: image.PixelMap | null = null;
@Trace photoAsset: photoAccessHelper.PhotoAsset | null = null;
constructor(id: string, name: string, size: number, createTime: string,
thumbnail: image.PixelMap | null = null, photoAsset: photoAccessHelper.PhotoAsset | null = null) {
this.id = id;
this.name = name;
this.size = size;
this.createTime = createTime;
this.thumbnail = thumbnail;
this.photoAsset = photoAsset;
}
}
// 图片浏览页面数据模型
@ObservedV2
class GalleryModel {
@Trace images: ImageItem[] = [];
@Trace filteredImages: ImageItem[] = [];
@Trace isLoading: boolean = false;
@Trace hasPermission: boolean = false;
@Trace isSearchMode: boolean = false;
}
@Entry
@ComponentV2
export struct Page01 {
@Local localModel: GalleryModel = new GalleryModel();
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
private phAccessHelper: photoAccessHelper.PhotoAccessHelper | null = null;
aboutToAppear() {
this.initializeGallery();
// 初始化过滤后的图片列表
this.localModel.filteredImages = this.localModel.images;
}
// 构建图片网格项
@Builder
buildImageItem(image: ImageItem) {
Stack({ alignContent: Alignment.TopEnd }) {
// 图片缩略图
Column() {
if (image.thumbnail) {
Image(image.thumbnail)
.width('100%')
.height(120)
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 8, topRight: 8 })
} else {
Image($r('app.media.startIcon'))
.width('100%')
.height(120)
.objectFit(ImageFit.Cover)
.borderRadius({ topLeft: 8, topRight: 8 })
.backgroundColor('#F0F0F0')
}
// 图片信息
Column() {
Text(image.name)
.fontSize(12)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.width('100%')
// 时间和文件大小在同一行
Row() {
Text(this.formatDateTime(image.createTime))
.fontSize(10)
.fontColor('#666666')
.layoutWeight(1)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(this.formatFileSize(image.size))
.fontSize(10)
.fontColor('#999999')
}
.width('100%')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.padding(8)
.width('100%')
}
.backgroundColor('#FFFFFF')
.borderRadius(8)
.shadow({
radius: 4,
color: '#1A000000',
offsetX: 0,
offsetY: 2
})
}
}
build() {
Column() {
if (this.localModel.isLoading) {
// 加载状态
Column() {
LoadingProgress()
.width(40)
.height(40)
.color('#2196F3')
Text('正在加载图片...')
.fontSize(14)
.fontColor('#666666')
.margin({ top: 16 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
} else if (!this.localModel.hasPermission) {
// 权限提示
Column() {
Image($r('app.media.lightbulb'))
.width(64)
.height(64)
.fillColor('#FFA726')
Text('需要访问相册权限')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ top: 16 })
Text('请在设置中开启相册访问权限,以便浏览和管理您的图片')
.fontSize(14)
.fontColor('#666666')
.textAlign(TextAlign.Center)
.margin({ top: 8, left: 32, right: 32 })
Button('重新授权')
.type(ButtonType.Capsule)
.backgroundColor('#2196F3')
.fontColor('#FFFFFF')
.fontSize(16)
.padding({
left: 24,
right: 24,
top: 12,
bottom: 12
})
.margin({ top: 24 })
.onClick(() => {
this.initializeGallery();
})
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
} else if ((this.localModel.isSearchMode ? this.localModel.filteredImages : this.localModel.images).length === 0) {
// 空状态
Column() {
Image($r(this.localModel.isSearchMode ? 'app.media.search' : 'app.media.grid_horizontal'))
.width(64)
.height(64)
.fillColor('#BDBDBD')
Text(this.localModel.isSearchMode ? '未找到相关图片' : '暂无图片')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ top: 16 })
Text(this.localModel.isSearchMode ? '尝试使用其他关键词搜索' : '您的相册中还没有图片')
.fontSize(14)
.fontColor('#666666')
.margin({ top: 8 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
} else {
// 图片网格
Scroll() {
Grid() {
ForEach(this.localModel.isSearchMode ? this.localModel.filteredImages : this.localModel.images,
(image: ImageItem) => {
GridItem() {
this.buildImageItem(image)
}
})
}
.columnsTemplate('1fr 1fr 1fr')
.rowsGap(12)
.columnsGap(12)
.padding(16)
}
.layoutWeight(1)
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Auto)
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
// 格式化时间显示
formatDateTime(dateTimeString: string): string {
try {
const date = new Date(dateTimeString);
const now = new Date();
const diffTime = now.getTime() - date.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
// 今天
return `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
} else if (diffDays === 1) {
// 昨天
return `昨天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
} else if (diffDays < 7) {
// 一周内
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return `${weekdays[date.getDay()]} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes()
.toString()
.padStart(2, '0')}`;
} else {
// 超过一周
return `${date.getFullYear()}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate()
.toString()
.padStart(2, '0')}`;
}
} catch (error) {
return dateTimeString; // 如果解析失败,返回原始字符串
}
}
// 初始化图库
private async initializeGallery(): Promise<void> {
try {
this.localModel.isLoading = true;
// 请求权限
const hasPermission = await this.requestPermission();
if (!hasPermission) {
console.error('权限被拒绝,无法访问相册');
this.localModel.hasPermission = false;
this.localModel.isLoading = false;
return;
}
this.localModel.hasPermission = true;
// 初始化PhotoAccessHelper
this.phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context);
// 加载图片
await this.loadImagesFromAlbum();
} catch (error) {
console.error('初始化图库失败:', error);
} finally {
this.localModel.isLoading = false;
}
}
// 请求相册读取权限
private async requestPermission(): Promise<boolean> {
try {
const atManager = abilityAccessCtrl.createAtManager();
const permission = 'ohos.permission.READ_IMAGEVIDEO';
// 检查权限状态
const grantStatus = await atManager.checkAccessToken(this.context.applicationInfo.accessTokenId, permission);
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return true;
}
// 请求权限
const requestResult: PermissionRequestResult =
await atManager.requestPermissionsFromUser(this.context, [permission]);
return requestResult.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch (error) {
console.error('权限请求失败:', error);
return false;
}
}
// 从相册加载图片数据
private async loadImagesFromAlbum(): Promise<void> {
if (!this.phAccessHelper) {
console.error('PhotoAccessHelper未初始化');
return;
}
try {
// 创建获取选项
const fetchOptions: photoAccessHelper.FetchOptions = {
fetchColumns: [
photoAccessHelper.PhotoKeys.URI,
photoAccessHelper.PhotoKeys.DISPLAY_NAME,
photoAccessHelper.PhotoKeys.SIZE,
photoAccessHelper.PhotoKeys.DATE_ADDED
],
predicates: new dataSharePredicates.DataSharePredicates()
};
// 按时间倒序排列
fetchOptions.predicates?.orderByDesc(photoAccessHelper.PhotoKeys.DATE_ADDED);
// 获取图片资源
const fetchResult = await this.phAccessHelper.getAssets(fetchOptions);
const photoAssets = await fetchResult.getAllObjects();
// 转换为ImageItem数组
const imageItems: ImageItem[] = [];
for (let i = 0; i < Math.min(photoAssets.length, 50); i++) {
const asset = photoAssets[i];
try {
// 获取缩略图
const thumbnail = await asset.getThumbnail({ width: 300, height: 300 });
// 获取文件大小
let fileSize = 0;
try {
const file = fs.openSync(asset.uri, fs.OpenMode.READ_ONLY);
const stat = await fs.stat(file.fd);
fileSize = stat.size;
fs.closeSync(file.fd);
} catch (sizeError) {
console.error(`获取文件大小失败 ${asset.displayName}:`, sizeError);
fileSize = 0;
}
// 获取图片的真实创建时间
const dateAdded = asset.get(photoAccessHelper.PhotoKeys.DATE_ADDED) as number;
const createTime = new Date(dateAdded * 1000).toISOString();
const imageItem = new ImageItem(
asset.uri,
asset.displayName || `图片_${i + 1}`,
fileSize,
createTime,
thumbnail,
asset
);
imageItems.push(imageItem);
} catch (thumbnailError) {
console.error(`获取缩略图失败 ${asset.displayName}:`, thumbnailError);
// 即使缩略图获取失败,也尝试获取文件大小
let fileSize = 0;
try {
const file = fs.openSync(asset.uri, fs.OpenMode.READ_ONLY);
const stat = await fs.stat(file.fd);
fileSize = stat.size;
fs.closeSync(file.fd);
} catch (sizeError) {
console.error(`获取文件大小失败 ${asset.displayName}:`, sizeError);
fileSize = 0;
}
// 获取图片的真实创建时间
const dateAdded = asset.get(photoAccessHelper.PhotoKeys.DATE_ADDED) as number;
const createTime = new Date(dateAdded * 1000).toISOString();
const imageItem = new ImageItem(
asset.uri,
asset.displayName || `图片_${i + 1}`,
fileSize,
createTime,
null,
asset
);
imageItems.push(imageItem);
}
}
this.localModel.images = imageItems;
this.localModel.filteredImages = imageItems;
console.info(`成功加载 ${imageItems.length} 张图片`);
// 关闭fetchResult
fetchResult.close();
} catch (error) {
console.error('加载相册图片失败:', error);
// 如果加载失败,显示空状态
this.localModel.images = [];
this.localModel.filteredImages = [];
}
}
// 格式化文件大小
private formatFileSize(bytes: number): string {
if (bytes === 0) {
return '0 B';
}
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
}
关键技术要点
1. 权限管理
- 必须在
module.json5
中声明权限 - 运行时动态申请权限
- 处理权限被拒绝的情况
2. 性能优化
- 限制加载图片数量(示例中限制为50张)
- 使用缩略图而非原图显示
- 异步加载,避免阻塞 UI 线程
3. 错误处理
- 权限申请失败处理
- 图片加载失败的降级方案
- 文件访问异常的容错机制
4. 状态管理
- 使用 V2 状态管理架构
- 响应式数据更新
- 组件状态与数据模型分离
总结
通过以上步骤,我们成功实现了 HarmonyOS 应用中读取相册图片并预览的功能。该实现方案具有以下特点:
- 安全性:严格的权限管理机制
- 性能:优化的图片加载和显示策略
- 用户体验:完善的加载状态和错误提示
- 可维护性:清晰的代码结构和状态管理
这个实现为开发者提供了一个完整的相册访问解决方案,可以作为类似功能开发的参考模板。
关于我们
如果你兴趣想要了解更多的鸿蒙应用开发细节和最新资讯,甚至你想要做出一款属于自己的应用!欢迎在评论区留言或者私信或者看我个人信息,可以加入技术交流群。
我们目前已经孵化了6个上架的鸿蒙作品
00
- 0回答
- 6粉丝
- 1关注
相关话题
- 如何调用系统拍照并获取图片
- 68.Harmonyos NEXT 图片预览组件应用实践(一):相册与社交场景
- 【HarmonyOS】模仿个人中心头像图片,调用系统相机拍照,从系统相册选择图片和圆形裁剪显示 (一)
- 【HarmonyOS】模仿个人中心头像图片,调用系统相机拍照,从系统相册选择图片和圆形裁剪显示 (二)
- 【拥抱鸿蒙】HarmonyOS NEXT实现双路预览并识别文字
- 鸿蒙保存图片到相册
- 【HarmonyOS NEXT】 鸿蒙图片或视频保存相册
- 57.Harmonyos NEXT 图片预览组件实现概览
- HarmonyOS实战:一招搞定保存图片到相册
- 66.Harmonyos NEXT 图片预览组件使用指南
- 60.Harmonyos NEXT 图片预览组件之边界处理与图片切换
- 鸿蒙Next如何实现打开相册选图片功能?
- 59.Harmonyos NEXT 图片预览组件之PicturePreviewImage实现原理
- 62.Harmonyos NEXT 图片预览组件之工具类实现
- 67.Harmonyos NEXT 图片预览组件之性能优化策略