HarmonyOS 读取系统相册图片并预览 
概述 
本文将详细介绍如何在 HarmonyOS 应用中实现读取用户相册图片并进行预览的功能。通过使用 HarmonyOS 提供的媒体库 API 和权限管理机制,我们可以安全、高效地访问用户的图片资源。
效果图 



核心 API 介绍 
1. PhotoAccessHelper API 
PhotoAccessHelper 是 HarmonyOS 提供的媒体库访问助手,用于管理和访问用户的图片、视频等媒体资源。
typescript
import { photoAccessHelper } from '@kit.MediaLibraryKit';
// 获取 PhotoAccessHelper 实例
this.phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context);主要功能:
- 获取媒体资源列表
- 支持条件查询和排序
- 获取媒体文件的缩略图
- 访问媒体文件的元数据
2. 权限管理 API 
访问用户相册需要申请 ohos.permission.READ_IMAGEVIDEO 权限。
typescript
import { abilityAccessCtrl, PermissionRequestResult } from '@kit.AbilityKit';
// 检查和请求权限
const atManager = abilityAccessCtrl.createAtManager();
const permission = 'ohos.permission.READ_IMAGEVIDEO';3. 数据查询 API 
使用 DataSharePredicates 进行数据查询和排序。
typescript
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 
获取文件详细信息,如文件大小。
typescript
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 状态管理。
typescript
@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 类 
图片浏览页面的状态管理模型。
typescript
@ObservedV2
class GalleryModel {
  @Trace images: ImageItem[] = [];          // 所有图片列表
  @Trace filteredImages: ImageItem[] = [];  // 过滤后的图片列表
  @Trace isLoading: boolean = false;       // 加载状态
  @Trace hasPermission: boolean = false;   // 权限状态
  @Trace isSearchMode: boolean = false;    // 搜索模式
}2. UI 组件 
主页面组件 
使用 V2 组件架构,支持响应式状态管理。
typescript
@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。
typescript
@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中声明权限
json
requestPermissions: [
    {
        name: "ohos.permission.READ_IMAGEVIDEO",
        reason: "$string:EntryAbility_label",
        usedScene: { abilities: [] },
    },
],步骤 2:权限申请 
在访问用户相册之前,必须先申请相应的权限。
typescript
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 
获取权限后,初始化媒体库访问助手。
typescript
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:查询图片资源 
配置查询选项,获取用户相册中的图片。
typescript
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:处理图片数据 
将获取的图片资源转换为应用内的数据模型。
typescript
// 转换为 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 界面。
typescript
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)
      }
    }
  }
}完整代码 
typescript
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个上架的鸿蒙作品
