欢迎光临
我们一直在努力

平行视界分栏布局与导航联动实战:大屏上“左列右详情“怎么搭【鸿蒙心迹】

在这里插入图片描述

摘要:本文围绕 HarmonyOS 平行视界(平行视窗)场景,讲清一套可复用的「断点驱动双栏联动」结构:用 mediaquery 监听窗口宽度得到断点 bp,把选中态 selectedId 上提到页面级作为唯一真相源,再由 bp 决定单栏 push 详情还是双栏同屏挂载,详情组件 DetailPane 只认 id 实现复用。文中给出骨架代码与决策流程图,并整理 5 个高频坑(右栏不刷新、折叠后选中丢失、返回键误关右栏、回调泄漏、首屏右栏空白)的「现象 → 原因 → 修复」排查路径,最后补充性能与边界条件:@ObjectLink 与 key 复用降低重渲、LazyForEach 应对大数据量、以及 lg 超大屏扩展三栏分支。

目录

  • 一、背景与痛点
  • 二、适用形态与规格
  • 三、核心代码(断点驱动的双栏)
  • 四、实战要点
  • 五、常见问题与排查
    • 问题 1:右栏点击无反应,选中项不刷新
    • 问题 2:折叠/展开后选中项丢失
    • 问题 3:返回键误关右栏
    • 问题 4:mediaquery 回调泄漏
    • 问题 5:首次进入右栏空白
  • 六、性能与边界条件
    • 6.1 DetailPane 频繁重渲的优化
    • 6.2 列表数据量大时用 LazyForEach 替代 ForEach
    • 6.3 三栏(lg 超大屏)场景下扩展 bp 分支
  • 七、小结与边界

一、背景与痛点

平板、折叠屏展开后,屏幕宽了,如果还把列表和详情做成"点了才跳下一页",空间就浪费了。HarmonyOS 的**平行视界(平行视窗)**思路是:同一应用内左右分栏,左边列表、右边详情,点左边右边实时刷新——这就是典型的 master-detail 模式。

但真做起来常踩:左右状态不同步、详情页返回丢了选中、小屏没退化成单栏、导航栈乱。本文用 Navigation + 分栏思路,把一套可复用的双栏联动结构讲清楚。

二、适用形态与规格

场景推荐布局
sm(手机) 单栏:列表 → 详情逐页跳转
md/lg(平板/折叠大屏) 双栏:左列表 + 右详情同屏
组件 Navigation + NavDestination,配合断点切换单/双栏

两种形态在交互细节上的差异,可以对照下表快速确认:

形态列表点击行为详情挂载方式返回键行为选中态存储位置
sm 单栏 点击后 push 新页面,跳转到详情 通过 NavDestination 以路由页形式挂载 返回即出栈,回到列表页 页面级 selectedId,同时作为路由参数传递
md/lg 双栏 点击只更新选中项,不跳转 作为右栏组件直接同屏挂载 返回退出应用或回首页,不关闭右栏 页面级 selectedId,左右两栏共享读取

关键点:双栏不是两套页面,而是同一 Navigation 在宽屏下的分栏呈现。用断点决定"分不分栏",而不是写死。

三、核心代码(断点驱动的双栏)

@Entry
@Component
struct SplitPage {
@State bp: string = 'sm';
@State selectedId: number = –1;

build() {
Navigation() {
if (this.bp === 'sm') {
// 单栏:列表点击后 push 详情
List({ space: 8 }) {
ForEach(this.items, (it) => ListItem() {
Text(it.title).onClick(() => {
this.selectedId = it.id;
this.pageStack.pushPath({ name: 'detail', param: it });
})
})
}
} else {
// 双栏:左列表 + 右详情同屏
Row() {
List() { /* 左栏,onClick 只更新 selectedId */ }
.width('38%')
if (this.selectedId >= 0) {
DetailPane({ id: this.selectedId }) // 右栏实时联动
.width('62%')
}
}
}
}.navDestination(this.detailBuilder)
}

@Builder detailBuilder(name: string, param: Object) {
NavDestination() { DetailPane({ id: param['id'] }) }
}
}

上面这段是骨架,下面按"断点监听 → 状态上提 → 单双栏分支 → 详情复用"四步拆开说明,方便对照自己的工程改造。

① 断点监听:bp 从哪来。 bp 不是写死的,而是由窗口宽度算出来的。实际工程里用 display 或 mediaquery 监听窗口尺寸变化,把结果写回 @State bp:

import { mediaquery } from '@kit.ArkUI';

