HarmonyOS 精准分享图片选择与素材封装(连载02)【共创稿事节】

上一节将目标窗口、文档修订号与触碰落点收束进一次可核验的投递会话。这一节处理另一半问题:手机上选中的现场照片究竟怎样进入素材清单,怎样避免一张图片因重复点击被添加两次,以及怎样将临时 URI 与接收端需要的业务信息分开。对于在平板图纸中插入现场照片的场景,目标校验通过仍不足以提交;接收端还需要能够访问且格式正确的素材。

现场图片选择、预览与分享对象封装

一、先选图片,再建立业务素材主键

HarmonyOS 的 Media Library Kit 提供系统图片选择器。应用可以通过 PhotoViewPicker 让用户自主选择图片,并从返回值取得 photoUris。选择器返回的是当前访问链路中的 URI,不应把该 URI 当成后端数据库里的永久素材主键:内容读取权限、文件当前位置和接收端可访问范围都需要按各自的分享方式处理。

本文的手机页面保持三个区域:顶部是“继续选取”入口,中间用缩略图展示待发图片,底部显示投递数量与下一步操作。选择完成后先生成业务 assetId,再关联 URI。用户重复选择同一 URI 时执行去重,取消选择则移除对应行,不误删其他图片的临时记录。

// entry/src/main/ets/common/AssetModel.ets
export type AssetStatus = 'selected' | 'prepared' | 'sent' | 'failed';

export interface SelectedAsset {
  assetId: string;
  sourceUri: string;
  displayName: string;
  order: number;
  status: AssetStatus;
}

export interface AssetBatch {
  batchId: string;
  createdAt: number;
  targetWindowId: string;
  targetDocumentId: string;
  assets: SelectedAsset[];
}

export function addUniqueAssets(
  current: SelectedAsset[], incomingUris: string[], now: number
): SelectedAsset[] {
  const result: SelectedAsset[] = current.slice();
  incomingUris.forEach((uri: string, index: number) => {
    if (uri.length === 0 || result.some((item: SelectedAsset) =>
      item.sourceUri === uri)) return;
    result.push({
      assetId: `asset-${now}-${index}`,
      sourceUri: uri,
      displayName: `现场照片 ${result.length + 1}`,
      order: result.length,
      status: 'selected'
    });
  });
  return result;
}

这里用时间戳加索引生成前端临时标识,作用范围是当前批次。在跨设备存储、并发重试或离线恢复的业务中,服务端应分配真正具备全局唯一性的素材标识。sourceUri 只服务于当前选取与读取环节,业务状态字段只表示应用对素材准备进度的认识。

二、接入 PhotoViewPicker 并管理状态

选择器调用放在用户点击事件内,避免页面自动弹出系统选取界面。选择最多六张图片,可以让列表布局、上传耗时和错误提示都更容易控制。若用户直接返回而没有选择,保持原有清单;发生选取异常时保留旧内容,并给出明确的操作反馈。

// entry/src/main/ets/pages/AssetPickerPage.ets
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { addUniqueAssets, SelectedAsset } from '../common/AssetModel';

@Entry
@Component
struct AssetPickerPage {
  @State images: SelectedAsset[] = [];
  @State hint: string = '请选择需要投递的现场照片';
  @State selecting: boolean = false;

  private async pickImages(): Promise<void> {
    if (this.selecting) return;
    this.selecting = true;
    try {
      const picker = new photoAccessHelper.PhotoViewPicker();
      const options = new photoAccessHelper.PhotoSelectOptions();
      options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
      options.maxSelectNumber = 6;
      const result = await picker.select(options);
      const uris: string[] = result.photoUris ?? [];
      this.images = addUniqueAssets(this.images, uris, Date.now());
      this.hint = this.images.length > 0 ?
        `已选${this.images.length}张,可继续核对目标` : '尚未选择图片';
    } catch (error) {
      this.hint = `选取图片遇到问题:${JSON.stringify(error)}`;
    } finally {
      this.selecting = false;
    }
  }

  build() {
    Column({ space: 12 }) {
      Text('现场素材').fontSize(26).fontWeight(FontWeight.Bold)
      Text(this.hint).fontColor('#64748B').fontSize(14)
      Button(this.selecting ? '正在选择' : '从相册选择')
        .enabled(!this.selecting)
        .onClick(() => { this.pickImages(); })
      List({ space: 10 }) {
        ForEach(this.images, (item: SelectedAsset) => {
          ListItem() {
            Row({ space: 12 }) {
              Image(item.sourceUri)
                .width(72).height(72).objectFit(ImageFit.Cover)
                .borderRadius(12)
              Column({ space: 6 }) {
                Text(item.displayName).fontSize(16)
                Text(item.status).fontSize(12).fontColor('#64748B')
              }.alignItems(HorizontalAlign.Start)
            }.width('100%')
          }
        }, (item: SelectedAsset) => item.assetId)
      }.layoutWeight(1)
      Button(`核对投递目标 · ${this.images.length}张`)
        .enabled(this.images.length > 0)
    }.padding(20).width('100%').height('100%')
  }
}

