鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 02:折叠态页面的信息密度控制
前言
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 折叠态页面的信息密度控制,核心是给字段分层。
外屏下保留标题、状态和主操作,可以让用户快速扫列表,不被摘要、标签、时间这些次级信息干扰。展开态再补充摘要、来源、时间、标签和优先级,可以减少进入详情的次数,也能让内屏空间发挥作用。
这个方法适合记录列表、通知列表、整理结果列表、任务列表、设置项列表等场景。真正落到项目里,不建议把字段简单按数量裁掉,而要按用户判断路径来排优先级。用户第一眼必须知道这是什么、现在是什么状态、能做什么;其他上下文,交给更宽的窗口展示。
更多推荐



所有评论(0)