private listener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync(
'(min-width: 600vp)'
);

aboutToAppear() {
this.bp = this.listener.matches ? 'md' : 'sm';
this.listener.on('change', (res: mediaquery.MediaQueryResult) => {
this.bp = res.matches ? 'md' : 'sm'; // 折叠/展开、旋转都会触发
});
}

aboutToDisappear() {
this.listener.off('change');
}

要点是监听要成对注册与注销,否则页面销毁后回调仍在跑,容易引发状态错乱。

② 状态上提:selectedId 为什么放在页面级。 selectedId 是左右两栏共享的唯一"真相源"。左栏点击只做一件事——改它;右栏只做一件事——读它。数据单向流动,联动自然成立:

// 左栏列表项:只更新选中态,不碰右栏
ListItem() {
Text(it.title)
.fontColor(this.selectedId === it.id ? '#0A59F7' : '#182431')
.onClick(() => { this.selectedId = it.id; })
}

如果把它塞进 ListItem 的局部变量,右栏拿不到变化,就会出现"点了没反应"。

③ 单双栏分支:同一份数据,两种挂载。 if (this.bp === 'sm') 决定的是呈现方式,不是数据来源。两种分支共用 this.items 和 this.selectedId,所以从手机切到平板时,选中项不会丢:

// 单栏:push 一个 NavDestination,参数带上 id
this.pageStack.pushPath({ name: 'detail', param: { id: it.id } });

// 双栏:不 push,直接把 id 传给右栏组件
DetailPane({ id: this.selectedId })

把上面两条分支串起来看,从窗口宽度变化到最终呈现,完整决策路径如下:

#mermaid-svg-yQLFvLwS1iCtXXEF{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yQLFvLwS1iCtXXEF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yQLFvLwS1iCtXXEF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yQLFvLwS1iCtXXEF .error-icon{fill:#552222;}#mermaid-svg-yQLFvLwS1iCtXXEF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yQLFvLwS1iCtXXEF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yQLFvLwS1iCtXXEF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yQLFvLwS1iCtXXEF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yQLFvLwS1iCtXXEF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yQLFvLwS1iCtXXEF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yQLFvLwS1iCtXXEF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yQLFvLwS1iCtXXEF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yQLFvLwS1iCtXXEF .marker.cross{stroke:#333333;}#mermaid-svg-yQLFvLwS1iCtXXEF svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yQLFvLwS1iCtXXEF p{margin:0;}#mermaid-svg-yQLFvLwS1iCtXXEF .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-yQLFvLwS1iCtXXEF .cluster-label text{fill:#333;}#mermaid-svg-yQLFvLwS1iCtXXEF .cluster-label span{color:#333;}#mermaid-svg-yQLFvLwS1iCtXXEF .cluster-label span p{background-color:transparent;}#mermaid-svg-yQLFvLwS1iCtXXEF .label text,#mermaid-svg-yQLFvLwS1iCtXXEF span{fill:#333;color:#333;}#mermaid-svg-yQLFvLwS1iCtXXEF .node rect,#mermaid-svg-yQLFvLwS1iCtXXEF .node circle,#mermaid-svg-yQLFvLwS1iCtXXEF .node ellipse,#mermaid-svg-yQLFvLwS1iCtXXEF .node polygon,#mermaid-svg-yQLFvLwS1iCtXXEF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yQLFvLwS1iCtXXEF .rough-node .label text,#mermaid-svg-yQLFvLwS1iCtXXEF .node .label text,#mermaid-svg-yQLFvLwS1iCtXXEF .image-shape .label,#mermaid-svg-yQLFvLwS1iCtXXEF .icon-shape .label{text-anchor:middle;}#mermaid-svg-yQLFvLwS1iCtXXEF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yQLFvLwS1iCtXXEF .rough-node .label,#mermaid-svg-yQLFvLwS1iCtXXEF .node .label,#mermaid-svg-yQLFvLwS1iCtXXEF .image-shape .label,#mermaid-svg-yQLFvLwS1iCtXXEF .icon-shape .label{text-align:center;}#mermaid-svg-yQLFvLwS1iCtXXEF .node.clickable{cursor:pointer;}#mermaid-svg-yQLFvLwS1iCtXXEF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yQLFvLwS1iCtXXEF .arrowheadPath{fill:#333333;}#mermaid-svg-yQLFvLwS1iCtXXEF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yQLFvLwS1iCtXXEF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yQLFvLwS1iCtXXEF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yQLFvLwS1iCtXXEF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yQLFvLwS1iCtXXEF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yQLFvLwS1iCtXXEF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yQLFvLwS1iCtXXEF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yQLFvLwS1iCtXXEF .cluster text{fill:#333;}#mermaid-svg-yQLFvLwS1iCtXXEF .cluster span{color:#333;}#mermaid-svg-yQLFvLwS1iCtXXEF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yQLFvLwS1iCtXXEF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yQLFvLwS1iCtXXEF rect.text{fill:none;stroke-width:0;}#mermaid-svg-yQLFvLwS1iCtXXEF .icon-shape,#mermaid-svg-yQLFvLwS1iCtXXEF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yQLFvLwS1iCtXXEF .icon-shape p,#mermaid-svg-yQLFvLwS1iCtXXEF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yQLFvLwS1iCtXXEF .icon-shape .label rect,#mermaid-svg-yQLFvLwS1iCtXXEF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yQLFvLwS1iCtXXEF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yQLFvLwS1iCtXXEF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yQLFvLwS1iCtXXEF :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

