导航该收起来,还是留在左边?HarmonyOS 6 侧栏小练习
1. 前言
路线收藏页有四个分类:城市漫步、周末山野、海边日落和收藏路线。手机宽度下,分类一直占着左边,正文会被挤得很窄;宽窗口下,每次切分类都先打开一个抽屉,又显得多了一步。
这次让同一个导航内容根据空间换一种出现方式:宽时常驻左侧,窄时放到覆盖式抽屉里。分类选中状态始终是一份,切完分类后收起抽屉,正文立即显示对应路线。
2. 常驻侧栏与覆盖抽屉
|
当前空间 |
导航如何出现 |
正文如何使用空间 |
|
宽度达到 760 vp |
左侧常驻 |
使用侧栏以外的区域 |
|
窄窗且抽屉关闭 |
不占正文宽度 |
正文铺满可用区域 |
|
窄窗且抽屉打开 |
覆盖在正文上方 |
保留正文位置,显示遮罩 |
这里最重要的是“关闭的抽屉不占宽度”。如果只是把导航内容变透明,却仍然保留一列空容器,正文依旧会少一块空间,看起来像是抽屉已经收起,布局却没有真正恢复。
在需要长期停留的桌面应用里,常驻导航还能减少用户来回打开目录的次数;在较小窗口中,正文优先使用空间。这里没有按设备名称直接写死模式,同一台二合一设备上的窄窗口,也会采用抽屉形态。用户正在使用的空间,比设备标签更适合决定这项布局。
导航宽度不是越大越好。四个短分类在 260 vp 内可以完整显示,正文仍然有足够阅读空间;若以后加入很长的项目名、头像和多行说明,应重新评估侧栏宽度与阈值。只把阈值照抄过来,却完全更换内容,往往又会回到文字拥挤的问题。
这里先做一个交互决定:内容空间不足时,让路线正文优先。目录仍然能打开,但平时不占据一列;空间足够时,目录就留在左侧,切分类少一步。这个决定会影响容器、按钮和关闭行为,所以先写清楚约定,再选 Row 或 Stack 来实现,比先画出一个抽屉再补规则更顺。
2.1 Row与Stack容器代码
下面使用 Row 和 Stack 组织常驻侧栏与覆盖抽屉,空间关系可以直接从组件结构中看清。已有导航框架的工程,可以沿用 SideBarContainer 或 Navigation,接入相同的宽度条件与选择状态,并让原有导航体系继续处理返回和焦点。
pages/Index.ets:
import { LengthMetrics } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { resizeWindow } from '../common/WindowTools';
@Entry
@Component
struct Index {
@State private viewportWidth: number = 0;
@State private opened: boolean = false;
@State private selected: number = 0;
@State private status: string = '选择一个路线分类';
private categories: string[] = ['城市漫步', '周末山野', '海边日落', '收藏路线'];
private descriptions: string[] = ['旧书店 → 林荫道 → 河边小桥', '公交终点 → 松林缓坡 → 山顶凉亭', '灯塔入口 → 礁石栈道 → 落日观景台', '把走过的好路线留下来,下次不用重新找'];
private async resize(w: number): Promise<void> {
this.status = await resizeWindow(this.getUIContext().getHostContext() as common.UIAbilityContext, w, this.getUIContext().vp2px(1));
}
@Builder
navigation() {
Column({ space: 16 }) {
Text('走走地图').fontSize(26).fontWeight(FontWeight.Bold).fontColor('#D9E8DF').width('100%').padding({ bottom: 24 });
ForEach(this.categories, (name: string, i: number) => {
Text(`${i + 1} ${name}`).fontSize(18).fontColor(this.selected === i ? '#123C31' : '#D9E8DF')
.width('100%').padding(18).borderRadius(12).backgroundColor(this.selected === i ? '#C8E7CC' : '#1F4E40')
.id(`nav${i}`).onClick(() => { this.selected = i; this.opened = false; });
});
Blank();
if (this.viewportWidth < 760) {
Button('收起导航').id('closeNav').onClick(() => { this.opened = false; });
}
}.padding(22).height('100%').backgroundColor('#1F4E40');
}
@Builder
content() {
Column({ space: 24 }) {
Flex({ wrap: FlexWrap.Wrap, space: { main: LengthMetrics.vp(8), cross: LengthMetrics.vp(8) } }) {
if (this.viewportWidth < 760) {
Button('目录').id('menu').backgroundColor('#3B7960').onClick(() => { this.opened = true; });
}
Button('窄窗').id('narrow').onClick(() => this.resize(460));
Button('宽窗').id('wide').onClick(() => this.resize(1100));
}.width('100%');
Text(this.categories[this.selected]).fontSize(34).fontWeight(FontWeight.Bold).fontColor('#244D3F').width('100%').id('selectedTitle');
Text(this.descriptions[this.selected]).fontSize(20).lineHeight(34).fontColor('#637D70').width('100%');
Column({ space: 18 }) {
Text('慢一点,也能到').fontSize(30).fontColor('#F6F1DB').fontWeight(FontWeight.Bold);
Text('约 4.8 公里 · 轻松路线').fontSize(17).fontColor('#D7E3D4');
Text('沿途留三次停下来的时间').fontSize(15).fontColor('#D7E3D4');
}.width('100%').layoutWeight(1).backgroundColor('#6D9077').borderRadius(26).padding(30);
Text(`${Math.round(this.viewportWidth)} vp · ${this.viewportWidth >= 760 ? '导航常驻' : '抽屉导航'}`).fontSize(13).id('mode').fontColor('#637D70').width('100%');
}.padding(28).width('100%').height('100%').backgroundColor('#F2F5EB');
}
build() {
Stack({ alignContent: Alignment.Start }) {
Row() {
if (this.viewportWidth >= 760) {
Column() { this.navigation(); }.width(260);
}
Column() { this.content(); }.layoutWeight(1);
}.width('100%').height('100%');
if (this.viewportWidth < 760 && this.opened) {
Row() {
Column() { this.navigation(); }.width('80%').constraintSize({ maxWidth: 300 });
Column().layoutWeight(1).height('100%').onClick(() => { this.opened = false; });
}.width('100%').height('100%').backgroundColor('#88000000');
}
}.width('100%').height('100%').onAreaChange((_old: Area, area: Area) => { this.viewportWidth = Number(area.width); });
}
}
common/WindowTools.ets:
import { window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
export async function resizeWindow(context: common.UIAbilityContext, widthVp: number,
density: number): Promise<string> {
try {
const mainWindow = await window.getLastWindow(context);
const rect = mainWindow.getWindowProperties().windowRect;
await mainWindow.resize(Math.round(widthVp * density), rect.height);
return '窗口已调整';
} catch (error) {
return '请先将窗口还原为自由窗口,再调整宽度';
}
}

本例路线都是内存里的示例文字,没有地图服务、定位或真实线路规划。背景卡片表达的是路线摘要,不是可交互地图。继续扩展时,可以让分类控制数据查询,但不要在每次抽屉展开时重新加载整页;用户只是打开目录,不应因此丢掉正文阅读位置。
继续扩展为多级目录时,可以先保存当前分类与展开层级,再决定抽屉如何显示。宽窄窗口切换不应替用户退回目录顶层,也不应把一个已选的子分类变成不存在的状态。先用这四个简单分类确认状态关系,再增加层级,比一开始就塞进复杂树形结构更容易检查。
3. 分类与抽屉状态
viewportWidth 决定当前属于宽窗还是窄窗,opened 只表示窄窗抽屉是否展开,selected 保存分类。这三个状态分别描述空间、临时展开动作和业务选择,避免一个变量同时承担几种含义。
navigation() 构建四个分类与关闭按钮。宽窗时放进固定 260 vp 的左栏,窄窗展开时放进 Stack 上层。选中项的底色与文字颜色由 selected 决定,所以同一个分类不会在常驻栏里高亮、换成抽屉后又忘记自己。
点击分类后,先更新 selected,再把 opened 设为 false。这个顺序让窄窗里的抽屉自然收起,也不影响宽窗。关闭按钮只在窄窗出现;宽窗导航本来就应该留在左边,额外放一个“收起”按钮会破坏这次练习的布局约定。

3.1 遮罩点击与关闭入口
抽屉之外的剩余区域承担遮罩点击,用户可以点击外侧关闭。遮罩与正文属于不同层,正文没有被重新创建成另一套业务页面。正式应用还应继续考虑系统返回、焦点管理与无障碍朗读,本例先把最基本的展开、选择和收起过程写清楚。
遮罩的颜色与点击区域也分开考虑。半透明背景让用户知道正文仍在下面,右侧的空白区域负责收起操作,导航列表本身则保留正常点击。不能把一个覆盖全屏的关闭监听直接压到菜单项上,否则用户点击分类时可能只触发关闭,根本选不中内容。
另一个值得试的过程是:在窄窗打开抽屉后,直接点击遮罩。正文的分类不应该因此改变,变化的只是 opened;再次打开目录,原来的高亮还在。如果关闭抽屉时顺手重置 selected,用户很快就会发现每次收起目录都回到了第一页。
最后,菜单入口需要清楚。窄窗时它出现在正文工具区,文字直接叫“目录”;用户不必猜一个陌生图标是什么功能。以后换成图标,也应保留可理解的辅助说明和足够的点击区域,不能因为想让页面显得简洁就把入口藏起来。
4. 窗口变化后的分类核对

演示先在宽窗选择“周末山野”,再缩到窄窗,打开目录并选择“海边日落”,最后展开窗口。常驻导航的高亮与正文标题都停在刚选的分类,没有回到第一项。
这是 HarmonyOS 6.1.1、API 24 的二合一模拟器实际窗口操作。窄窗按钮调用窗口接口缩小应用;页面再从自己的内容区域获取宽度。菜单按钮是否出现、抽屉是否覆盖正文,取决于真实的宽度条件。

可以先让抽屉打开,再把窗口拉宽。宽窗分支会显示常驻导航,覆盖层只在窄窗条件下存在,因此不会把一层半透明遮罩留在已经变宽的正文上。反过来,从宽窗收窄时,正文也不会继续保留固定侧栏占用的空间。
有一个便于理解的状态组合:分类仍是“海边日落”,但 opened 可以从 true 变为 false。关闭目录不应改分类;窗口变宽后是否显示常驻导航,也不应再由 opened 决定。将这三个条件分别看待,才不会出现正文已经换好,导航高亮却停在另一个分类的情况。
5. 总结
判断这份导航是否合适,可以回到一次切路线的动作:找得到目录,选得中分类,关掉后正文仍是刚才那一条。宽窄变化只影响目录出现的位置和时机。后面扩展层级或接入真实路线时,也先保住这一条连贯的操作。
这份示例可以继续发展成路线收藏夹、阅读分类或资料导航。先把展开与关闭、当前选择和正文空间确认好,再逐步补上真实业务数据,页面会比直接堆一排菜单更容易维护。
更多推荐

所有评论(0)