前言

Pura X Max 的外屏并不小,但它仍然属于折叠态下的窄窗口场景。列表页如果把标题、摘要、时间、来源、标签、状态、按钮全都塞进一张卡片,外屏很快就会变得拥挤。用户真正想先看到的是这条记录是什么、当前处于什么状态、能不能马上处理,其他信息可以等到空间足够时再展示。

Pura X Max 的外屏为 5.4 英寸,内屏为 7.7 英寸,外屏分辨率为 1848 × 1264,内屏分辨率为 2584 × 1828,系统版本为 HarmonyOS 6.1。外屏和内屏的尺寸差异很明显,同一张卡片在两种形态下不应该展示完全相同的信息量。

我在处理这类页面时,会先把业务数据拆成两层:核心信息和扩展信息。折叠态只保留核心信息,展开态再展示扩展信息。这样做之后,外屏不会因为字段太多而显得挤,展开态也不会因为信息太少而显得空。

问题出在字段没有分层

很多卡片一开始会把所有字段都展示出来。比如一条整理记录,可能包含这些内容:

标题。

状态。

来源。

时间。

标签。

摘要。

优先级。

操作按钮。

这些字段都重要,但它们的重要性并不一样。

外屏空间有限时,标题和状态的优先级最高。标题让用户知道这条记录是什么,状态让用户知道是否需要处理,主操作按钮让用户可以继续下一步。摘要、来源、时间、标签这些信息虽然有价值,但它们不一定要在折叠态里全部出现。

如果卡片没有信息层级,折叠态会出现几个问题。

卡片高度变大,一屏能看到的记录变少。

摘要占据太多空间,标题反而不突出。

标签、来源、时间同时出现,视觉噪声变多。

主操作按钮被挤到卡片底部,点击路径变长。

折叠态页面更适合快速浏览。用户先扫一遍标题和状态,再决定是否进入处理。展开态有更大的显示空间,可以把摘要、来源、时间、标签展示出来,帮助用户减少点击和跳转。

把同一条数据拆成两层

信息密度控制的关键,是在 UI 层明确区分核心信息和扩展信息。

核心信息放在 compact 状态下:

title
status
primaryAction

扩展信息放在 expanded 状态下:

summary
source
time
tag
priority

这不是简单隐藏几个字段。字段分层之后,页面的阅读路径会更清楚。外屏下,用户只需要扫标题和状态;展开态下,用户可以在不进入详情的情况下看到更多上下文。

响应式布局本身强调根据屏幕尺寸和窗口变化调整页面结构,让不同设备和不同窗口尺寸下的阅读、交互体验保持稳定。Pura X Max 这种外屏和内屏差异明显的设备,尤其适合用断点控制字段显示。

我这里仍然使用窗口宽度做判断。页面宽度低于 720vp 时进入 compact,只显示核心信息;达到 720vp 后进入 expanded,显示更多字段。

private readonly expandedWidth: number = 720;

private isExpanded(): boolean {
  return this.pageWidth >= this.expandedWidth;
}

真实项目里可以继续细分,例如 compact、medium、expanded 三档。外屏只显示标题和状态,普通平板显示标题、状态和时间,Pura X Max 展开态显示摘要和标签。为了让问题聚焦,这里先保留两档。

用一个页面验证信息密度变化

下面的页面模拟了一组信息整理记录。窄窗口下,卡片只展示标题、状态和处理按钮;窗口变宽后,摘要、来源、时间、标签和优先级会一起出现。这样可以比较直观看到同一组数据在折叠态和展开态下的信息差异。

页面可以放到 entry/src/main/ets/pages/Index.ets 运行。Pura X Max 适配调试可以使用 DevEco Studio 6.1.0,并安装 Pura X Max 模拟器验证外屏和展开态效果。

interface InfoCardData {
  id: number;
  title: string;
  status: string;
  actionText: string;
  summary: string;
  source: string;
  time: string;
  tag: string;
  priority: string;
}

@Entry
@Component
struct Index {
  @State private pageWidth: number = 0;
  @State private selectedId: number = 1;

  private readonly expandedWidth: number = 720;

