记忆洗牌是时光胶囊里最有仪式感的功能——用户点击🎲按钮,系统随机抽取一个历史胶囊展示。用户不知道下一张会显示哪个胶囊,这种"随机重逢"的设计让回顾历史变成了拆盲盒式的体验。这个页面的代码量不大,但随机算法、卡片翻转、空状态处理都有值得拆解的细节。

完整效果
在这里插入图片描述

在这里插入图片描述

一、记忆洗牌的入口

两个入口位置

入口 位置 触发方式
标题栏 🎲 时间线 Tab 右上角 点击跳转
设置页 设置 Tab 列表项 点击跳转
// 入口 1:标题栏
Text('🎲').fontSize(20).width(40).height(40)
  .onClick(() => this.goToShuffle())

// 入口 2:设置页
Row() {
  Text('🎲 记忆洗牌').fontSize(FontSize.md)
  Blank()
  Text('›').fontSize(FontSize.xxl).fontColor(this.getColors().textSecondary)
}
.onClick(() => this.goToShuffle())

两个入口指向同一个页面——但使用场景不同。 标题栏入口适合"我想随机看看"的冲动式操作,设置页入口适合"我找找这个功能"的探索式操作。两种用户行为都有对应的入口。

路由跳转

private goToShuffle(): void {
  router.pushUrl({ url: 'pages/MemoryShuffle' })
}

在这里插入图片描述

没有传参数——MemoryShuffle 自己从数据库获取所有胶囊。 和详情页不同(传 capsuleId),洗牌页需要的是"全部胶囊"的数据,不是某一个。所以不需要路由参数,页面自己查询。

二、随机算法的设计

Fisher-Yates 洗牌算法

// 推断的洗牌算法
private shuffleArray(array: Capsule[]): Capsule[] {
  const result = array.slice()  // 复制原数组
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1))
    const temp = result[i]
    result[i] = result[j]
    result[j] = temp
  }
  return result
}

Fisher-Yates 是标准的均匀洗牌算法——保证每种排列出现的概率相等。 slice() 复制原数组——不修改数据库里的顺序。

为什么不直接用 Math.random() 排序

// 错误做法:按 random 排序
array.sort(() => Math.random() - 0.5)

sort(() => Math.random() - 0.5) 不是均匀洗牌——它有 bias。 某些排列出现的概率比其他排列高。Fisher-Yates 保证均匀性——每个胶囊出现在每个位置的概率都是 1/n。

洗牌队列的管理

private queue: Capsule[] = []     // 洗牌队列
private currentIndex: number = 0   // 当前展示位置

private initQueue(): void {
  this.queue = this.shuffleArray(this.database.getAllCapsules())
  this.currentIndex = 0
}

洗牌一次,顺序全部确定——不是每次都随机取一个。 用户点击"下一张"时,按队列顺序展示下一个。这保证了:1)同一次洗牌不会重复展示同一个胶囊;2)所有胶囊都会被展示一遍。

三、卡片展示的三种状态

从 CapsuleState 推断

// 导入
import { getCapsuleState, CapsuleState } from '../model/Capsule'

洗牌展示的卡片需要区分胶囊状态:

状态 展示方式 交互
SEALED 🔒 + 倒计时 显示"这个胶囊还在封印中"
READY 显示心情 + 引导开启 可以点击开启
OPENED 显示内容 + 心情 + 时间 完整展示

封印中胶囊的特殊处理

// 推断的封印中展示
Column() {
  Text('🔒').fontSize(64)
  Text('这个胶囊还在封印中')
    .fontSize(FontSize.md).fontColor(this.getColors().textSecondary)
  Text('距离开启还有 ' + this.countdownText(capsule.unlockAt))
    .fontSize(FontSize.sm).fontColor(this.getColors().primary)
}

洗到封印中的胶囊不是"失败"——而是"提醒"。 它告诉用户"你还有一个胶囊在等待",增加期待感。这比只展示已开启的胶囊更有意义——随机性包括了"还没到时间"的情况。

四、卡片的视觉设计

大卡片模式

// 推断的洗牌卡片结构
Column() {
  // 心情 emoji
  Text(capsule.mood).fontSize(72).margin({ bottom: Spacing.lg })

  // 内容
  Text(capsule.content)
    .fontSize(FontSize.lg).fontColor(this.getColors().text)
    .lineHeight(28).textAlign(TextAlign.Center)
    .margin({ bottom: Spacing.lg })

  // 标签
  Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
    ForEach(capsule.tags, (tag: string) => {
      Text('#' + tag).fontSize(FontSize.sm).fontColor(this.getColors().primary)
        .margin({ right: Spacing.xs, bottom: Spacing.xs })
    })
  }

  // 时间信息
  Text(this.formatDate(capsule.createdAt))
    .fontSize(FontSize.xs).fontColor(this.getColors().textSecondary)
    .margin({ top: Spacing.md })
}
.width('100%').padding(Spacing.xl)
.backgroundColor(this.getColors().surface)
.borderRadius(BorderRadius.lg)

在这里插入图片描述

和 CapsuleCard 的对比

特性 CapsuleCard(列表) 洗牌卡片
尺寸 小卡片,紧凑 大卡片,宽松
内容 截取前50字 完整内容
心情 小 emoji(20px) 大 emoji(72px)
信息密度 高(多字段) 低(聚焦内容)
用途 快速浏览 沉浸阅读

