鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 03:展开态列表详情联动布局 原创
头像 小雨同学 2026-05-13 08:44:21    发布
1 浏览 0 点赞 0 收藏

前言

Pura X Max 展开后,列表页最明显的变化是横向空间变多了。外屏下点击一条记录再进入详情页,这个路径很自然;到了展开态,如果还沿用同样的跳转方式,用户就会在列表页和详情页之间反复切换,屏幕右侧的大块空间也没有被用起来。

Pura X Max 外屏为 5.4 英寸,内屏为 7.7 英寸,系统版本为 HarmonyOS 6.1。外屏和内屏的尺寸差异足够明显,列表页在展开态下可以把列表和详情放到同一个页面里。用户点击左侧列表,右侧详情直接更新,阅读路径会短很多。

HarmonyOS 多设备页面布局里,空间充足时可以采用分栏布局,把窗口划分为两栏或三栏,用来展示多类内容。响应式布局也把分栏布局作为常见方式之一,适合把导航区和内容区同屏左右展示。

我在处理列表详情页时,会先保留窄屏的普通列表体验,再给展开态增加右侧详情区。这样做不会影响外屏的操作习惯,也能让内屏真正承担更多信息。

问题出在展开态仍然跳转

普通手机上的列表页,一般是这样的路径:

点击列表项。

进入详情页。

返回列表。

再点击另一项。

这个路径适合窄屏,因为一屏很难同时放下列表和详情。用户每次只关注一个页面,层级也比较清楚。

Pura X Max 展开态就不一样了。内屏宽度变大以后,继续让列表占满整屏,会出现几个问题。

列表卡片被横向拉宽,但信息密度没有明显提升。

查看不同记录时,需要频繁进入详情和返回列表。

用户已经拥有更大的屏幕,页面仍然按窄屏流程工作。

这种页面更适合做成主从结构。左侧列表负责选择对象,右侧详情负责展示内容。用户不需要离开当前页面,就能快速比较不同记录。

这里不需要一开始就引入复杂路由。先用 Row 把页面分成左右两块,左侧放列表,右侧放当前选中项的详情。Row 本身就是沿水平方向布局的容器,适合搭建这种左右结构。

用 Row 改成列表详情

页面仍然根据窗口宽度判断布局状态。

窄屏进入 compact,只展示列表。这个状态适合外屏、普通手机宽度和分屏后的窄窗口。

宽屏进入 expanded,页面切成左右两栏。左侧列表负责切换选中项,右侧详情展示选中记录的完整内容。

判断逻辑仍然保持简单:

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

这里把阈值设置成 760vp,是为了给右侧详情区留出足够空间。列表详情联动比双列卡片更吃宽度,如果阈值过低,左右两栏都会显得挤。

展开态布局可以这样理解:

Row() {  // 左侧列表  // 右侧详情}

窄屏布局则保持单页面列表:

Column() {  // 普通列表}

这个结构的关键点在于:布局可以变化,但选中数据不变。当前选中的记录由 selectedId 保存,左侧列表点击后更新它,右侧详情根据它渲染内容。

把列表和详情放进同一个页面

下面这个页面模拟了一组材料记录。窄窗口下只显示普通列表;展开态下,左侧显示列表,右侧显示当前选中记录详情。点击左侧不同记录,右侧详情会立即变化。

页面放在 entry/src/main/ets/pages/Index.ets 即可运行。Pura X Max 适配调试可以使用 DevEco Studio 6.1.0,并安装 Pura X Max 模拟器验证不同窗口形态下的表现。

interface MaterialItem {  id: number;  title: string;  status: string;  source: string;  time: string;  tag: string;  owner: string;  summary: string;  detail: string;  todo: string;}​@Entry@Componentstruct Index {  @State private pageWidth: number = 0;  @State private selectedId: number = 1;​  private readonly expandedWidth: number = 760;​  private readonly materials: MaterialItem[] = [    {      id: 1,      title: '社区物业缴费提醒',      status: '待处理',      source: '拍照整理',      time: '09:20',      tag: '通知',      owner: '物业服务中心',      summary: '识别到缴费截止日期、费用明细和办理地点。',      detail: '这条记录来自一张物业缴费通知。内容里包含缴费周期、应缴金额、截止日期和办理地点。折叠态下只需要知道它是一条待处理提醒,展开态下可以直接看到更多上下文,减少进入详情页的次数。',      todo: '添加缴费提醒,并确认是否需要同步到日程。'    },    {      id: 2,      title: 'Pura X Max 适配会议纪要',      status: '待确认',      source: '语音转写',      time: '10:45',      tag: '会议',      owner: '产品研发组',      summary: '整理出外屏、展开态、横屏和悬停态几类页面问题。',      detail: '会议讨论了多个页面在 Pura X Max 上的展示问题,其中列表页、详情页、设置页和图片预览页都需要重新检查窗口变化后的布局表现。展开态更适合使用列表详情结构,减少页面跳转。',      todo: '确认适配清单,并把列表详情联动加入开发任务。'    },    {      id: 3,      title: '活动报名确认单',      status: '已保存',      source: '相册导入',      time: '11:30',      tag: '表单',      owner: '活动运营',      summary: '提取到报名人、联系方式、活动时间和签到地址。',      detail: '这条记录适合在列表右侧直接查看摘要和关键字段。用户通常只是确认活动时间和地点,不一定需要进入完整详情页。',      todo: '保留记录,并在活动前一天提醒。'    },    {      id: 4,      title: '客户需求变更记录',      status: '待处理',      source: '文本整理',      time: '13:10',      tag: '项目',      owner: '客户成功组',      summary: '本次变更涉及首页布局、权限配置和通知策略。',      detail: '需求变更类记录往往需要反复对照多个条目。展开态下把列表和详情放在同一屏,可以减少返回列表的频率,也方便连续检查不同变更项。',      todo: '同步项目负责人,并拆分到研发排期。'    },    {      id: 5,      title: '课程作业提交说明',      status: '已整理',      source: '拍照整理',      time: '15:25',      tag: '学习',      owner: '课程助教',      summary: '识别到提交时间、文件格式、命名规范和邮箱地址。',      detail: '学习类通知一般字段较多。展开态详情区可以把提交要求完整展示出来,左侧列表继续保留其他记录,切换查看会更快。',      todo: '创建作业待办,并保留提交格式说明。'    }  ];​  private isExpanded(): boolean {    return this.pageWidth >= this.expandedWidth;  }​  private getSelectedItem(): MaterialItem {    const found = this.materials.find((item: MaterialItem) => item.id === this.selectedId);    return found ? found : this.materials[0];  }​  private getPagePadding(): number {    return this.isExpanded() ? 24 : 16;  }​  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 StatusPill(status: string) {    Text(status)      .fontSize(12)      .fontColor(this.getStatusColor(status))      .padding({ left: 8, right: 8, top: 4, bottom: 4 })      .backgroundColor(this.getStatusBgColor(status))      .borderRadius(999)  }​  @Builder  private ListCard(item: MaterialItem) {    Column({ space: 10 }) {      Row({ space: 8 }) {        this.StatusPill(item.status)​        if (this.isExpanded()) {          Text(item.tag)            .fontSize(12)            .fontColor('#2F8F83')            .padding({ left: 8, right: 8, top: 4, bottom: 4 })            .backgroundColor('#E6F4F1')            .borderRadius(999)        }​        Blank()​        if (this.selectedId === item.id) {          Text('当前')            .fontSize(12)            .fontColor('#2F8F83')        }      }      .width('100%')​      Text(item.title)        .fontSize(17)        .fontWeight(FontWeight.Medium)        .fontColor('#111827')        .maxLines(2)        .textOverflow({ overflow: TextOverflow.Ellipsis })​      if (this.isExpanded()) {        Text(item.summary)          .fontSize(13)          .fontColor('#6B7280')          .lineHeight(19)          .maxLines(2)          .textOverflow({ overflow: TextOverflow.Ellipsis })      }​      Row({ space: 8 }) {        Text(item.source)          .fontSize(12)          .fontColor('#6B7280')​        Text('·')          .fontSize(12)          .fontColor('#9CA3AF')​        Text(item.time)          .fontSize(12)          .fontColor('#6B7280')      }      .width('100%')    }    .width('100%')    .padding(15)    .backgroundColor(this.selectedId === item.id ? '#EEF7F5' : '#FFFFFF')    .borderRadius(18)    .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    })    .onClick(() => {      this.selectedId = item.id;    })  }​  @Builder  private ListPanel() {    Column({ space: 12 }) {      Row() {        Column({ space: 4 }) {          Text(this.isExpanded() ? '材料列表' : '整理记录')            .fontSize(this.isExpanded() ? 22 : 24)            .fontWeight(FontWeight.Bold)            .fontColor('#111827')​          Text(this.isExpanded() ? '点击左侧记录,右侧详情会同步更新' : '外屏保持普通列表浏览')            .fontSize(14)            .fontColor('#6B7280')        }        .layoutWeight(1)​        Text(Math.round(this.pageWidth).toString() + 'vp')          .fontSize(12)          .fontColor('#374151')          .padding({ left: 10, right: 10, top: 6, bottom: 6 })          .backgroundColor('#FFFFFF')          .borderRadius(999)      }      .width('100%')​      Scroll() {        Column({ space: 12 }) {          ForEach(this.materials, (item: MaterialItem) => {            this.ListCard(item)          }, (item: MaterialItem) => item.id.toString())        }        .width('100%')        .padding({ bottom: 20 })      }      .layoutWeight(1)      .width('100%')      .edgeEffect(EdgeEffect.Spring)    }    .width('100%')    .height('100%')  }​  @Builder  private DetailPanel(item: MaterialItem) {    Column({ space: 18 }) {      Row() {        this.StatusPill(item.status)​        Blank()​        Text(item.tag)          .fontSize(13)          .fontColor('#2F8F83')          .padding({ left: 10, right: 10, top: 5, bottom: 5 })          .backgroundColor('#E6F4F1')          .borderRadius(999)      }      .width('100%')​      Column({ space: 8 }) {        Text(item.title)          .fontSize(27)          .fontWeight(FontWeight.Bold)          .fontColor('#111827')          .lineHeight(34)​        Text(item.summary)          .fontSize(15)          .fontColor('#4B5563')          .lineHeight(22)      }      .width('100%')      .alignItems(HorizontalAlign.Start)​      Row({ space: 10 }) {        this.MetaBlock('来源', item.source)        this.MetaBlock('时间', item.time)        this.MetaBlock('负责人', item.owner)      }      .width('100%')​      Column({ space: 8 }) {        Text('内容整理')          .fontSize(16)          .fontWeight(FontWeight.Medium)          .fontColor('#111827')​        Text(item.detail)          .fontSize(15)          .fontColor('#4B5563')          .lineHeight(24)      }      .width('100%')      .padding(16)      .backgroundColor('#F9FAFB')      .borderRadius(18)​      Column({ space: 8 }) {        Text('建议动作')          .fontSize(16)          .fontWeight(FontWeight.Medium)          .fontColor('#111827')​        Text(item.todo)          .fontSize(15)          .fontColor('#4B5563')          .lineHeight(23)      }      .width('100%')      .padding(16)      .backgroundColor('#F3F8F7')      .borderRadius(18)​      Blank()​      Row({ space: 12 }) {        Button('标记完成')          .fontSize(15)          .fontColor('#FFFFFF')          .height(42)          .layoutWeight(1)          .backgroundColor('#2F8F83')          .borderRadius(21)​        Button('进入详情')          .fontSize(15)          .fontColor('#2F8F83')          .height(42)          .layoutWeight(1)          .backgroundColor('#E6F4F1')          .borderRadius(21)      }      .width('100%')    }    .width('100%')    .height('100%')    .padding(24)    .backgroundColor('#FFFFFF')    .borderRadius(24)    .shadow({      radius: 12,      color: '#10000000',      offsetX: 0,      offsetY: 4    })  }​  @Builder  private MetaBlock(label: string, value: string) {    Column({ space: 4 }) {      Text(label)        .fontSize(12)        .fontColor('#9CA3AF')​      Text(value)        .fontSize(14)        .fontColor('#374151')        .maxLines(1)        .textOverflow({ overflow: TextOverflow.Ellipsis })    }    .layoutWeight(1)    .padding(12)    .backgroundColor('#F9FAFB')    .borderRadius(14)  }​  build() {    Column() {      if (this.isExpanded()) {        Row({ space: 18 }) {          Column() {            this.ListPanel()          }          .width(340)          .height('100%')​          Column() {            this.DetailPanel(this.getSelectedItem())          }          .layoutWeight(1)          .height('100%')        }        .width('100%')        .height('100%')        .padding(24)      } else {        Column() {          this.ListPanel()        }        .width('100%')        .height('100%')        .padding({          left: this.getPagePadding(),          right: this.getPagePadding(),          top: 18        })      }    }    .width('100%')    .height('100%')    .backgroundColor('#F6F7F9')    .onAreaChange((_: Area, newValue: Area) => {      const width = Number(newValue.width);      if (!Number.isNaN(width) && width > 0) {        this.pageWidth = width;      }    })  }}