是:单栏

否:双栏

窗口宽度变化(折叠/展开、旋转、分屏)

mediaquery 回调 on('change')

更新 @State bp(sm / md / lg)

bp === 'sm' ?

列表点击 pushPath 跳转详情

NavDestination 挂载 DetailPane(id)

列表点击只更新 selectedId

右栏 DetailPane(id) 随 selectedId 重渲

返回键出栈,回到列表

返回键退出应用或回首页

可以看到,bp 只决定"走哪条分支",而 selectedId 始终是两栏共享的唯一真相源:单栏时它随路由参数一起传递,双栏时它直接驱动右栏重渲。这样无论窗口怎么变,选中项都不会丢,返回键行为也能按形态各自收敛。

④ 详情复用:DetailPane 只认 id。 DetailPane 的入参只有 id,内部自己按 id 取数据、渲染内容。这样单栏的 NavDestination 和双栏的右栏挂的是同一个组件,内容逻辑只写一遍:

@Component
struct DetailPane {
@Prop id: number; // 只依赖 id,不关心自己被挂在哪
build() {
Column() {
Text(`详情 #${this.id}`).fontSize(20)
// 按 id 拉取并渲染详情内容
}
}
}

⑤ 首次进入的默认选中。 双栏形态下右栏空着会显得突兀,建议在 aboutToAppear 里判断:若当前是宽屏且 selectedId < 0,默认选中列表第一项,让用户一进来就看到"左列右详情"的完整效果。

四、实战要点

要点 1:selectedId 提到页面级,联动才有"记忆"。 双栏下点左栏只更新 selectedId,右栏 DetailPane 自动随它重渲。别把选中态塞进列表项的局部变量,否则右栏不刷新。

要点 2:小屏必须退化成单栏。 用断点 bp 切换单/双栏:sm 走 push 跳转,md/lg 走同屏双栏。否则平板上也逐页跳,体验退回到手机。

要点 3:返回键行为要一致。 双栏时按返回应退出应用或回首页,而不是"关掉右栏"。在 Navigation 的 backButton/系统返回里统一处理,避免右栏点了返回却跳回列表。

要点 4:详情内容复用同一组件。 单栏的 NavDestination 和双栏的 DetailPane 用同一个详情组件,只是挂载位置不同。这样两套形态下内容逻辑零分歧。

五、常见问题与排查

双栏联动跑通之后,真正上线前往往还会遇到一些"看起来像玄学"的问题。下面按「现象 → 原因 → 修复」整理 5 个高频坑,代码均为 ArkTS 片段,可直接对照自己的工程排查。

问题 1:右栏点击无反应,选中项不刷新

现象:双栏下点左栏列表,右栏内容纹丝不动,只有重新进入页面才更新。

原因:选中态被塞进了 ListItem 的局部变量或普通成员变量,没有上提到页面级 @State,右栏读不到变化,自然不会重渲。

修复:把 selectedId 提到页面级并用 @State 修饰,左栏只负责改它。

@Entry
@Component
struct SplitPage {
@State selectedId: number = –1; // 页面级唯一真相源

build() {
Row() {
List() {
ForEach(this.items, (it: Item) => {
ListItem() {
Text(it.title)
.fontColor(this.selectedId === it.id ? '#0A59F7' : '#182431')
.onClick(() => { this.selectedId = it.id; }) // 只改状态
}
}, (it: Item) => it.id.toString())
}
.width('38%')

if (this.selectedId >= 0) {
DetailPane({ id: this.selectedId }).width('62%')
}
}
}
}