  private readonly cards: InfoCardData[] = [
    {
      id: 1,
      title: '社区物业缴费提醒',
      status: '待处理',
      actionText: '处理',
      summary: '识别到物业费缴纳截止日期、金额明细和办理地点,建议添加提醒。',
      source: '拍照整理',
      time: '09:20',
      tag: '通知',
      priority: '高优先级'
    },
    {
      id: 2,
      title: 'Pura X Max 适配会议纪要',
      status: '待确认',
      actionText: '确认',
      summary: '整理出外屏、展开态、横屏和悬停态几类页面问题,适合作为后续开发清单。',
      source: '语音转写',
      time: '10:45',
      tag: '会议',
      priority: '中优先级'
    },
    {
      id: 3,
      title: '活动报名确认单',
      status: '已保存',
      actionText: '查看',
      summary: '提取到报名人、联系电话、活动时间和签到地址,后续可以加入日程。',
      source: '相册导入',
      time: '11:30',
      tag: '表单',
      priority: '普通'
    },
    {
      id: 4,
      title: '客户需求变更记录',
      status: '待处理',
      actionText: '处理',
      summary: '本次变更涉及首页布局、权限配置、消息提醒和后台字段展示,需要同步给开发。',
      source: '文本整理',
      time: '13:10',
      tag: '项目',
      priority: '高优先级'
    },
    {
      id: 5,
      title: '课程作业提交说明',
      status: '已整理',
      actionText: '查看',
      summary: '识别到提交时间、文件格式、命名规范和邮箱地址,适合保存为待办。',
      source: '拍照整理',
      time: '15:25',
      tag: '学习',
      priority: '普通'
    },
    {
      id: 6,
      title: '门诊复查预约提示',
      status: '已保存',
      actionText: '查看',
      summary: '提取到复查时间、科室、楼层和注意事项,可以作为健康提醒保存。',
      source: '相册导入',
      time: '16:40',
      tag: '提醒',
      priority: '中优先级'
    }
  ];

  private isExpanded(): boolean {
    return this.pageWidth >= this.expandedWidth;
  }

  private getColumnsTemplate(): string {
    return this.isExpanded() ? '1fr 1fr' : '1fr';
  }

  private getPagePadding(): number {
    return this.isExpanded() ? 24 : 16;
  }

  private getHeaderTitle(): string {
    return this.isExpanded() ? '展开态显示完整上下文' : '折叠态保留核心信息';
  }

  private getHeaderDesc(): string {
    if (this.isExpanded()) {
      return '摘要、来源、时间、标签和优先级已经展开,适合在大屏上快速判断记录内容。';
    }

    return '当前只保留标题、状态和主操作,减少外屏卡片里的字段堆叠。';
  }

  private getStatusColor(status: string): string {
    if (status === '待处理') {
      return '#B25E00';
    }

    if (status === '待确认') {
      return '#7C3AED';
    }

    return '#276749';
  }

  private getStatusBgColor(status: string): string {
    if (status === '待处理') {
      return '#FFF4E5';
    }

    if (status === '待确认') {
      return '#F1EAFE';
    }

    return '#E7F5EE';
  }

  @Builder
  private MetaPill(text: string) {
    Text(text)
      .fontSize(12)
      .fontColor('#4B5563')
      .padding({ left: 8, right: 8, top: 4, bottom: 4 })
      .backgroundColor('#F3F4F6')
      .borderRadius(999)
  }

  @Builder
  private InfoCard(item: InfoCardData) {
    Column({ space: 12 }) {
      Row({ space: 10 }) {
        Column({ space: 8 }) {
          Text(item.title)
            .fontSize(this.isExpanded() ? 18 : 17)
            .fontWeight(FontWeight.Medium)
            .fontColor('#111827')
            .maxLines(this.isExpanded() ? 1 : 2)
            .textOverflow({ overflow: TextOverflow.Ellipsis })

          Row({ space: 8 }) {
            Text(item.status)
              .fontSize(12)
              .fontColor(this.getStatusColor(item.status))
              .padding({ left: 8, right: 8, top: 4, bottom: 4 })
              .backgroundColor(this.getStatusBgColor(item.status))
              .borderRadius(999)

            if (!this.isExpanded()) {
              Text('核心信息')
                .fontSize(12)
                .fontColor('#6B7280')
            }
          }
        }
        .layoutWeight(1)

        Button(item.actionText)
          .fontSize(13)
          .fontColor('#FFFFFF')
          .height(32)
          .padding({ left: 12, right: 12 })
          .backgroundColor('#2F8F83')
          .borderRadius(16)
          .onClick(() => {
            this.selectedId = item.id;
          })
      }
      .width('100%')
      .alignItems(VerticalAlign.Top)

      if (this.isExpanded()) {
        Text(item.summary)
          .fontSize(14)
          .fontColor('#4B5563')
          .lineHeight(20)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Row({ space: 8 }) {
          this.MetaPill(item.source)
          this.MetaPill(item.time)
          this.MetaPill(item.tag)
          this.MetaPill(item.priority)
        }
        .width('100%')
      }
    }
    .width('100%')
    .padding(this.isExpanded() ? 18 : 16)
    .backgroundColor(this.selectedId === item.id ? '#EEF7F5' : '#FFFFFF')
    .borderRadius(20)
    .border({
      width: this.selectedId === item.id ? 1.5 : 1,
      color: this.selectedId === item.id ? '#2F8F83' : '#E5E7EB'
    })
    .shadow({
      radius: this.selectedId === item.id ? 12 : 8,
      color: '#12000000',
      offsetX: 0,
      offsetY: 4
    })
  }

