v0.6.0 图标改为开抽屉入口 + 新增底部悬浮快捷栏(照鸿蒙 6 悬浮导航规范)
用户 2026-10-12 三条要求。
## ① 点顶栏图标 = 开抽屉
用户:「把那个图标改成这个功能吧,就是当用户点击左上角那个图标的时候,把抽屉列出来」。
⇒ `TopBar` 的 logo 加 `.onClick { drawerOpen = true }`。
⚠️ 点击直接挂在 Image 上、**不套 Button** —— Button 自带最小尺寸与内边距,
会把图标挤离左边线(用户要"图标靠左")。
⇒ 开抽屉现有三个入口:点图标 / 应用内水平滑动 / 底部悬浮栏。
## ② 底部悬浮快捷栏 FbFloatDock(新增)
用户:「页面正下方设置几个浮动按钮…比如日程、名册、任务和笔记…
这样用户不用拉抽屉,就可以直接点击使用。
但一定要注意,使用浮动的方式,**不要固定在下面**,而是**浮动着的**。」
🔴 **形态照鸿蒙 6 官方设计规范的「悬浮导航(Float Navigation)」**:
「导航栏脱离底部边界,以**四周留白、圆角卡片**形态悬浮于内容之上」。
参考实现参数直接用上:`height(56) + margin({left:16,right:16,bottom:16})
+ borderRadius(28)`,材质用系统沉浸材质
`uiMaterial.ImmersiveMaterial({ style: ULTRA_THIN })`(设计规范的"超薄"档)。
⚠️ 用了 systemMaterial 就不再叠加 backgroundBlurStyle/backgroundColor ——
材质自带模糊+边框+阴影,叠加会双重模糊发灰。
⚠️ **摆法关键**:`Index` 的根容器从 `Column` 改成
`Stack({ alignContent: Bottom })` —— 写在 Column 里它就是"占一行的固定底栏"
(老样式);用 Stack 覆盖 ⇒ 内容从它下面穿过、它浮在上面。
(实测印证:栏体 y=2132..2307,而"我的家人"标题在 y=2336 仍可见 ⇒ 内容穿过去了。)
⚠️ 内容底部加 `FB_DOCK_RESERVE`(88vp) 预留,否则最后一屏内容会被悬浮栏压住。
12 个页面统一处理。
⚠️ 宽屏不显示(抽屉常驻,且屏宽用不上);抽屉展开时也不显示(避免浮层叠浮层)。
⚠️ 选中项用模块主题色高亮,用户一眼知道"我在哪"。
## 验证(真机,全部通过)
· 点顶栏图标 ⇒ 抽屉打开 ✓
· 点悬浮栏「任务」⇒ 切到任务页 ✓;点「日程」⇒ 切到日程页 ✓
· 悬浮栏几何:栏体 x=21..1060、y=2132..2307(高 58vp),
栏外 x=0..20 为纸色 ⇒ **四周留白** 符合规范 ✓
· 栏内呈半透材质色(#EFEBE2) ⇒ `ImmersiveMaterial` 生效 ✓
· 14 页遍历 **14/14**;深验 **12/12**;编译 **0 error / 0 warning**
## 过程中的一个坑
给 12 个页面补 `FB_DOCK_RESERVE` 的 import 时,脚本统一写成
`'../components/FloatDock'` —— 但**页面在 `pages/<模块>/` 下,需要 `../../`**
(`Index.ets` 在 `pages/` 下才是 `../`)。批量改 import 路径要按**文件深度**区分。
This commit is contained in:
@@ -18,6 +18,7 @@ import { FbColors, FbMetric } from '../common/Theme';
|
||||
import { FbAppState, FB_WIDE_BREAKPOINT, fbAppState } from '../common/Store';
|
||||
import { FbDrawerShell } from '../components/Drawer';
|
||||
import { FbTopBar } from '../components/TopBar';
|
||||
import { FbFloatDock } from '../components/FloatDock';
|
||||
import { FbHomePage } from './home/FbHomePage';
|
||||
import { FbMoviesPage } from './movies/FbMoviesPage';
|
||||
import { FbTripsPage } from './trips/FbTripsPage';
|
||||
@@ -64,20 +65,28 @@ struct Index {
|
||||
}
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
FbDrawerShell({ app: this.app }) {
|
||||
this.mainArea()
|
||||
// 🔴 用 Stack 而不是 Column:底部快捷栏要**浮在内容之上**(用户要求"不要固定在下边,
|
||||
// 要浮着")。写在 Column 里它就是"占一行的固定底栏"(老样式)。
|
||||
Stack({ alignContent: Alignment.Bottom }) {
|
||||
Column() {
|
||||
FbDrawerShell({ app: this.app }) {
|
||||
this.mainArea()
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
// 底部悬浮快捷栏(日程 / 名册 / 任务 / 笔记)
|
||||
// ⚠️ 只在窄屏显示:宽屏时抽屉常驻,且屏幕宽,用不上这个快捷入口。
|
||||
// ⚠️ 抽屉展开时不显示 —— 那时用户正在抽屉里选模块,浮层再叠一个反而乱。
|
||||
if (!this.app.wideLayout && !this.app.drawerOpen) {
|
||||
FbFloatDock({ app: this.app })
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
// 🔴 沉浸式的全部秘诀在这一句(照 SyncCalendar 的写法):
|
||||
// `expandSafeArea` 让**根容器的背景**延伸到状态栏/手势条区域,
|
||||
// 但**不移动任何子组件** —— 内容仍在安全区内正常排版,
|
||||
// 只有背景色一路铺到屏幕边缘。
|
||||
// 再配合 EntryAbility 里"系统栏全透明",顶部通知栏透出的就是这层背景色,
|
||||
// 于是「上面那一块和通知栏完全融为一体」。
|
||||
// ⇒ 不需要手算状态栏高度、不需要给顶栏加 padding,也就不会算错。
|
||||
// 沉浸式:让**根容器背景**延伸到状态栏/手势条区域,但不移动子组件。
|
||||
// 配合 EntryAbility 里"系统栏与顶栏同色",通知栏与标题栏融为一体。
|
||||
.backgroundColor(FbColors.paper)
|
||||
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbBookItem, FbGroup } from '../../model/Types';
|
||||
import { FB_DEMO_BOOKS, fbGroupBook } from '../../model/DemoData';
|
||||
import { FbEmptyBox, FbIconButton, FbProgressLine, FbRating, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbBooksPage {
|
||||
@@ -83,6 +84,8 @@ export struct FbBooksPage {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
@@ -18,6 +18,7 @@ import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbCalBook, FbCalEvent } from '../../model/Types';
|
||||
import { FB_DEMO_CAL_BOOKS, FB_DEMO_EVENTS_SOON, FB_DEMO_EVENTS_TODAY } from '../../model/DemoData';
|
||||
import { FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
/** 月历上「非本月」日期点的灰(hex 常量:格子数据结构里存的是字符串色,不能用 Resource) */
|
||||
const FB_FAINT: string = '#A8988A';
|
||||
@@ -236,6 +237,8 @@ export struct FbCalendarPage {
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 28 })
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.scrollBar(BarState.Off)
|
||||
|
||||
@@ -15,6 +15,7 @@ import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbAnniversaryItem, FbKinChip } from '../../model/Types';
|
||||
import { FB_DEMO_ANNIVERSARIES } from '../../model/DemoData';
|
||||
import { FbAvatar, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbDaysPage {
|
||||
@@ -96,6 +97,8 @@ export struct FbDaysPage {
|
||||
.scrollable(ScrollDirection.Horizontal)
|
||||
.scrollBar(BarState.Off)
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
}
|
||||
.padding(FbMetric.cardPad)
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
FB_DEMO_MODULE_CARDS, FB_DEMO_TASKS, FB_DEMO_WEATHER, FB_DEMO_WEEK_STAT
|
||||
} from '../../model/DemoData';
|
||||
import { FbAvatar, FbDot, FbDotLine, FbSectionTitle, FbStatTriple, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbHomePage {
|
||||
@@ -153,6 +154,8 @@ export struct FbHomePage {
|
||||
}, (mk: string, idx: number): string => 'mk_' + idx.toString() + '_' + mk)
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.alignItems(HorizontalAlign.Start)
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
FB_DEMO_LEDGER_BOOKS, FB_DEMO_LEDGER_ENTRIES, fbLedgerKindName, fbMoney
|
||||
} from '../../model/DemoData';
|
||||
import { FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbLedgerPage {
|
||||
@@ -217,6 +218,8 @@ export struct FbLedgerPage {
|
||||
}, (e: FbLedgerEntry): string => 'le_' + e.id.toString())
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.padding(FbMetric.cardPad)
|
||||
.backgroundColor(FbColors.surface)
|
||||
|
||||
@@ -16,6 +16,7 @@ import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbNoteItem, FbNotificationItem } from '../../model/Types';
|
||||
import { FB_DEMO_NOTES, FB_DEMO_NOTIFICATIONS } from '../../model/DemoData';
|
||||
import { FbAvatar, FbDot, FbEmptyBox, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
// ============================================================
|
||||
// 笔记
|
||||
@@ -91,6 +92,8 @@ export struct FbNotesPage {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
@@ -14,6 +14,7 @@ import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbMovieItem, FbKinChip } from '../../model/Types';
|
||||
import { FB_DEMO_CINEMAS, FB_DEMO_MOVIES } from '../../model/DemoData';
|
||||
import { FbDot, FbEmptyBox, FbIconButton, FbPersonChip, FbPrimaryButton, FbRating, FbSectionTitle } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbMoviesPage {
|
||||
@@ -123,6 +124,8 @@ export struct FbMoviesPage {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
@@ -17,6 +17,7 @@ import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbGroup, FbKinRelation, FbRosterItem } from '../../model/Types';
|
||||
import { FB_DEMO_KIN_RELATIONS, FB_DEMO_ROSTER, fbGroupRoster } from '../../model/DemoData';
|
||||
import { FbAvatar, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
/** 家谱里一个人一圈的分组 */
|
||||
interface FbKinRing {
|
||||
@@ -70,6 +71,8 @@ export struct FbPeoplePage {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
@@ -13,6 +13,7 @@ import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbGroup, FbResourceItem } from '../../model/Types';
|
||||
import { FB_DEMO_RESOURCES, fbGroupResource } from '../../model/DemoData';
|
||||
import { FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbResourcesPage {
|
||||
@@ -100,6 +101,8 @@ export struct FbResourcesPage {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
@@ -14,6 +14,7 @@ import { FbColors, FbMetric, fbPalette } from '../../common/Theme';
|
||||
import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbModuleDef, FB_MODULES, FB_PRESET_COLORS } from '../../model/Modules';
|
||||
import { FbAvatar, FbDot, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbSettingsPage {
|
||||
@@ -153,6 +154,8 @@ export struct FbSettingsPage {
|
||||
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12, bottom: 28 })
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
@@ -14,6 +14,7 @@ import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbGroup, FbTaskItem } from '../../model/Types';
|
||||
import { FB_DEMO_TASK_BOARDS, FB_DEMO_TASKS, fbGroupTask } from '../../model/DemoData';
|
||||
import { FbCheckCircle, FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbTasksPage {
|
||||
@@ -104,6 +105,8 @@ export struct FbTasksPage {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
@@ -17,6 +17,7 @@ import { FbAppState, fbAppState } from '../../common/Store';
|
||||
import { FbTripItem, FbTripNode } from '../../model/Types';
|
||||
import { FB_DEMO_TRIPS, FB_DEMO_TRIP_NODES, fbMoney } from '../../model/DemoData';
|
||||
import { FbDot, FbEmptyBox, FbIconButton, FbPrimaryButton, FbTag } from '../../components/Widgets';
|
||||
import { FB_DOCK_RESERVE } from '../../components/FloatDock';
|
||||
|
||||
@ComponentV2
|
||||
export struct FbTripsPage {
|
||||
@@ -85,6 +86,8 @@ export struct FbTripsPage {
|
||||
}
|
||||
}
|
||||
.width('100%')
|
||||
// 底部悬浮快捷栏的预留:它浮在内容之上,不留白会压住最后一屏
|
||||
.padding({ bottom: FB_DOCK_RESERVE })
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
Reference in New Issue
Block a user