v60b:所有弹框统一主题色描边环 + 编辑改居中弹框 + 修名册下拉回弹吃掉悬浮栏
1)9 个浮层统一描边环(抽组件,不复制 8 遍)
新增 components/FbSheetRing.ets:环宽 FB_RING_W=2 全工程唯一一处;
实现是"外层 Column 铺色 + padding",不是 border(border 画在盒子内侧);
外圆角 = 内圆角 + 环宽。简要卡/详情/编辑/日程详情/日程编辑/观影详情/影院/
日程设置/模块设置全部改用;各浮层的环色 = 所属模块的主题色。
2)编辑从"贴底满宽 + 只有上圆角"改成居中弹框
人员编辑与日程编辑:Stack 对齐改 Center,卡片 92% 宽 + 四角圆 + 环;
constraintSize 留在卡片自己身上(挪到外层会让内层 Scroll().layoutWeight
拿到 0 高度,资料区塌成一条线)。日程的日期时间选择弹层、两个删除确认框
一并套环,避免同一次交互里出现两种形态。
3)修名册下拉到头回弹后悬浮按钮消失
EdgeEffect.Spring 在越界松手后把内容弹回,每帧 offset>0 被当成"用户在往上滑"
⇒ 收起悬浮栏。判据 = 手指已抬起 + 最后一次方向是下滑 + 抬手不到 500ms;
拦截必须排在 offset >= HIDE_AT 快路径之前(回弹第一帧位移常 >= 12vp)。
4)真机截图暴露的两个观感问题(静态检查全绿也照错)
- Scroll().layoutWeight(1) 让短内容也撑满 maxHeight ⇒ 卡片空半屏
(日程详情/模块设置),改为 flexShrink(1);内容本来就长的浮层不动。
- 人员编辑"虚拟人物(还没弄清真实身份)"被卡片裁掉半句:两个小开关从
并排 Row 改为 Flex(wrap) 换行,间距改走 margin。
验证:ArkTS 守卫 444/0;反向验证 17/17;真机 _fb_v60b_verify.py 11/0,
且关掉回弹拦截重编重装后该用例确实变红(证明测试不是假绿);
9 个浮层逐个真机截图确认。
This commit is contained in:
@@ -49,6 +49,59 @@ const HIDE_AT: number = 12;
|
||||
*/
|
||||
const SHOW_AT: number = 8;
|
||||
|
||||
/**
|
||||
* 🔴 2026-10-19 用户:「名册下拉的时候,拉到头,它会**回弹一下**,
|
||||
* 导致下面的悬浮按钮**又消失了**。」
|
||||
*
|
||||
* 真因(`EdgeEffect.Spring` 的必然结果):
|
||||
* · 手指往下拉(内容往下走)⇒ 每帧 `offset < 0` ⇒ 我们**正确地唤回**悬浮栏;
|
||||
* · 拉到顶之后继续拉 = **越界**,内容停在原位、手指还在动;
|
||||
* · **松手** ⇒ 系统弹簧把内容**弹回去**(内容往上走)⇒ 每帧 `offset > 0`
|
||||
* ⇒ 被当成"用户在往上滑" ⇒ **收起悬浮栏**。
|
||||
* 用户看到的就是"按钮闪没"。
|
||||
*
|
||||
* 判据(不用猜,用**手指方向 + 时间**):
|
||||
* · 回弹一定发生在**抬手之后**、且紧跟着一次**下滑**;
|
||||
* · 真正的"往上滑"要么手指还在屏幕上(`touchStartY >= 0`),
|
||||
* 要么上一次手指方向就是往上(`lastDir === 1`)。
|
||||
* ⇒ 只有"手指已抬起 + 上一次是下滑 + 刚抬手不到 `SPRING_MS`"才拦。
|
||||
*
|
||||
* ⚠️ 为什么用 500ms:本机弹簧动画(Spring)在真机上约 300–450ms 结束,
|
||||
* 取 500 能把整段回弹盖住;再长就会误伤"下拉之后马上接着上滑"。
|
||||
*/
|
||||
const SPRING_MS: number = 500;
|
||||
|
||||
/**
|
||||
* 手指按下位置(**-1 = 当前没有按压**;用哨兵值而不是 null,ArkTS 对可选值更严格)。
|
||||
*
|
||||
* ⚠️ 提到文件上方(原来是放在触摸段里的):`fbOnScroll` 也要读它判断
|
||||
* "现在是指针在拖、还是抬手后的惯性/回弹"。
|
||||
*/
|
||||
let touchStartY: number = -1;
|
||||
|
||||
/** 一次滑动至少要移动这么多(vp)才认作"有方向意图"(避免把点击当滑动) */
|
||||
const TOUCH_MIN: number = 16;
|
||||
|
||||
/** 手指**最近一次**明确方向:1 = 往上滑(内容上移),-1 = 往下滑,0 = 还没动 */
|
||||
let lastDir: number = 0;
|
||||
|
||||
/** 最近一次抬手的时间戳(0 = 还没抬过手) */
|
||||
let lastUpAt: number = 0;
|
||||
|
||||
/**
|
||||
* 现在收到的"正向位移"(内容往上走)是不是**弹簧回弹**。
|
||||
*
|
||||
* ⚠️ 只在 `fbOnScroll` 里用;判定条件见 `SPRING_MS` 上方的说明。
|
||||
*/
|
||||
function isSpringBack(): boolean {
|
||||
// 手指还在屏幕上 ⇒ 用户正在拖,尊重他的方向
|
||||
if (touchStartY >= 0) {
|
||||
return false;
|
||||
}
|
||||
// 刚刚是"往下拉",且才抬手不久 ⇒ 这一串正向位移是弹簧把内容弹回去
|
||||
return lastDir === -1 && lastUpAt > 0 && (Date.now() - lastUpAt) < SPRING_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 横向滚动容器上报(挂在**横向** Scroll 上的 `.onDidScroll`)。
|
||||
*
|
||||
@@ -94,6 +147,13 @@ export function fbHScrollBusy(withinMs: number = 350): boolean {
|
||||
export function fbOnScroll(offset: number, state: ScrollState): void {
|
||||
const app = fbAppState();
|
||||
|
||||
// 🔴 先拦"回弹":见 `SPRING_MS` 的说明。
|
||||
// ⚠️ 必须放在**最前面**(包括快路径之前):
|
||||
// 回弹的第一帧位移往往就 ≥ `HIDE_AT`,放到后面就来不及了。
|
||||
if (offset > 0 && isSpringBack()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ⚠️ **不跳过 Idle**:`Fling`(惯性)结束时最后那一下会带 Idle,
|
||||
// 而内容短的页面可能**只在惯性阶段**产生位移 —— 跳过就永远收不起来。
|
||||
// 但 Idle 的 offset 偶尔是 0(无方向含义),所以下面按 offset 的正负判断,
|
||||
@@ -140,12 +200,8 @@ export function fbOnScroll(offset: number, state: ScrollState): void {
|
||||
// ============================================================
|
||||
// 触摸方向兜底(内容不足一屏的页面用)
|
||||
// ============================================================
|
||||
|
||||
/** 手指按下的位置(**-1 = 当前没有按压**;用哨兵值而不是 null,ArkTS 对可选值更严格) */
|
||||
let touchStartY: number = -1;
|
||||
|
||||
/** 一次滑动至少要移动这么多(vp)才认作"有方向意图"(避免把点击当滑动) */
|
||||
const TOUCH_MIN: number = 16;
|
||||
// ⚠️ `touchStartY` / `TOUCH_MIN` 已提到文件上方 —— `fbOnScroll` 也要读它们
|
||||
// (判断"手指在拖"还是"抬手后的回弹")。这里不再重复声明。
|
||||
|
||||
/**
|
||||
* 触摸方向兜底:在**页面根容器**挂 `.onTouch(fbOnTouch)`。
|
||||
@@ -180,6 +236,11 @@ export function fbOnTouch(event: TouchEvent): void {
|
||||
if (arr.length > 0) {
|
||||
touchStartY = arr[0].windowY;
|
||||
}
|
||||
// 🔴 新一轮手势开始 ⇒ 清掉上一轮的方向残留。
|
||||
// ⚠️ 不清的后果:上一轮是"往下拉"(`lastDir = -1`),这一轮只点了一下
|
||||
// (位移 < `TOUCH_MIN`,不更新方向)就抬手 ⇒ 抬手的瞬间窗口又开了
|
||||
// ⇒ 系统惯性里任何正向位移都被误判成回弹、悬浮栏该收却不收。
|
||||
lastDir = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -194,12 +255,15 @@ export function fbOnTouch(event: TouchEvent): void {
|
||||
const dy: number = arr[0].windowY - touchStartY;
|
||||
// dy < 0 ⇒ 手指往上抹 ⇒ 内容往上走 ⇒ 收起
|
||||
if (dy <= -TOUCH_MIN) {
|
||||
// 记下方向:抬手后若出现"相反方向的内容位移"(弹簧回弹),用它来识别
|
||||
lastDir = 1;
|
||||
if (!app.dockHidden) {
|
||||
app.dockHidden = true;
|
||||
}
|
||||
// ⚠️ **不更新** touchStartY:否则手指继续上抹时会一直在阈值边缘反复触发。
|
||||
// 保持不动 ⇒ 判断是"相对起点的总位移",更稳。
|
||||
} else if (dy >= TOUCH_MIN) {
|
||||
lastDir = -1;
|
||||
if (app.dockHidden) {
|
||||
app.dockHidden = false;
|
||||
}
|
||||
@@ -209,5 +273,8 @@ export function fbOnTouch(event: TouchEvent): void {
|
||||
|
||||
if (t === TouchType.Up || t === TouchType.Cancel) {
|
||||
touchStartY = -1;
|
||||
// 🔴 抬手时刻:之后的 500ms 内,与"最后一次手指方向"相反的滚动
|
||||
// 会被当作弹簧回弹忽略(见 `isSpringBack`)。
|
||||
lastUpAt = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user