  build() {
    Column({ space: 16 }) {
      Column({ space: 8 }) {
        Row() {
          Column({ space: 4 }) {
            Text('信息密度控制')
              .fontSize(this.isExpanded() ? 28 : 23)
              .fontWeight(FontWeight.Bold)
              .fontColor('#111827')

            Text(this.getHeaderTitle())
              .fontSize(15)
              .fontColor('#2F8F83')
          }
          .layoutWeight(1)

          Text(Math.round(this.pageWidth).toString() + 'vp')
            .fontSize(13)
            .fontColor('#374151')
            .padding({ left: 10, right: 10, top: 6, bottom: 6 })
            .backgroundColor('#FFFFFF')
            .borderRadius(999)
        }
        .width('100%')

        Text(this.getHeaderDesc())
          .fontSize(14)
          .fontColor('#6B7280')
          .lineHeight(21)
      }
      .width('100%')
      .padding({
        left: this.getPagePadding(),
        right: this.getPagePadding(),
        top: 18
      })

      Scroll() {
        Grid() {
          ForEach(this.cards, (item: InfoCardData) => {
            GridItem() {
              this.InfoCard(item)
            }
          }, (item: InfoCardData) => item.id.toString())
        }
        .columnsTemplate(this.getColumnsTemplate())
        .columnsGap(12)
        .rowsGap(12)
        .width('100%')
        .padding({
          left: this.getPagePadding(),
          right: this.getPagePadding(),
          bottom: 24
        })
      }
      .layoutWeight(1)
      .width('100%')
      .edgeEffect(EdgeEffect.Spring)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F6F7F9')
    .onAreaChange((_: Area, newValue: Area) => {
      const width = Number(newValue.width);
      if (!Number.isNaN(width) && width > 0) {
        this.pageWidth = width;
      }
    })
  }
}

关键实现点和适配边界

这里的实现重点不在卡片样式,而在字段显示策略。

pageWidth 记录页面当前可用宽度,isExpanded() 判断是否进入展开态。页面根容器绑定 onAreaChange 后,窗口尺寸变化会更新 pageWidth。组件区域变化事件会在组件显示尺寸或位置变化时触发,适合用来处理这类页面级布局响应。

@State private pageWidth: number = 0;

private isExpanded(): boolean {
  return this.pageWidth >= this.expandedWidth;
}

卡片里的核心信息一直显示。

Text(item.title)
Text(item.status)
Button(item.actionText)

扩展信息只在 expanded 下显示。

if (this.isExpanded()) {
  Text(item.summary)

  Row({ space: 8 }) {
    this.MetaPill(item.source)
    this.MetaPill(item.time)
    this.MetaPill(item.tag)
    this.MetaPill(item.priority)
  }
}

这个判断看起来很简单,但它解决的是页面信息层级问题。外屏下,用户看到的是标题、状态、操作;展开态下,用户看到的是完整上下文。两种形态使用同一组数据,但 UI 呈现的信息量不同。

真实项目里可以把字段进一步分层。

核心信息适合包含标题、状态、主操作、异常提示。

扩展信息适合包含摘要、来源、时间、标签、优先级、关联对象。

详情信息适合放到详情页或展开面板里,比如完整原文、识别结果、操作日志、附件列表。

折叠态页面最怕字段堆叠。能在列表里隐藏的内容,就不要全部放到卡片上。用户需要快速判断时,字段越多,决策成本越高。

验证要看什么

这次适合截两张图。

第一张保留窄窗口状态。顶部显示 折叠态保留核心信息,卡片里只有标题、状态和按钮。这个状态下要重点看卡片高度是否克制,一屏能不能看到多条记录,按钮是否容易点击。

第二张保留展开态状态。顶部显示 展开态显示完整上下文,卡片里出现摘要、来源、时间、标签和优先级。这个状态下要重点看字段是否分组清楚,摘要是否影响列表扫描,双列卡片是否仍然保持足够留白。

如果外屏截图里一张卡片已经占了大半屏,说明字段收得还不够。如果展开态截图里卡片显得空,说明扩展信息没有把大屏价值体现出来。

总结

Pura X Max 折叠态页面的信息密度控制,核心是给字段分层。

外屏下保留标题、状态和主操作,可以让用户快速扫列表,不被摘要、标签、时间这些次级信息干扰。展开态再补充摘要、来源、时间、标签和优先级,可以减少进入详情的次数,也能让内屏空间发挥作用。

这个方法适合记录列表、通知列表、整理结果列表、任务列表、设置项列表等场景。真正落到项目里,不建议把字段简单按数量裁掉,而要按用户判断路径来排优先级。用户第一眼必须知道这是什么、现在是什么状态、能做什么;其他上下文,交给更宽的窗口展示。

Logo

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

更多推荐