跨端: 上下黑边真的消了 —— 全屏 + 避让是"同一套东西的两半",上次只删了一半
用户第三次报同一条:「你再看看页面底部,那么大的黑色,你看从头到尾都没
修好,你能不能好好看看我给你的示例工程怎么处理上下黑边的」。
## 根因:上一次把"两半"当成了"一件事",删掉一半就以为修好了
`6861934` 的注释白纸黑字写着「★ **刻意不用** `setWindowLayoutFullScreen(true)`」,
理由是「实测过:它确实也消掉黑带,但会连状态栏区域一起吃进布局,于是页签栏
被时钟/电量盖住(截图硬证「07:43」与「收件箱」重叠)」。
那次实测**是真的**,结论**下错了**:被盖住不是"不该全屏",而是
**只做了全屏、没做避让**。示例工程里这两件事本来就是**同一套东西的两半**:
common/.../util/WindowUtil.ets → setWindowLayoutFullScreen(true)
+ getWindowAvoidArea(TYPE_SYSTEM /
TYPE_NAVIGATION_INDICATOR)
features/mine/.../view/MineView.ets:251 → .margin({ top: statusBarHeight + …,
bottom: naviIndicatorHeight })
只做前半 ⇒ 内容跑到状态栏底下没人让(那次退回的原因);
只做后半 ⇒ 黑边照旧(这三次报修的原因)。退回的代价是**黑边留了三天**。
## 实测(模拟器 1256x2760,四页一致)
修前:顶部纯黑 136px、底部纯黑 60px + 手势条 20px
修后:四页**纯黑段均为 0**;y=0..135 是壁纸(时钟浮在上面,正是示例工程的效果)
y=2662+ 壁纸铺到底、底栏浮在手势区之上
## 改了什么
- `entryability/EntryAbility.ets`:拆出 `setupFullScreenWindow()`,
在 `loadContent` **回调里**调(与示例工程同一位置 —— `px2vp` 要用 `getUIContext()`,
那要有已加载内容才拿得到)。全屏 + 读两个避让区 + 订 `avoidAreaChange`。
`setWindowSystemBarEnable(['status'])` 保留(状态栏要看得见),但**不再靠它**消黑边。
- `model/WindowInsets.ts`(新):纯逻辑 `insetsFromAvoidArea()`,不 import SDK ——
判据才能在 node 里直接喂样本验换算。形参叫 `toVp` 而**不是** `px2vp`:
后者是 SDK 已废弃的全局函数名,`harmony-system-api` 按名字扫,同名形参会误报。
- `pages/MainPage.ets`:`@StorageLink(KEY_WINDOW_INSETS)` 订阅;状态栏高度当
**内容层**的 `padding-top`(**不是**根 Stack —— 壁纸必须铺到屏幕四边,根上加
padding 会把壁纸一起缩进去,黑边只是换个地方出现);底栏与内容末尾让开手势条。
reserve 收成**一个** `recomputeNavReserve()`,宽度变化与避让变化两个触发点共用
(转屏只改避让不改宽度,各写一份迟早漏一个)。
## 判据:`test/harmony-window.test.mjs`(8 条)
钉的是"两半必须同时存在"——**只钉一半的话,"退回某一半"照样能全绿通过**,
而那次退回恰恰就是删了一半。纯逻辑 3 条(换算/取不到就是 0 不猜/键名是常量)
+ 接线 4 条(全屏在、避让在、布局真消费了值、纯逻辑模块不许 import SDK)
+ 设备行为 1 条(全屏没把应用搞成白屏)。
**变异自检两个方向都跑过**(这是本轮最该记的一步):
- 删掉 `setWindowLayoutFullScreen`(重演 6861934)⇒ 2 条红 ✓
- 删掉 `getWindowAvoidArea`(只全屏不让位)⇒ 1 条红 ✓
★ 第一版判据**锚错了**:正则直接扫全文,而注释里正好有 `setWindowLayoutFullScreen(true)`
这个串 —— 把真正的调用删掉后判据**仍然全绿**。锚落在"代码对自己的描述"上了。
加 `stripped()` 剥注释后,变异才咬得住。这条与仓里那条"判据的锚不能落在
被守对象的自述上"是同一件事,这次是它的实例。
## 顺带修的两条既有判据(不是放宽,是它们把"当时的字符串"当成了"要守的坑")
- `harmony-nav` ④:留白断言写死了 `bottom: NAV_BAR_BOTTOM` 那个字面串。
它本来要守的是"留白靠 padding 不靠 margin"(margin 在 ArkUI 里加在宽度外面,
100% + margin 会顶出父容器)—— 那是另一件事。改成剥注释后验三段在不在、
离底是否**从** `NAV_BAR_BOTTOM` **起**。变异(padding→margin)仍判红 ✓
- `harmony-widescreen` ④:同上,写死了 `? 0 : NAV_CONTENT_RESERVE`。
改成"宽屏必为 0、窄屏含 NAV_CONTENT_RESERVE(可再加避让)"。
两处都是"加一个正当的避让"与"真犯那个错"会红得一模一样 —— 那就不再是守坑,
是守字符串。
另:`align-refs` / `build-stamp` / `packaging` 三条 broken 是前端 `99a2d7a`
(另一个人改的 `CalendarView.tsx`)带来的,与本轮无关,留给他。
This commit is contained in:
122
client/harmony/entry/src/main/ets/model/WindowInsets.ts
Normal file
122
client/harmony/entry/src/main/ets/model/WindowInsets.ts
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 窗口避让区(safe area / avoid area)—— 全屏布局的另一半。
|
||||
*
|
||||
* ── 为什么需要这个文件(2026-09-18 用户第三次报同一条)──
|
||||
*
|
||||
* 用户原话:「你再看看页面底部,那么大的黑色,你看从头到尾都没修好,
|
||||
* 你能不能好好看看我给你的示例工程怎么处理上下黑边的」。
|
||||
*
|
||||
* 实测(模拟器 1256x2760 @3.5 ⇒ 359vp 宽)四页一致:
|
||||
* · 顶部纯黑 y=0..135 (136px ≈ 39vp,状态栏)
|
||||
* · 底部纯黑 y=2662..2721 (60px ≈ 17vp)
|
||||
* · 底部纯黑 y=2740..2759 (20px ≈ 6vp,手势条)
|
||||
* 中间 y=2724..2736 是 rgb(34,34,34) —— 系统导航栏本身,不是我们的内容。
|
||||
*
|
||||
* ── 前一次为什么没修好(这是关键,别再退回)──
|
||||
*
|
||||
* `EntryAbility` 里原本写着:
|
||||
* 「★ 刻意不用 setWindowLayoutFullScreen(true)。实测过:它确实也消掉黑带,
|
||||
* 但会连状态栏区域一起吃进布局,页签栏被时钟/电量盖住(截图硬证「07:43」
|
||||
* 与「收件箱」重叠)。」
|
||||
*
|
||||
* 那段经历是真的,**但结论下错了**:被盖住不是"不该全屏",而是"只做了全屏、
|
||||
* 没做避让"。示例工程(`/tmp/harmonyos-samples-reference`)的做法是**两件事一起**:
|
||||
*
|
||||
* ① `setWindowLayoutFullScreen(true)` 窗口铺满(内容延伸到状态栏/导航条底下)
|
||||
* ② `getWindowAvoidArea(TYPE_SYSTEM /
|
||||
* TYPE_NAVIGATION_INDICATOR)` 读出上下被系统遮住的高度
|
||||
* ③ 布局里 `top: statusBarHeight` /
|
||||
* `bottom: naviIndicatorHeight` 把这高度当内边距用
|
||||
*
|
||||
* 只做 ① ⇒ 内容跑到状态栏底下且**没人往后让** ⇒ 被时钟盖住(正是当时看到的现象)。
|
||||
* 于是那次把 ① 整个退回了 —— 连带把"黑边消失"也退掉了,因为 ① 才是消黑边的那个动作。
|
||||
*
|
||||
* 示例工程里这套在 `common/src/main/ets/util/WindowUtil.ets`(`registerBreakpoint`
|
||||
* 监听 `avoidAreaChange`)与 `features/mine/.../MineView.ets:251`(消费端):
|
||||
* `.margin({ top: statusBarHeight + NAVIGATION_HEIGHT, bottom: naviIndicatorHeight })`
|
||||
*
|
||||
* ── 本工程的做法 ──
|
||||
*
|
||||
* 与示例工程同构,但不引入它的 `GlobalInfoModel`/`AppStorage` 那一套:
|
||||
* 避让高度是**窗口级**的量,直接放 `@StorageLink` 让页面读(页面切换/横竖屏都跟着变)。
|
||||
* 「读」在 `EntryAbility.onWindowStageCreate` 里做(`WindowUtil.initialize` 的位置),
|
||||
* 「用」在 `MainPage` 的根容器(唯一根,改动面最小)。
|
||||
*
|
||||
* ★ 与"隐藏系统导航条"(`setWindowSystemBarEnable(['status'])`)的关系:
|
||||
* 那个调用**留不住** —— 实测它没能消掉底部黑边(60px + 20px 都还在)。
|
||||
* 而且语义上它是在跟系统讨"请别画导航条",而全屏 + 避让是**我们自己在内容里让位**,
|
||||
* 不依赖系统肯不肯隐藏。所以现在以全屏 + 避让为准,那条调用留着(它对状态栏仍有效,
|
||||
* 且退回旧行为时不会更糟),但它不再是消黑边的手段。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 一条边被系统遮住的高度(vp)。
|
||||
*
|
||||
* 用 class 而不是 interface:判据里要能 `new Insets()` 拿默认值,
|
||||
* 且 ArkTS 对"对象字面量必须能对应到具名类型"要求严格,
|
||||
* class 带默认字段写起来最省事(示例工程也是 class + AppStorage)。
|
||||
*/
|
||||
export class Insets {
|
||||
/** 状态栏高度(vp)。全屏后顶部这段是被时钟/电量占住的,内容要从它下面开始 */
|
||||
statusBar: number = 0;
|
||||
/** 底部手势条/导航条高度(vp)。全屏后这段要让出来,否则最后一行压在系统手势区 */
|
||||
navIndicator: number = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读数用的键名。
|
||||
*
|
||||
* 抽成常量而不是各处写字面量:`@StorageLink('xxx')` 拼错**不会报错**,
|
||||
* 只会恒等于默认值(表现为"避让永远是 0,黑边照旧")—— 那正是这次要修的病症,
|
||||
* 不能用一个拼错了就不声不响的机制来修它。判据钉这个常量。
|
||||
*/
|
||||
export const KEY_WINDOW_INSETS: string = 'agentmail.window.insets';
|
||||
|
||||
/**
|
||||
* 把 SDK 给的 `window.AvoidArea` 转成 vp 的 `Insets`。
|
||||
*
|
||||
* 为什么单独一个纯函数:换算要用 `UIContext.px2vp`(全局 `px2vp` 已废弃,
|
||||
* 判据 `harmony-system-api` 对全局调用默认判红),而 `UIContext` 要传进来 ——
|
||||
* 这样这个函数**不依赖窗口对象**,可以在判据里直接喂样本验。
|
||||
*
|
||||
* ★ 形参叫 `toVp` 而**不叫** `px2vp`:后者是 SDK 已废弃的**全局函数名**,
|
||||
* 而 `harmony-system-api` 那条判据是按名字扫源码的(它认的是"调了废弃的全局函数")——
|
||||
* 一个同名形参会被它读成调用。名字改成 `toVp` 后,语义一样而扫描不再误报。
|
||||
* (实测:我第一版就叫 `px2vp`,那条判据直接判红 `WindowInsets.ts: px2vp`。)
|
||||
*
|
||||
* 形参用**窄接口**而不是 import SDK 类型:判据用 `--experimental-strip-types` 跑纯 `.ts`,
|
||||
* **没有** ArkUI 运行时可以 import(`@kit.ArkUI` 在 node 里不存在)⇒ 若这里 import,
|
||||
* 判据就连编都编不过。这是本仓反复用的办法:纯逻辑与 SDK 隔离,SDK 那侧只留薄薄一层接线。
|
||||
*/
|
||||
export function insetsFromAvoidArea(
|
||||
system: AvoidAreaLike | undefined,
|
||||
navIndicator: AvoidAreaLike | undefined,
|
||||
toVp: (px: number) => number
|
||||
): Insets {
|
||||
const out: Insets = new Insets();
|
||||
/*
|
||||
* 取不到就留 0 —— **不是**"猜一个 39vp"。0 的表现是"黑边照旧"(与修之前一样),
|
||||
* 猜一个的表现是"按猜的值让位、错得看不出来"。两害相权取能暴露的那个。
|
||||
*/
|
||||
const sysTop: number = system?.topRect?.height ?? 0;
|
||||
const navBottom: number = navIndicator?.bottomRect?.height ?? 0;
|
||||
out.statusBar = sysTop > 0 ? toVp(sysTop) : 0;
|
||||
out.navIndicator = navBottom > 0 ? toVp(navBottom) : 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* `window.AvoidArea` 里我们真正要用的那部分形状。
|
||||
*
|
||||
* 只声明用到的字段(`topRect`/`bottomRect` 的 `height`),不搬整个 SDK 类型 ——
|
||||
* 判据要构造样本喂进 `insetsFromAvoidArea`,形状越窄越好构造、也越不容易把
|
||||
* "我们其实没读的字段"写成契约。
|
||||
*/
|
||||
export interface InsetsRect {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface AvoidAreaLike {
|
||||
topRect?: InsetsRect;
|
||||
bottomRect?: InsetsRect;
|
||||
}
|
||||
Reference in New Issue
Block a user