HarmonyOS 应用实战 55:结果页分享别打断动画,把动作放到结果稳定之后

结果页刚打开时,答案文本、来源题库和操作区并不是同时稳定:AnswerPage 先拿到路由参数,再异步读取收藏状态和题库名,同时分四段播放入场动画。此时如果把“分享”做成一进页就执行,用户看到的可能是半透明答案、空题库名,或者被系统选择器盖住的动画。

《答案之书》当前只实现了复制与题库文本分享:答案页的“复制”调用 Clipboard.copyText(this.param.text);题库列表读取完整 Deck 后用 composeShareText 拼成 名称#答案1#答案2,再复制到剪贴板。结果页尚无分享入口。本文以这些事实为基础,设计一个不打断结果呈现的分享时机,而不把设计示例说成已实现功能。

在这里插入图片描述

结果稳定不是“动画结束后等一秒”

AnswerPage.startEntrance() 的四段动画有明确时序:引号 380ms,正文在 120ms 延迟后 500ms,题库来源延迟 380ms,操作行延迟 480ms 后持续 420ms。最后一个动作的视觉结束大约在 900ms,而 deckName 又来自一次异步 DeckService.get

条件 来源 分享能否依赖
param.text 非空 DrawingPageAnswerParams 可以构造答案主体
param.answerId 非空 抽取服务返回的 Answer 可作内部关联,不必公开
deckName 已加载 DeckService.get(deckId) 可选,缺失时不能阻塞分享
操作行已显示 入场动画状态 适合开放用户点击

所以“稳定”是业务数据可用加上用户可感知的 UI 已就绪,而不是随手写一个 setTimeout(1000)。固定延迟会在低端设备、动画时长调整或题库读取变慢时失效。

在这里插入图片描述
在这里插入图片描述

现有抽取链路已经提供了正确的结果快照

DrawingPageonReady 时并行启动翻书动画和 AnswerPickService.pickOne(deckId);抽取成功后等待最短过渡时间,再用 replacePathdeckIdanswerIdtext、可选 question 传给 AnswerPage。这份 AnswerParams 是本次结果的快照。

const params: AnswerParams = {
  deckId: this.param.deckId,
  answerId: this.picked.id,
  text: this.picked.text,
  question: this.param.question
};
this.pathStack.replacePath({ name: RouteName.Answer, param: params });

分享应从这份参数构造文案,而不是在点击分享时再随机抽一次,也不该从当前选择器重新读取题库来猜答案。否则用户分享的可能不是屏幕上看到的那条结果,重现问题时也无法定位。

把“文案”和“系统分享”分成两个边界

当前项目已经有 Clipboard 工具;它只负责把纯文本写入剪贴板,并在失败时记录错误后抛出。建议新增的结果分享也遵循同样边界:纯函数负责生成文案,页面只在用户点击后调用具体通道。下面是设计示例。

interface AnswerShareSnapshot {
  answer: string;
  deckName?: string;
  question?: string;
}

function composeAnswerShareText(snapshot: AnswerShareSnapshot): string {
  const lines: string[] = ['《启示散页》的一页答案', `${snapshot.answer}`];
  if (snapshot.question?.trim()) {
    lines.push(`对应问题:${snapshot.question.trim()}`);
  }
  if (snapshot.deckName?.trim()) {
    lines.push(`来源题库:${snapshot.deckName.trim()}`);
  }
  return lines.join('\n');
}

问题是否放进分享文案必须由用户决定。问题常常比答案敏感;一个安全的默认值是只分享答案与应用名,提供一个明确开关“包含问题”,而不是默认把 param.question 发到外部应用。

用可观察的 ready 状态控制按钮,不用猜时间

当前答案页已经有 actionsOpacityactionsTranslateY。若加入分享按钮,可额外维护一个业务状态 shareReady:正文参数必须有效,操作行需显示,题库名不是必需条件。它不需要持久化,离开页面自然销毁。

@State private shareReady: boolean = false;

private markShareReady(): void {
  if (this.param.text.trim().length > 0) {
    this.shareReady = true;
  }
}

private async onShare(): Promise<void> {
  if (!this.shareReady) {
    return;
  }
  const text = composeAnswerShareText({
    answer: this.param.text,
    deckName: this.deckName
  });
  await Clipboard.copyText(text);
  promptAction.showToast({ message: '分享文案已复制' });
}