关键实现点和适配边界

这个页面最重要的状态只有两个。

一个是窗口宽度。

@State private pageWidth: number = 0;

另一个是当前选中记录。

@State private selectedId: number = 1;

宽度决定当前采用列表模式还是列表详情模式,选中记录决定右侧展示什么内容。

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

展开态通过 Row 切出左右两栏。

Row({ space: 18 }) {  Column() {    this.ListPanel()  }  .width(340)​  Column() {    this.DetailPanel(this.getSelectedItem())  }  .layoutWeight(1)}

左侧宽度我设置成 340vp。这个数值不是固定标准,只是为了让列表项在展开态下保持稳定宽度。真实项目里可以根据业务列表内容调整,比如记录标题普遍较长,可以给到 360vp;如果只是短标题列表,320vp 就够用。

右侧详情使用 layoutWeight(1) 占满剩余空间。这样窗口继续变宽时,详情区获得更多空间,左侧列表不会被拉得过宽。

窄屏下只渲染 ListPanel()

Column() {  this.ListPanel()}

真实项目里,窄屏点击列表项通常会进入详情页。为了让页面能在一个 Index.ets 里直接看到效果,示例里保留了点击选中状态,没有额外做路由跳转。回到项目时,可以把 compact 下的点击事件换成 Navigation 路由,把 expanded 下的点击事件保留为更新 selectedId