问题 2:折叠/展开后选中项丢失

现象:平板折叠成手机再展开,右栏回到空白,之前选中的条目没了。

原因:断点切换时重建了列表或详情组件,而 selectedId 存在被重建的组件内部;或者单栏 push 时只传了路由参数,没有同步回页面级状态。

修复:selectedId 始终放在页面级,单栏 push 时同时写回它,保证两种形态共用同一份状态。

// 单栏点击:既 push 路由,也写回页面级状态
.onClick(() => {
this.selectedId = it.id; // 关键:同步回页面级
this.pageStack.pushPath({ name: 'detail', param: { id: it.id } });
})

// 断点变化时不要重置 selectedId,只切换呈现方式
this.listener.on('change', (res: mediaquery.MediaQueryResult) => {
this.bp = res.matches ? 'md' : 'sm'; // 只改 bp,不动 selectedId
});

问题 3:返回键误关右栏

现象:双栏下按系统返回键,右栏被关掉、左栏还在,用户以为"退了一半"。

原因:右栏被当成一个可出栈的路由页处理,或者 Navigation 的返回逻辑没有按形态区分。

修复:双栏时右栏只是同屏组件,不参与路由栈;在 Navigation 上统一拦截返回行为。

Navigation() {
// 单/双栏内容
}
.mode(NavigationMode.Stack)
.onNavBarStateChange((visible: boolean) => {
// 双栏下右栏不产生独立路由,返回直接交给系统
})
.navDestination(this.detailBuilder)

// 双栏形态下,返回键交给系统退出/回首页
onBackPress(): boolean {
if (this.bp !== 'sm') {
return false; // 不拦截,交给系统默认行为
}
return false; // 单栏由 Navigation 自行出栈
}

问题 4:mediaquery 回调泄漏

现象:页面反复进出后,断点回调被触发多次,bp 抖动甚至报错。

原因:on('change') 注册了但没在 aboutToDisappear 里 off,页面销毁后监听仍在跑。

修复:注册与注销成对出现,并保存 listener 引用。

private listener: mediaquery.MediaQueryListener =
mediaquery.matchMediaSync('(min-width: 600vp)');

aboutToAppear() {
this.bp = this.listener.matches ? 'md' : 'sm';
this.listener.on('change', this.onBpChange);
}

aboutToDisappear() {
this.listener.off('change', this.onBpChange); // 必须成对注销
}

private onBpChange = (res: mediaquery.MediaQueryResult): void => {
this.bp = res.matches ? 'md' : 'sm';
};

问题 5:首次进入右栏空白

现象:宽屏首次进入页面,左栏有列表,右栏一片空白,观感像"没加载完"。

原因:selectedId 初始值为 -1,双栏分支里 if (this.selectedId >= 0) 不成立,右栏根本没挂载。

修复:在 aboutToAppear 里判断宽屏且未选中时,默认选中第一项。

aboutToAppear() {
this.bp = this.listener.matches ? 'md' : 'sm';
this.listener.on('change', this.onBpChange);

// 宽屏首次进入,默认选中第一项,避免右栏空白
if (this.bp !== 'sm' && this.selectedId < 0 && this.items.length > 0) {
this.selectedId = this.items[0].id;
}
}

排查这类问题的通用思路是:先确认状态存在哪(页面级还是组件内),再确认状态有没有被同步写回,最后确认监听有没有成对注销。这三步能覆盖绝大多数双栏联动的"灵异现象"。

六、性能与边界条件

双栏联动跑通、常见坑也排查完之后,还有一层容易被忽略:性能与形态边界。列表数据一多、详情内容一复杂,或者屏幕宽到可以塞下三栏时,前面那套结构就需要做针对性优化。下面按三点展开,代码均为 ArkTS 片段。

6.1 DetailPane 频繁重渲的优化

问题:DetailPane 用 @Prop id 接收选中项时,父组件每次状态变化都会把整个对象深拷贝一份传给子组件;如果详情数据本身是个较大的对象,频繁切换选中项就会带来明显的拷贝与重渲开销。

优化一:@Prop 改 @ObjectLink,只传引用不拷贝。 把详情数据抽成 @Observed 类,子组件用 @ObjectLink 持有引用,父组件改字段时子组件按需刷新,避免整对象拷贝:

@Observed
class DetailModel {
id: number = –1;
title: string = '';
content: string = '';
}

@Component
struct DetailPane {
@ObjectLink model: DetailModel; // 持有引用,不深拷贝

build() {
Column() {
Text(this.model.title).fontSize(20)
Text(this.model.content).fontSize(14)
}
}
}

