跨端: P5 悬浮玻璃导航取代系统 TabBar —— 自绘浮动条 + 命中区 ≥44vp + 内容让位
(subject 原为「跨端(P5): …」—— 被自己的 commit-hygiene 判据判红:约定是 subject 里带 `跨端:`,而 `跨端(P5):` 让字面 grep 找不到。判据是对的,改提交不改成判据。)
This commit is contained in:
72
client/harmony/entry/src/main/ets/model/NavItems.ts
Normal file
72
client/harmony/entry/src/main/ets/model/NavItems.ts
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 底部导航(P5:悬浮玻璃条)的**清单与尺寸**。
|
||||
*
|
||||
* 为什么数据放在 `.ts` 而不是写在 `MainPage.ets` 里:
|
||||
* - 判据可以直接 `import` 它(`node --experimental-strip-types` 能跑纯逻辑 `.ts`),
|
||||
* 于是"命中区 ≥44vp"这类要求判的是**数值本身**,而不是拿正则去源码里猜;
|
||||
* - 「通信 → 联系人」的顺序与文案是信息架构,和 CommTabs 一样属于跨端要对齐的东西
|
||||
* (WebUI 侧同样是两项,撤掉会话 tab 的时机见计划文档 §7.15)。
|
||||
*/
|
||||
|
||||
/** 一个底部导航项。字段与 WebUI `Sidebar.tsx` 的项一一对应 */
|
||||
export interface NavItem {
|
||||
/** 稳定键(ForEach 的 key,也是判据认的名字) */
|
||||
key: string;
|
||||
/** 显示文案(与 WebUI 逐字一致) */
|
||||
label: string;
|
||||
/** 图标字形(鸿蒙侧用文本字形,不用猜 sys.media 名字) */
|
||||
icon: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平级导航项。**只有两项**:通信(内部三栏:收件箱/发件箱/授权)与联系人。
|
||||
*
|
||||
* 「会话」原先是个平级 tab,已撤 —— 它不是第三个地方,而是"同一批数据的另一种看法"
|
||||
* (收件箱按会话折叠、联系人卡片视图就是会话的进度视角)。理由与时机见 `MainPage.build()` 的注释。
|
||||
* 「日历」等 P6 的内容做完再上入口 —— 不留点进去空着的页签(§7.15)。
|
||||
*/
|
||||
export const NAV_ITEMS: NavItem[] = [
|
||||
{ key: 'comm', label: '通信', icon: '✉️' },
|
||||
{ key: 'contacts', label: '联系人', icon: '👤' }
|
||||
];
|
||||
|
||||
/** 命中区下限(vp)。**44 是可点区域的下限**,不是"看起来够大"的估计 */
|
||||
export const NAV_ITEM_MIN_HIT: number = 44;
|
||||
|
||||
/** 浮动条自身高度(vp)。≥ NAV_ITEM_MIN_HIT,否则命中区内边距会互相挤压 */
|
||||
export const NAV_BAR_HEIGHT: number = 56;
|
||||
|
||||
/** 悬浮:左右留白(vp)—— 贴边就不是"悬浮"了,WebUI 的底栏同样浮在内容之上 */
|
||||
export const NAV_BAR_SIDE: number = 16;
|
||||
|
||||
/** 悬浮:离屏幕底部的留白(vp) */
|
||||
export const NAV_BAR_BOTTOM: number = 12;
|
||||
|
||||
/** 圆角(vp)。用"胶囊"半径(= 高度一半),与 WebUI 的 `rounded-2xl` 观感一致 */
|
||||
export const NAV_BAR_RADIUS: number = 28;
|
||||
|
||||
/**
|
||||
* 内容底部要让出的高度(vp):条高 + 离底留白 + 一点余量。
|
||||
*
|
||||
* **这是"点得到"的问题,不是美观问题**:条是浮在内容之上的,不让出这段高度,
|
||||
* 列表最后一行就永远压在玻璃条底下 —— 看得见、点不到。
|
||||
*/
|
||||
export const NAV_CONTENT_RESERVE: number = NAV_BAR_HEIGHT + NAV_BAR_BOTTOM + 8;
|
||||
|
||||
/** 把任意下标归一化到合法范围(点击/外部传值都过这里,别直接赋值) */
|
||||
export function normalizeNavIndex(index: number): number {
|
||||
if (index >= 0 && index < NAV_ITEMS.length) {
|
||||
return index;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** 第 index 项的键(内容按它挂载,判据也认它) */
|
||||
export function navKeyAt(index: number): string {
|
||||
return NAV_ITEMS[normalizeNavIndex(index)].key;
|
||||
}
|
||||
|
||||
/** 第 index 项的标签(给判据与无障碍文案用) */
|
||||
export function navLabelAt(index: number): string {
|
||||
return NAV_ITEMS[normalizeNavIndex(index)].label;
|
||||
}
|
||||
@ -48,6 +48,17 @@ import {
|
||||
emptyHint
|
||||
} from '../model/CommTabs';
|
||||
import { MailDetailParams, ComposeParams } from '../model/RouteParams';
|
||||
import {
|
||||
NAV_BAR_BOTTOM,
|
||||
NAV_BAR_HEIGHT,
|
||||
NAV_BAR_RADIUS,
|
||||
NAV_BAR_SIDE,
|
||||
NAV_CONTENT_RESERVE,
|
||||
NAV_ITEM_MIN_HIT,
|
||||
NAV_ITEMS,
|
||||
NavItem,
|
||||
normalizeNavIndex
|
||||
} from '../model/NavItems';
|
||||
|
||||
/** 一页取多少封。取满了就要如实提示"可能还有更多"(服务端 total 是未读数,不是总封数)。 */
|
||||
const INBOX_PAGE_SIZE: number = 50;
|
||||
@ -1674,15 +1685,52 @@ struct MainPage {
|
||||
}
|
||||
|
||||
@Builder
|
||||
TabBarBuilder(title: string, icon: string, index: number) {
|
||||
NavItem(item: NavItem, index: number) {
|
||||
Column() {
|
||||
Text(icon).fontSize(20)
|
||||
Text(title)
|
||||
Text(item.icon).fontSize(20)
|
||||
Text(item.label)
|
||||
.fontSize(Theme.fontTiny)
|
||||
.fontColor(this.currentIndex === index ? Theme.navFgActive : Theme.navFg)
|
||||
}
|
||||
.width('100%')
|
||||
/*
|
||||
* 命中区:**显式给下限**(44vp),不靠"看起来够大"。
|
||||
* 每个项 `layoutWeight(1)` 分到条宽的一半,高度就是条高 —— 两处都远大于 44,
|
||||
* 但仍把下限写出来:以后有人把条调矮时,这里会挡住(判据也钉这个常量)。
|
||||
*/
|
||||
.layoutWeight(1)
|
||||
.height(NAV_BAR_HEIGHT)
|
||||
.constraintSize({ minHeight: NAV_ITEM_MIN_HIT, minWidth: NAV_ITEM_MIN_HIT })
|
||||
.justifyContent(FlexAlign.Center)
|
||||
// 点击:过 normalizeNavIndex 归一化(与内部页签同一套纪律:不直接赋值)
|
||||
.onClick(() => {
|
||||
this.currentIndex = normalizeNavIndex(index);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 底部导航:**自绘的悬浮玻璃条**(P5),取代系统 `Tabs` 的 bar。
|
||||
*
|
||||
* 为什么换成自绘的:
|
||||
* - 系统 `Tabs` 的 bar 只能在 `barPosition` 上下两侧、贴边、按系统规矩排布,
|
||||
* 摆不出 WebUI 那种"浮在内容之上、四周留白、圆角胶囊"的形状;
|
||||
* - 内容仍按 `currentIndex` 挂载(状态机是同一个),所以这次换的是**条**,不是信息架构。
|
||||
*
|
||||
* 玻璃**只在这一层**,而且这次是"**第二处**允许的模糊":
|
||||
* 壁纸层整个不吃材质(同一张底只许糊一次),而这条背后是**会滚动的内容** ——
|
||||
* 满足 pi 给的放行条件(§7.16),所以它在 `GLASS_REGISTRY` 里登记了并写了理由。
|
||||
*/
|
||||
@Builder
|
||||
NavBar() {
|
||||
Row() {
|
||||
ForEach(NAV_ITEMS, (item: NavItem, index: number) => {
|
||||
this.NavItem(item, index)
|
||||
}, (item: NavItem) => item.key)
|
||||
}
|
||||
.width('100%')
|
||||
.height(NAV_BAR_HEIGHT)
|
||||
// 悬浮:四周留白(贴边就不是悬浮了)+ 胶囊圆角
|
||||
.margin({ left: NAV_BAR_SIDE, right: NAV_BAR_SIDE, bottom: NAV_BAR_BOTTOM })
|
||||
.borderRadius(NAV_BAR_RADIUS)
|
||||
/*
|
||||
* 导航底:**系统材质**,不是手写 alpha。
|
||||
*
|
||||
@ -1690,8 +1738,6 @@ struct MainPage {
|
||||
* 那等于"我们替系统猜了深色该怎么做",与"用系统方案"直接冲突,
|
||||
* 而且还要我们自己维护两套。现在只声明**档次**(`Theme.navMaterial`),
|
||||
* 深浅两套颜色与模糊半径都由系统按主题给。
|
||||
*
|
||||
* 玻璃**只出现在这一层**(内容面不透)—— 嵌套各加一层模糊是 pi 点名要避免的。
|
||||
*/
|
||||
.backgroundBlurStyle(Theme.navMaterial)
|
||||
}
|
||||
@ -1717,25 +1763,27 @@ struct MainPage {
|
||||
* (status 与 from_agent 参考实现也不显示,见 `WorkCard.tsx` 的字段集,
|
||||
* 判据里有一条"卡片字段两边一致"钉住这件事,避免以后以为漏了。)
|
||||
*/
|
||||
Stack() {
|
||||
Stack({ alignContent: Alignment.Bottom }) {
|
||||
// 背景在最底下:内容面(系统色)盖在它上面
|
||||
this.WallpaperLayer()
|
||||
Tabs({ barPosition: BarPosition.End }) {
|
||||
TabContent() {
|
||||
CommPage({ bgActive: this.bgActive })
|
||||
/*
|
||||
* 内容按 `currentIndex` 挂载(原来这里是系统 `Tabs` 的 TabContent)。
|
||||
* 底部**必须让出** NAV_CONTENT_RESERVE 的高度:条是浮在内容之上的,
|
||||
* 不让出这一段,列表最后一行就永远压在玻璃条底下 —— 看得见、点不到。
|
||||
*/
|
||||
Column() {
|
||||
if (this.currentIndex === 0) {
|
||||
CommPage({ bgActive: this.bgActive })
|
||||
} else {
|
||||
ContactsTab({ bgActive: this.bgActive })
|
||||
}
|
||||
}
|
||||
.tabBar(this.TabBarBuilder('通信', '✉️', 0))
|
||||
|
||||
TabContent() {
|
||||
ContactsTab({ bgActive: this.bgActive })
|
||||
}
|
||||
.tabBar(this.TabBarBuilder('联系人', '👤', 1))
|
||||
}
|
||||
.onChange((index: number) => {
|
||||
this.currentIndex = index;
|
||||
})
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.padding({ bottom: NAV_CONTENT_RESERVE })
|
||||
|
||||
// 浮动玻璃条浮在最上层(自绘,见 NavBar 的说明)
|
||||
this.NavBar()
|
||||
}
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
|
||||
Reference in New Issue
Block a user