这个方案适合材料列表、会议列表、客户列表、任务列表、消息列表等场景。它不适合所有列表。像聊天消息、时间线动态、审批流记录这类强顺序内容,更适合保持单列连续阅读,不宜强行拆成左右两栏。

验证要看什么

外屏或窄窗口里,页面应该是普通列表。顶部显示当前窗口宽度,列表从上到下排列,卡片宽度不会被压缩。这个状态下要重点看卡片标题是否能读完,状态标签是否明显,滚动是否自然。

展开态里,左侧应该是固定宽度列表,右侧是详情内容。点击左侧不同记录,右侧标题、摘要、来源、负责人、内容整理和建议动作都会切换。这个状态下要重点看三个位置。

左侧列表是否过宽。

右侧详情是否有足够阅读空间。

点击切换时,选中态和详情内容是否一致。

如果右侧详情只是重复左侧标题,分栏的价值就不明显。展开态的详情区应该承载更多上下文,比如摘要、完整整理内容、建议动作、操作按钮和关联信息。

总结

Pura X Max 展开态适合把列表页从单页面浏览改成列表详情联动。

外屏继续保持普通列表,符合窄屏操作习惯;展开态把列表和详情放到同一屏,用户点击左侧记录,右侧内容直接更新,减少来回跳转。这个结构的关键是把窗口宽度和选中状态分开处理。窗口宽度决定页面结构,选中状态决定详情内容。

实际项目里可以把这个思路放到材料整理、会议记录、客户资料、任务管理和设置分类页面中。只要列表项和详情之间存在频繁切换的需求,展开态分栏就能明显减少操作路径。


©本站发布的所有内容,包括但不限于文字、图片、音频、视频、图表、标志、标识、广告、商标、商号、域名、软件、程序等,除特别标明外,均来源于网络或用户投稿,版权归原作者或原出处所有。我们致力于保护原作者版权,若涉及版权问题,请及时联系我们进行处理。
分类
HarmonyOS
头像

小雨同学

产品总监、独立开发者社群主理人、资深全栈工程师,HarmonyOS应用开发者高级认证,PMP认证,CSDN博客专家,鸿蒙极客,Trae Fellow,阿里云社区专家博主、51CTO 博客专家、OpenTiny 优秀布道师、科大讯飞荣誉讲师。

2

帖子

0

提问

24

粉丝

关注
热门推荐
地址:北京市朝阳区北三环东路三元桥曙光西里甲1号第三置业A座1508室 商务内容合作QQ:2291221 电话:13391790444或(010)62178877
版权所有:电脑商情信息服务集团 北京赢邦策略咨询有限责任公司
声明:本媒体部分图片、文章来源于网络,版权归原作者所有,我司致力于保护作者版权,如有侵权,请与我司联系删除

京ICP备:2022009079号-2

京公网安备:11010502051901号

ICP证:京B2-20230255