// 父组件:只改 model 的字段,DetailPane 精准刷新
@State detail: DetailModel = new DetailModel();

// 切换选中项时
this.detail.id = it.id;
this.detail.title = it.title;
this.detail.content = it.content;

优化二:加 key 复用组件实例。 如果详情区是列表/多块结构,给 ForEach 或子组件加稳定的 keyGenerator,让框架按 key 复用节点而不是整片重建:

// 详情内的分块渲染:key 稳定,切换选中项时只更新变化的块
ForEach(this.detail.blocks, (block: Block) => {
DetailBlock({ block: block })
}, (block: Block) => `${this.detail.id}–${block.key}`) // key 带 id,避免跨条目误复用

要点:@Prop 适合传简单值(如 id: number),@ObjectLink 适合传大对象;key 要稳定且能区分条目,否则复用会串数据。

6.2 列表数据量大时用 LazyForEach 替代 ForEach

问题:ForEach 会一次性构建全部列表项,数据上千条时首屏卡顿、内存飙升。

优化:改用 LazyForEach + IDataSource,按需创建、滑出即回收:

class ItemDataSource implements IDataSource {
private items: Item[] = [];
private listeners: DataChangeListener[] = [];

totalCount(): number { return this.items.length; }

getData(index: number): Item { return this.items[index]; }

registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) this.listeners.push(listener);
}

unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) this.listeners.splice(pos, 1);
}
}

// 左栏列表:LazyForEach 按需渲染
private dataSource: ItemDataSource = new ItemDataSource();

List() {
LazyForEach(this.dataSource, (it: Item) => {
ListItem() {
Text(it.title)
.fontColor(this.selectedId === it.id ? '#0A59F7' : '#182431')
.onClick(() => { this.selectedId = it.id; })
}
}, (it: Item) => it.id.toString()) // key 必须唯一且稳定
}
.width('38%')

要点:LazyForEach 的 key 必须唯一稳定,数据变更要通过 notifyDataAdd/notifyDataChange 通知,不能直接改数组。

6.3 三栏(lg 超大屏)场景下扩展 bp 分支

问题:当前 bp 只有 sm / md 两档,lg 超大屏(如平板横屏、桌面模式)其实可以塞下「列表 + 详情 + 辅助栏」三栏,但代码里 else 分支把 md 和 lg 混在一起了。

优化:把断点拆成三档,bp 分支从二选一变三选一,三栏时多挂一个辅助栏:

// 断点监听拆成两段,分别判断 md 与 lg
private mdListener = mediaquery.matchMediaSync('(min-width: 600vp)');
private lgListener = mediaquery.matchMediaSync('(min-width: 840vp)');

private updateBp(): void {
if (this.lgListener.matches) {
this.bp = 'lg';
} else if (this.mdListener.matches) {
this.bp = 'md';
} else {
this.bp = 'sm';
}
}

// 分支:sm 单栏 / md 双栏 / lg 三栏
build() {
Navigation() {
if (this.bp === 'sm') {
// 单栏:push 详情
} else if (this.bp === 'md') {
// 双栏:左列表 + 右详情
Row() {
List() { /* 左栏 */ }.width('38%')
if (this.selectedId >= 0) {
DetailPane({ id: this.selectedId }).width('62%')
}
}
} else {
// 三栏:左列表 + 中详情 + 右辅助栏
Row() {
List() { /* 左栏 */ }.width('28%')
if (this.selectedId >= 0) {
DetailPane({ id: this.selectedId }).width('44%')
AuxPane({ id: this.selectedId }).width('28%') // 辅助栏,如目录/评论
}
}
}
}.navDestination(this.detailBuilder)
}

要点:断点判断要「从宽到窄」依次匹配(先 lg 再 md 最后 sm),否则宽屏会被窄断点先命中;三栏时辅助栏同样只读 selectedId,保持单一真相源不变。

七、小结与边界平行视界本质是"宽屏空间利用 + 主次信息同屏"。它依赖断点判断形态,依赖页面级状态做联动,依赖组件复用保一致。落地记住:断点切单双栏、选中态上提、返回统一、详情组件复用。


本文为原创首发实战笔记,基于 HarmonyOS 平行视界/Navigation 多形态布局能力,代码示例为调用范式示意,实际接口以华为官方文档为准。

赞(0)
未经允许不得转载:171主机测评 » 平行视界分栏布局与导航联动实战:大屏上“左列右详情“怎么搭【鸿蒙心迹】
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址