洗牌卡片的核心是"沉浸感"——大字号、大 emoji、大间距。 用户不需要快速扫描信息,而是坐下来慢慢读一篇"来自过去的信"。

五、翻页交互

下一张按钮

// 推断的翻页 UI
Row() {
  Text('🎲 再来一张')
    .fontSize(FontSize.md).fontColor('#FFFFFF')
    .fontWeight(FontWeight.Bold)
}
.width('100%').height(48)
.justifyContent(FlexAlign.Center)
.backgroundColor(this.getColors().primary)
.borderRadius(BorderRadius.lg)
.onClick(() => this.nextCard())

nextCard 的逻辑

private nextCard(): void {
  this.currentIndex++
  if (this.currentIndex >= this.queue.length) {
    // 队列用完,重新洗牌
    this.initQueue()
  }
}

队列用完后自动重新洗牌——用户可以无限刷。 不会出现"没有更多了"的死胡同。重新洗牌时顺序重新随机——用户不会看到完全相同的顺序。

进度指示

// 推断的进度显示
Text(`${this.currentIndex + 1} / ${this.queue.length}`)
  .fontSize(FontSize.xs).fontColor(this.getColors().textSecondary)

显示"3 / 12"格式——告诉用户当前是第几张,总共多少张。 进度感让用户知道"还剩多少没看",控制浏览节奏。

六、空状态处理

没有胶囊时的洗牌页

if (this.queue.length === 0) {
  Column() {
    Text('🔮').fontSize(64).margin({ bottom: Spacing.md })
    Text('还没有胶囊可洗')
      .fontSize(FontSize.lg).fontColor(this.getColors().text)
      .fontWeight(FontWeight.Bold).margin({ bottom: Spacing.sm })
    Text('先去封印几个胶囊吧')
      .fontSize(FontSize.sm).fontColor(this.getColors().textSecondary)
  }
  .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
}

和首页空状态同样的设计模式——图标+标题+引导文字。 但没有"开始封印"按钮——洗牌页是独立页面,跳转回首页再切到封印 Tab 比直接改 currentTab 更合理(因为 currentTab 是首页的状态,洗牌页无法直接修改)。

只有一个胶囊时

只有一个胶囊也能洗——洗牌结果就是它自己。 不需要特殊处理——Fisher-Yates 对单元素数组不做任何交换,直接返回原数组。

七、记忆洗牌的心理设计

随机性的价值

模式 用户心理 适用场景
时间线(有序) “我想找某个特定胶囊” 有目的的回顾
洗牌(随机) “我想看看命运给我什么” 无目的的偶遇

随机性创造了"命运感"——用户觉得是"命运让我看到这个胶囊",而不是"我选择了这个胶囊"。 这种心理让回顾历史变得更有仪式感——就像翻开一副随机抽到的塔罗牌。

为什么洗牌比列表更有温度

列表是"我管理数据"的心态——用户像管理员一样浏览记录。洗牌是"我和记忆对话"的心态——用户像读者一样接收来自过去的信。产品设计的目标是后者——时光胶囊不是一个数据库管理工具,它是一个情感载体。

八、页面的嵌套结构

Column                              页面容器
  ├→ Row (标题栏)                   返回按钮 + 标题
  ├→ Column (内容区)
  │    ├→ 洗牌卡片                  心情 + 内容 + 标签
  │    ├→ 进度指示                  "3 / 12"
  │    └→ 下一张按钮                "🎲 再来一张"
  └→ 底部信息                       创建时间

嵌套深度 3 层——最浅的页面之一。 因为洗牌页只有一个核心功能——展示一张卡片。不需要复杂布局,Column 纵向排列就够了。

九、与其他功能的数据关系

洗牌页的数据来源

Database.getAllCapsules()
  → shuffleArray() 洗牌
  → queue[] 洗牌队列
  → queue[currentIndex] 当前展示

洗牌页不修改数据库——它是只读的。 用户在洗牌页看到胶囊后,如果想开启,需要跳转到详情页操作。洗牌页只负责"展示",不负责"操作"。

从洗牌到详情的跳转

// 推断:点击洗牌卡片跳转详情
.onClick(() => {
  setCurrentCapsuleId(capsule.id)
  router.pushUrl({
    url: 'pages/CapsuleDetail',
    params: { capsuleId: capsule.id }
  })
})

洗到一个胶囊后,用户可以点进去看详情。 跳转逻辑和首页完全一致——setCurrentCapsuleId + router.pushUrl。复用同一套路由模式。

十、洗牌功能的扩展可能

当前版本

  • 随机展示所有胶囊
  • 封印中胶囊特殊展示
  • 队列用完自动重新洗牌
  • 点击跳转详情页

未来可能的扩展

扩展方向 实现方式 价值
按心情筛选 queue = capsules.filter(c => c.mood === ‘😊’) “我想看开心的回忆”
按时间筛选 queue = capsules.filter(c => c.createdAt > oneWeekAgo) “我想看最近的回忆”
分享卡片 Canvas 截图 + 分享 API 把回忆分享给朋友
每日推送 每天随机展示一个胶囊 被动回忆,增加打开率

当前版本足够简单——但架构为扩展留了空间。 只需要改 initQueue() 的筛选逻辑,就能实现按心情/时间筛选。不需要改页面 UI——卡片展示逻辑不变。

Logo

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

更多推荐