From fc3fff4f36b5060cdc8c53b1bda53c3bf2fe64fd Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 17 Sep 2026 16:43:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(harmony):=20=E6=BB=9A=E5=8A=A8=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E4=B8=8A=E4=B8=8B=E8=BE=B9=E7=BC=98=E6=B8=90=E9=9A=90?= =?UTF-8?q?=EF=BC=8C=E6=9B=BF=E6=8E=89=E7=A1=AC=E6=88=AA=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户(2026-09-17):「上下还是硬截断,不是 webui 那种渐变」。 WebUI 的做法是 `index.css` 给 `.overflow-y-auto` 加 `mask-image: linear-gradient(to bottom, transparent 0, #000 min(12px,10%), ...)`。 ArkUI 的对应物是滚动容器自带的 `fadingEdge(enabled, { fadingEdgeLength })` (API 14+,定义在 `ScrollableCommonMethod` 上 ⇒ List/Scroll/Grid/WaterFlow 都有), 它淡掉的是**渲染结果本身**,与背景无关。 **刻意不用** linearGradient + mask 手搓渐变色遮罩:那是拿背景色画一个 并不存在的"底色",在壁纸/玻璃主题下必然露馅(壁纸本身带颜色,遮罩层对不上)。 - 新增 `LIST_FADE_LENGTH = 12`(NavItems.ts):与 WebUI 的 `min(12px, 10%)` 同量级,也接近列表内边距(10vp)—— 静止时几乎看不出,滚动时才起作用。 - MainPage 的 5 个列表容器(收件箱/发件箱/授权/联系人卡片/联系人列表) 全部挂 `.fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) })`。 显式传长度而不是用默认 32vp:WebUI 那边踩过同一个坑 (「渐变用的过猛了,比如收件人候选那里」——固定像素用在高度不固定的容器上)。 判据(harmony-nav.test.mjs 新增一条):常量值、五个列表都挂、长度走 LengthMetrics.vp、禁止 linearGradient+mask 顶替;并做了变异自检 (删掉任一处 fadingEdge 必须判红,实测通过)。 实测(模拟器 1256×2760,密度 3.489): - 滚动后顶边被切断的卡片文字明显洗白(红色评分 5.5 vs 完整卡片 15.5); - 底边同样渐隐;截图 /tmp/harmony-fade-scroll.jpeg。 测试:199 passed / 0 failed。 --- client/electron/test/harmony-nav.test.mjs | 50 +++++++++++++++++++ .../entry/src/main/ets/model/NavItems.ts | 16 ++++++ .../entry/src/main/ets/pages/MainPage.ets | 8 +++ 3 files changed, 74 insertions(+) diff --git a/client/electron/test/harmony-nav.test.mjs b/client/electron/test/harmony-nav.test.mjs index 3d46e3e..8d5a05d 100644 --- a/client/electron/test/harmony-nav.test.mjs +++ b/client/electron/test/harmony-nav.test.mjs @@ -455,3 +455,53 @@ test('★ ⑤ 变异自检:nav 项退回带文字或 22 尺寸或图标不上 `Theme.navMaterial 用的档位 \`${decl[1]}\` 必须在 SDK 的 BlurStyle 枚举里` + `(成员:${members.join('、')})`); }); + +/* + * ★ 滚动列表的上下边缘渐隐(2026-09-17 用户:「上下还是硬截断,不是 webui 那种渐变」)。 + * + * WebUI 的做法是 `index.css` 里给 `.overflow-y-auto` 加 + * `mask-image: linear-gradient(to bottom, transparent 0, #000 min(12px,10%), ...)`。 + * ArkUI 里**不要**手搓一层渐变色遮罩 —— 那是拿背景色画一个并不存在的"底色", + * 在壁纸/玻璃主题下会露馅(壁纸本身带颜色,遮罩层一定对不上)。 + * 对应物是滚动容器自带的 `fadingEdge(enabled, { fadingEdgeLength })`(API 14+, + * 定义在 `ScrollableCommonMethod` 上 ⇒ List/Scroll/Grid/WaterFlow 都有), + * 它淡掉的是**渲染结果本身**,与背景无关。 + * + * 这一条钉三件事: + * ① 常量存在且是"可争论的那一个数"(派生自 WebUI 的 12px,不是随手写); + * ② 五个列表容器**都**挂上了(漏一个就是"有的地方还是硬截断"); + * ③ 用的是 `LengthMetrics.vp(...)` 而不是裸数字 —— fadingEdgeLength 是 LengthMetrics。 + */ +test('★ 滚动列表上下边缘渐隐:所有列表容器都挂 fadingEdge(对应 WebUI 的 mask-image)', () => { + const nav = read('model/NavItems.ts'); + const len = /export const LIST_FADE_LENGTH:\s*number\s*=\s*(\d+)\s*;/.exec(nav); + assert.ok(len, 'NavItems.ts 要导出 LIST_FADE_LENGTH(渐隐高度,单一可争论的数)'); + assert.equal(len[1], '12', + '渐隐高度取 12vp:与 WebUI 的 `min(12px, 10%)` 同量级、也接近列表内边距(10vp)。' + + '改这个数要有理由(它直接决定"切得有交代"的观感强度)'); + + // 五个列表容器:收件箱 / 发件箱 / 授权 / 联系人卡片 / 联系人列表 + const lists = main.match(/List\(\{[^)]*\}\)\s*\{/g) ?? []; + assert.ok(lists.length >= 5, `MainPage 里应当有 5 个列表容器,实际 ${lists.length} 个(判据前提要复核)`); + + const fades = main.match(/\.fadingEdge\(/g) ?? []; + assert.equal(fades.length, lists.length, + `每个列表容器都要挂 fadingEdge(实际 ${fades.length} 个 / ${lists.length} 个列表)。` + + '漏掉的那个就是"上下还是硬截断" —— 用户 2026-09-17 原话。'); + + // 每个 fadingEdge 都要带上显式长度,且走 LengthMetrics.vp(不是裸数字) + const withLen = main.match(/\.fadingEdge\(true,\s*\{\s*fadingEdgeLength:\s*LengthMetrics\.vp\(LIST_FADE_LENGTH\)\s*\}\)/g) ?? []; + assert.equal(withLen.length, fades.length, + `每个 fadingEdge 都要写 \`{ fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) }\`。` + + '不写长度会落回默认 32vp —— 在 58px 高的小容器里会把内容洗白' + + '(WebUI 那边踩过同一个坑:「渐变用的过猛了,比如收件人候选那里」)。'); + + // 不许用"渐变色遮罩"顶替:那是拿背景色画假底色,壁纸主题下必然露馅 + assert.ok(!/linearGradient\(\{[^}]*transparent[\s\S]{0,200}\.mask\(/.test(main), + '★ 不许用 linearGradient + mask 手搓渐隐:那是拿背景色画一个不存在的"底色",' + + '壁纸/玻璃主题下会露馅。用滚动容器原生的 fadingEdge(淡的是渲染结果本身)。'); + + // LengthMetrics 要真的 import 进来(否则编译不过 —— 但这条更早给出可读的错) + assert.match(main, /import\s*\{[^}]*LengthMetrics[^}]*\}\s*from\s*'@kit\.ArkUI'/, + 'MainPage 要从 @kit.ArkUI import LengthMetrics'); +}); diff --git a/client/harmony/entry/src/main/ets/model/NavItems.ts b/client/harmony/entry/src/main/ets/model/NavItems.ts index 4b388df..b03fd19 100644 --- a/client/harmony/entry/src/main/ets/model/NavItems.ts +++ b/client/harmony/entry/src/main/ets/model/NavItems.ts @@ -111,6 +111,22 @@ export const NAV_CONTENT_ITEMS: NavItem[] = NAV_ITEMS.slice(0, NAV_CONTENT_COUNT /** 外壳入口(第 4 项「我的」)的路由目标 —— 与 WideSidebar 的 onSettings 同一地址 */ export const NAV_SETTINGS_ROUTE: string = 'pages/SettingsPage'; +/** + * 滚动列表上下边缘的渐隐高度(vp)。 + * + * 对应 WebUI `index.css` 的 `.overflow-y-auto { mask-image: linear-gradient(...) }`: + * 滚动条本身是“一刀切”,卡片滑到边缘被硬截断;加一层上下渐隐后, + * 切断处变成渐隐(2026-09-14 用户:「内容项上下滑动会直接被切断」)。 + * + * ArkUI 的对应物是 `List.fadingEdge(true, { fadingEdgeLength })`(API 14+), + * **不要**手捧渐变色遮罩:那是拿背景色画一个不存在的“底色”, + * 在壁纸/玻璃主题下会露馅。 + * + * 取 12:与 WebUI 的 `min(12px, 10%)` 同量级,也接近列表内边距(10vp), + * 静止时几乎看不出,滚动时才起作用。 + */ +export const LIST_FADE_LENGTH: number = 12; + /** 把任意下标归一化到内容窗格的合法范围(点击/外部传值都过这里,别直接赋值) */ export function normalizeNavIndex(index: number): number { if (index >= 0 && index < NAV_CONTENT_COUNT) { diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index 5aca4e9..b84171f 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -68,7 +68,9 @@ import { MailDetailParams, ComposeParams } from '../model/RouteParams'; * 这条注释留在 **import 区**而不是跟着表走,因为要守的是"import 在最前"这个位置本身。 */ import { CalendarPage } from './CalendarPage'; +import { LengthMetrics } from '@kit.ArkUI'; import { + LIST_FADE_LENGTH, NAV_BAR_BOTTOM, NAV_BAR_HEIGHT, NAV_BAR_RADIUS, @@ -475,6 +477,8 @@ struct InboxTab { }, (g: SessionGroup) => g.key) } .width('100%').layoutWeight(1) + /* WebUI `.overflow-y-auto` 的上下渐隐(mask-image):卡片滑到边缘不硬截断 */ + .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) }) .padding({ left: 10, right: 10, top: 10, bottom: 10 }) } } @@ -846,6 +850,7 @@ struct SentTab { }, (g: SessionGroup) => g.key) } .width('100%').layoutWeight(1) + .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) }) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) } } @@ -1078,6 +1083,7 @@ struct PermissionTab { }, (req: PermissionRequest) => req.request_id) } .width('100%').layoutWeight(1) + .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) }) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) } } @@ -1358,6 +1364,7 @@ struct ContactsTab { }, (_c: Contact, idx: number) => idx.toString()) } .width('100%').layoutWeight(1) + .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) }) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) } else { // 列表视图:同样**每项一张卡**(不再用贯通分隔线)—— 与卡片视图同一语义 @@ -1373,6 +1380,7 @@ struct ContactsTab { }, (_c: Contact, idx: number) => idx.toString()) } .width('100%').layoutWeight(1) + .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(LIST_FADE_LENGTH) }) .padding({ left: 12, right: 12, top: 8, bottom: 8 }) } }