markShareReady 应由最后一段操作区动画完成的回调或明确的页面状态切换触发;不要通过 setTimeout 与动画数字复制绑定。第一版可以先提供“复制分享文案”,它复用已有 Clipboard 边界;接入系统分享面板时,再把 onShare 的最后一层替换为平台能力,文案生成逻辑保持不变。

为什么不在 onReady 自动拉起分享面板

系统分享面板是一次外部交互,会抢占焦点。自动拉起有三个问题:用户还没读到答案;动画被遮住,回到页面时视觉状态难解释;如果用户是从“再来一次”快速切换,旧页面的异步回调还可能影响新页面。

正确顺序应是:

DrawingPage 得到 Answer
  → replacePath(AnswerParams)
  → AnswerPage 渲染与入场动画
  → 操作区可见,shareReady = true
  → 用户点击分享
  → 构造当前参数快照 → Clipboard / 系统分享

这里没有 Repository 或 AppStorage,因为分享当前结果不是持久化业务事实。不要为了显示“分享成功”给 AppStorage 增加键;Toast 或页面内短暂反馈足够。若以后要统计分享次数,应只记录聚合事件码和结果类型,避免保存答案与用户问题正文。

type ShareTrace = {
  channel: 'clipboard' | 'system_panel';
  answerLength: number;
  includeQuestion: boolean;
  result: 'success' | 'failure';
};

function logShareTrace(trace: ShareTrace): void {
  hilog.info(DOMAIN, TAG, 'share channel=%{public}s len=%{public}d q=%{public}d ok=%{public}s',
    trace.channel, trace.answerLength, trace.includeQuestion ? 1 : 0, trace.result);
}

这类诊断只记录通道、长度和结果,既能区分剪贴板或系统面板失败,也不会把答案内容带进日志;它应在分享完成或捕获异常后调用,而不是在构造快照时记录正文。

与“复制答案”“分享题库”不要混为一个协议

功能 当前/建议载荷 入口 隐私与长度
复制答案 单条 param.text 答案页已有 最小载荷
分享结果 答案 + 可选题库名/问题 答案页建议能力 问题默认不带出
分享题库 name#answer… 题库列表已有 可能很长,需用户明确触发

复用 composeShareText 去分享单条结果会得到无法理解的导入格式;反过来拿结果文案做题库导出会破坏 parseDeckImport 的约定。协议的使用者不同,格式就必须各自稳定。

验证重点是“分享的就是眼前这一条”

实现后可按以下路径复查:

  1. 抽取一条答案,动画未完成时确认分享按钮不可点或不可见。
  2. 动画完成后点击分享,复制文本应与页面答案一致,且不出现另一轮随机结果。
  3. deckName 仍未加载或题库已删除时分享:答案仍可分享,来源行可省略。
  4. 有问题、无问题两种路径分别验证默认文案;开启“包含问题”时再确认正文准确。
  5. 快速点“再来一次”并返回结果页,确认旧页面不会再次弹 Toast 或改变新结果状态。
  6. 让剪贴板写入失败,确认保留当前页面、记录非敏感错误,并提示“复制失败”。

常见问题与定位

现象 优先检查 修复
分享内容不是屏幕上的答案 是否点击时重新调用了 pickOne 只使用当前 AnswerParams 快照
动画被分享面板打断 是否在 onReady 自动触发 改为操作区稳定后由用户点击
分享文案漏题库名 是否把题库名当成必填 deckName 可选,不能阻塞答案分享
用户问题被意外带出 是否默认拼接 question 默认只含答案,改为显式选择
从结果页分享出导入格式 是否误用 composeShareText 为结果单独维护 composeAnswerShareText

排障日志沿用现有做法:记录通道成功/失败、字符串长度或是否携带可选字段,不记录答案和问题正文。这样能知道链路在哪里失败,又不会把用户内容写入 hilog

小结

结果页分享的关键不是多一个图标,而是尊重 DrawingPage → AnswerPage 产生的结果快照和答案页的入场节奏。用数据就绪和操作区可见定义可分享状态,让用户主动触发,再把文案构造与平台通道分开,才能避免动画被抢焦点、答案串位和隐私泄露。

当前项目已具备答案复制和题库文本导出,尚未实现结果分享入口。本文的 shareReady 与结果文案函数是可落地的扩展设计;真机分享面板、返回行为与不同设备体验仍需在实际工程中验证。

Logo

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

更多推荐