Image(uri) 用于页面预览;它只负责渲染,不能据此判定原始图片尺寸、文件大小或接收端访问权限。实际工程中,清单数量发生变化时还应更新总大小估算和上传策略。如果需要长期保留资源,应根据文件来源及使用期限将素材复制到应用控制的目录,并按媒体访问规范处理。

DevEco Studio 中的图片选择器调用与预览列表

三、为投递建立完整的批次描述

接收端需要知道“投递哪些素材”和“向哪里插入”,但这两组数据的生命周期不同。图像 URI 可随选取会话变化,目标窗口可随文档切换变化。最清晰的做法是在用户确认后创建不可随意改写的批次快照:包含批次标识、目标窗口与文档、素材顺序和各素材业务主键。

下面的函数复制字段而不改写原始列表。通过 status 控制素材准备结果,下一阶段再把接收端返回的条目结果合入清单。复制结构可以减少异步处理中“选中第二张图后,第一张图也被改写”的问题。

// entry/src/main/ets/common/BatchBuilder.ets
import { AssetBatch, SelectedAsset } from './AssetModel';

export function createBatch(
  windowId: string, documentId: string,
  selected: SelectedAsset[], timestamp: number
): AssetBatch {
  const items: SelectedAsset[] = selected
    .filter((item: SelectedAsset) => item.sourceUri.length > 0)
    .map((item: SelectedAsset, index: number) => ({
      assetId: item.assetId,
      sourceUri: item.sourceUri,
      displayName: item.displayName,
      order: index,
      status: 'selected'
    } as SelectedAsset));
  return {
    batchId: `batch-${timestamp}`,
    createdAt: timestamp,
    targetWindowId: windowId,
    targetDocumentId: documentId,
    assets: items
  };
}

export function canPrepare(batch: AssetBatch): boolean {
  if (!batch.targetWindowId || !batch.targetDocumentId) return false;
  if (batch.assets.length < 1 || batch.assets.length > 12) return false;
  return batch.assets.every((asset: SelectedAsset) =>
    asset.sourceUri.length > 0 && asset.assetId.length > 0);
}

真实落点应与上一节的 TargetEnvelope 关联,而非作为永久文本说明写在素材上。假如用户先选六张图、又关闭目标文档,图片列表仍可继续使用,只需重新取得窗口信息并生成新的目标会话。相反,如果用户在确认页移除一张图,就应该重新创建批次快照,防止接收端的素材数量与界面上的数量不一致。

四、普通分享与精准插入的两条路径

系统 Share Kit 已支持文本、图片等跨应用分享场景。其文件分享方式应按 Share Kit 对 URI、数据类型及权限的要求构造,避免将本机私有路径直接作为其他应用可读取的地址。本文选取到的 photoUris 首先进入应用素材清单,之后再由传输适配层转成对应分享载荷。

对于普通分享,用户通过系统分享面板选取目标应用;对于目标窗口明确且需插入画布坐标的精准分享,接收端还应校验 windowId、documentId、触点位置和会话修订号,才能在目标业务服务中写入图片。不能将“选择器返回 URI”“系统分享面板打开”“素材已插入指定画布”看作同一个状态。

协议层建议至少区分以下回执:ACCEPTED 表示目标受理,ASSET_READY 表示读取完成,INSERTED 表示写入目标文档,REJECTED 表示拒绝接收。显示在用户界面上的成功标记只依据 INSERTED,避免传输发起后误导用户。

手机端待投递素材清单与图片预览

五、处理 URI 过期、取消与重复选择

图片选择器的返回结果需要尽快消费,尤其是在应用切到后台、跨应用跳转、内存重建之后。业务层应区分“用户取消选择”和“文件读取失败”,二者在体验上完全不同。前者保持原列表,后者允许针对受影响的条目重新选择或重新准备。若图片已经从相册删除,清单中的旧 URI 也应失效,并要求用户确认新素材。

情况判断点对应动作
选取结果为空photoUris.length保留原有素材清单
重复选中同一图片sourceUri 已存在保持一条记录,不改变顺序
列表项无法预览当前 URI 的读取结果标记准备失败并允许重选
目标窗口切换新旧文档和修订号保留图片,重新校验目标
接收端部分成功每项 assetId 回执按条目更新状态,避免整批误判
用户离开选择页面页面生命周期与待处理任务取消过期操作,释放暂存资源

调试重点可以安排成三组。第一组多次选择相同照片,确认列表不出现重复项;第二组先选照片再改变目标,确认旧落点不能继续使用;第三组人为造成某张素材不可读取,确认其他素材仍可显示、失败项具备明确状态。日志以批次与素材业务标识串联,不记录图片内容本身。需要按文件内容去重时,应在取得合法可读数据后再计算摘要,不能直接用 URI 字符串代替内容哈希。

六、准备下一步的落点坐标映射

到这里,系列工程已经拥有两个独立且可组合的能力:一套目标会话状态机,一套图片选择与业务素材批次。下一阶段将解决更细的定位问题:平板窗口有标题栏、工具区和可缩放画布,触碰发生的位置属于哪套坐标系,才能决定照片落到图纸上的哪一个点。只有把缩放比例、平移偏移和修订号统一到坐标转换中,才能完成真正可控的定向插入。

参考文档:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