# PLAN · 悬浮球观感与空闲表现优化(**待评审 · 未实施**) > ## ⛔ 本方案已弃用(2026-10-14) > 用户明确表示「上面的方案放弃」,并改为直接要求**去掉点击球后的跳动/展开动画** —— > 那件事已单独做完(见 `README.md` §8 的「点击后先跳一下」一节与 `src/floatball.js`)。 > 本文件**仅作为当时的分析留档**,不要再照它实施;其中"球的直径 / 按钮文字 / 空闲淡化" > 三个议题仍未定,未做任何改动。 --- > 2026-10-14。起因:三个观感问题(按钮文字 / 球直径 / 空闲淡化或贴边)。 > **本文只做方案讨论,一行代码都没改**(`dist` 与设备上的产物仍一致)。 > 规格书原文取自资料库 doc `JZf9xTgLXBFgjXJ4dJWVsU` §10(本次逐条核对过,不是凭记忆)。 --- ## 0. 先摆事实:现状基线(真机实测值) 设备 Pixel 5a / 1080×1920 / **density 2**。 | 项 | 值 | 来源 | |---|---|---| | 折叠态窗口 | 88×88 dp(242×242 px) | `collapsedSizeDp()` = 热区 64 + 两侧内边距 12×2 | | 展开态窗口 | **308×264 dp(616×528 px)** | `ball-layout.txt` 实测 | | 球体 / 热区 / 呼吸环 | Ø48 / Ø64 / Ø60 dp | §10.1;环在 `buildBall()` 里硬编码 60 | | 子按钮 / 扇形半径 | Ø48 / R=96 dp | §10.1 | | 标签 | 高 24 dp、11px 字,实测最长「隐藏悬浮球」75 dp 宽 | §10.1;`ball-layout.txt` | | 展开窗口里给标签留的位置 | **`labelMax = 96 dp`(硬编码)** | `fanSizeDp()` —— 实测最长标签只用了 75 dp,**有 21 dp 是白留的** | | 展开态遮屏面积 | 308×264 = **81,312 dp²** | 计算 | **规格书 §10 里与本次三个问题直接相关的原文**(逐条摘录,后面反复引用): | 条款 | 原文要点 | |---|---| | §10.1 尺寸 | 「默认位置:屏幕左侧,左边距 12,垂直居中 → **球心 (36, 406)**」「球体 **Ø48(透明热区 64)**」「扇形半径 R=96」「子按钮 Ø48」「**标签胶囊** 高 24、圆角 99、距按钮右侧 4px、11px 字」「展开动画 180ms」 | | §10.1 不透明度 | 「**1.0(三态统一。球内是图片,半透明会发灰;状态一律靠外环表达)**」 | | §10.3 四个按钮 | 图标分别是「播放三角 / 方块」「**四宫格**」「**eye-off**」「**power**」;「按钮数量上限就是 4 个。R=96 时相邻按钮间隙约 17.7px,已接近舒适下限」 | | §10.2 按钮坐标 | 给了**可直接落码**的按钮中心 / 按钮左上角 / **标签左上角 + 标签宽**(38 / 59 / 71 / 60) | | §10.4 三态 | 运行中外环「**有(外扩到 r=28)**」 | | §10.9 技术要点③ | 「与无障碍点击冲突:脚本坐标点击可能点到球;**运行时建议降不透明度**,脚本侧点击区域避开球位」 | | §10.10 与运行条的关系 | 「**若后续觉得重复,最省事的收敛方式是 App 前台时把球降到 0.3 透明度(保留但不抢注意力),而不是隐藏。**」 | | §9 待确认项③ | 「触控目标:既有控件视觉高 26px < 44pt → **实现时热区撑到 44**」 | > ⚠️ 有一处**规格书内部张力**要先说清楚,否则后面每条都会绕: > §10.1 说不透明度「1.0 **三态统一**」,而 §10.9③ / §10.10 又建议"运行时降透明度""App 前台降到 0.3"。 > 合理的读法是:**§10.1 约束的是「不要用透明度表达三种状态」(状态只许由外环表达); > 而"按场景"降低透明度(App 前台 / 运行中)是规格书自己建议的**。本方案的 Q3 就建立在这个读法上。 --- ## 1. Q1 · 展开后要不要去掉按钮文字? ### 1.1 先问清"去掉它想解决什么" 两个完全不同的诉求,答案也不同: | 诉求 | 正确的是 | |---|---| | 视觉啰嗦(一排"运行 / 回到 App / 隐藏悬浮球 / 退出应用"看着像工具条) | 减**视觉噪声** | | 展开时那块透明窗口太大(挡住内容、引擎要绕开一大片) | 减**窗口尺寸** | **关键事实:这两件事在本实现里是耦合的** —— 窗口宽度里那 96 dp 就是给标签留的。 所以"标签"不只是视觉元素,它直接决定**展开时窗口多大**,而窗口大小又会经 `ball-rect.json` 影响**引擎的避让范围**(引擎绕开的是一整个窗口矩形,不是只有按钮)。 ### 1.2 三个方案 | 方案 | 窗口宽 | 遮屏面积变化 | 语义风险 | 前置工程 | |---|---|---|---|---| | **A 纯图标**(彻底去文字) | 308 → **208 dp** | **−32.5%** | **高**:现有图标是字符 `▦`/`⊘`/`✕`,`⊘`(隐藏) 与 `✕`(退出) 都是"让球消失",无文字时极易点错 | 需先把图标换成 §10.3 的线性图标(**四宫格 / eye-off / power**),从 Ardot H2/H3(`34:23`/`34:37`)导出 PNG | | **B 缩短文字** | 308 → 260 dp(最长 "运行/停止" 2 字) | −15.6% | 中:「隐藏悬浮球」压成「隐藏」还行,但「退出应用」压成「退出」可能被读成"退出扇形" | 无 | | **C 保留文字,只降视觉权重**(更小字 / 更低底色 alpha / 去掉胶囊底) | 不变 | 0% | 无 | 无 | **方案 C 解决不了"窗口太大"** —— 它只治观感。所以如果诉求是第二个,C 不算答案。 ### 1.3 一条必须写下来的技术结论(它否掉了两个"看起来很聪明"的方案) > **不能用「按下按钮时才把窗口变大」来浮出文字。** > 因为**改窗口尺寸会让 WindowManager 把进行中的触摸流判为 `ACTION_CANCEL`** > —— 那一刻用户的手指还按着,这次点击会被直接取消掉。README §3 第 6 条就是这个坑的真事记录 > (早期把 CANCEL 当 UP 处理,表现是"每次展开立刻又收起")。 > ⇒ 标签要么**常驻在窗口内**,要么**根本没有**;没有第三种"按需变大"。 同时否掉的还有:**空闲变小 / 触摸变大**这类尺寸动画 —— 同理(改尺寸打断手势), 而且它还要在"手指正按着球"时改尺寸。**要"退让"就用透明度,不要用尺寸。** ### 1.4 推荐 **首选「A + 一次性引导」的组合**,理由是它同时解决两个诉求且不牺牲语义: - 图标先升级为 §10.3 的四个线性图标(四宫格 / eye-off / power / 播放-方块)。 这四个都是系统级通用隐喻,**无文字也能独立表意**;这是"纯图标"能成立的前提。 - 窗口收窄到 208 dp(−32.5%),引擎避让范围同步变小。 - 首次使用的**前两次展开**仍显示文字(`ball.labelsSeen` 计数落盘,≥2 后不再显示), 解决"第一次不知道哪个是哪个"。 - ✅ 这一步**不违反上一条结论**:窗口宽窄的切换发生在**展开这一刻**(手势已结束), 不是按下那一刻。现在的展开本来就是"几何先到位再建视图",机制现成。 **次选「B 缩短文字」**(不想动图标资源时的折中):窗口 −15.6%,代价小但要接受语义折损。 **若诉求纯粹是"视觉啰嗦"**:只做 C 即可,一行都不用改结构。 **顺带一个白捡的优化(与选哪个方案无关)**:`fanSizeDp()` 里 `labelMax = 96` 是硬编码, 而实测最长标签只有 75 dp —— **21 dp 是白留的**。改成按当前实际文案算(`measurePillWidth` 已经能算), 窗口立刻从 308 降到 ~287 dp,**零语义代价**。 --- ## 2. Q2 · 球直径改小 ### 2.1 关键:视觉直径与触控直径是**两件事**,本实现已经解耦 `FB.DIAM` = 视觉球体;`FB.HOT` = 透明热区(触控目标)。两者独立。 所以"改小好看"与"会不会不好戳"**可以分开决定**: - §9③ 自己强调"热区撑到 44";Android 官方最小触控 48 dp。 - 现状 Ø48 视觉 / Ø64 热区:**每边有 8 dp 的"看不见但能点"的余量** —— 本来就有,不是新问题。 - 缩小视觉球会**放大这个余量**(球 Ø40 + 热区 Ø64 → 每边 12 dp)。余量的代价是**误触**: 点到球旁边的空气也会展开扇形。 ### 2.2 推荐 **球 Ø40(−17%)+ 热区 Ø56**(每边 8 dp 余量,仍 > 48 dp 官方最小)。 - 视觉得体,且不牺牲可点性;热区从 64 → 56 会**顺带把折叠态窗口从 88 → 80 dp** 缩小一点。 - 不建议 Ø36:`mark_only_412` 已把品牌标记放大到球直径 87.4%,Ø36 时标记只有 ~31 dp (2x 屏 62 px),笔画细节很可能糊 —— **这一项只能真机看一眼再定**,我先给结论:Ø40 稳,Ø36 待验。 - 不建议保持 Ø48 不动 —— 但也不认为 Ø48 是错的:它是规格书值,动它属于主动偏离。 ### 2.3 ★ 必答的一个副问题:球心坐标跟着变,规格书 §10.2 的表要重算 §10.1 的「左边距 12 → **球心 (36, 406)**」里,**球心 36 = 12 + 半径 24**。 本实现的 `anchorX = dp(FB.EDGE) = 12dp` 语义是**球的最左边缘离屏 12 dp**。于是球改小后: | 取法 | 球心 x(Ø40) | 规格书 §10.2 坐标表 | 观感 | |---|---|---|---| | **边距仍 12**(推荐) | 12 + 20 = **32** | 整表 x 左移 4 dp | 球"贴边距离"不变,符合 §10.1 字面 | | 保球心 36(EDGE 改 16) | **36** | 整表不动 | 球离屏 16 dp,看着比现在"离边远一点" | 两取法都要**重算 §10.2 / §10.11 的两张坐标表**(它们全部由球心推导:按钮中心 = 球心 + R·(cosθ, −sinθ))。 建议选前者(贴边 12 不变)—— "贴边"是这件东西的观感核心,球心只是个中间量。 ### 2.4 改直径的**联动清单**(漏一处就"看起来有点歪",且不会有任何报错) | # | 位置 | 为什么 | |---|---|---| | 1 | `FB.DIAM` | 视觉直径本体 | | 2 | `FB.HOT` | 热区;决定 `collapsedSizeDp()` → 折叠窗口 + 发布给引擎的 `ball-rect.json` | | 3 | **呼吸环尺寸**(`buildBall` / `drawBallAt` 里硬编码的 `60`) | 球 Ø40 而环仍 60,会变成"球很小、环很粗"。§10.4 说的是运行中"外扩到 r=28"(= Ø56)→ 建议环改成 `DIAM + 16`,天然符合 §10.4 | | 4 | `FB.restoreAnchor()` / `FB.clampAnchor()` / `FB.snapToEdge()` / `FB.applyPosition()` | 这四处都用 `dp(FB.DIAM)` 反推球心(`球心 = 窗口位置 + 内边距 + 球心在根里的偏移`),改了直径不改这里,球会整体偏 | | 5 | `buildBall()` / `drawBallAt()` 里 `(FB.HOT - FB.DIAM) / 2` | 球在热区里的居中偏移 | | 6 | **`tools/smoke.js` 两条断言**(`(64 + 12*2) * density`、`48 * density / 2`) | 写死了 64 / 48。**这是好事**:漏改会让自检立刻变红 —— 只要跑一次自检就能确认联动没漏 | | 7 | 品牌图可读性 | 只能真机看(见 2.2) | | 8 | `tools/audit-values.js` | 若有引用直径的断言也要跟着改(当前没有,但改完跑一遍确认) | > 联动之所以这么多,是因为"球心反推"这条公式里有直径。这也是为什么**不建议把直径做成可配置** > —— 一个 `Store` 字段就能改球大小,会让上述 8 处里的一半变成"只有在某个取值下才错", > 而设备上没有 linter。**要改就改常量。** --- ## 3. Q3 · 空闲 3 秒后淡化 / 贴边? ### 3.1 先纠正一个前提:**"贴边"已经在做了** §10.5 就是"松手吸附最近边缘并**记忆位置**",已实现并真机验过(`ball.side` + `ball.yRatio` 落盘)。 所以 Q3 真正要讨论的是"**要不要按时间自动淡化**"以及"**要不要自动收起扇形**"。 顺带**明确否掉**一个变体:**空闲 3 秒自动把球吸到边缘**(球不在边上时自己滑过去)。 理由:① 球会在用户眼皮底下自己移动,观感惊悚;② 移动的目标位置可能正是用户下一个要点的地方; ③ §10.5 已经把"吸附"绑在"拖动松手"这个明确的用户动作上,自动吸附会破坏这个语义。 ### 3.2 规格书里的三条相关原文,指向的**都不是"按时间"** | 条款 | 触发条件 | 透明度 | |---|---|---| | §10.1 | 三态之间(空闲/运行/报错) | **统一 1.0**,不要用透明度表达状态 | | §10.9③ | **运行时**(防脚本坐标点击误点球) | 建议降 | | §10.10 | **App 在自己前台时**(那时 App 内已有运行条,球是重复信息) | **降到 0.3**(原话) | 三条的共同点:**淡化与否取决于"此刻有没有替代品 / 有没有冲突",与"用户多久没碰它"无关。** 而"按时间淡化"有三个实打实的问题: 1. **在 TikTok 上球是唯一控制入口** —— 淡了之后用户"找不到球",而这恰恰是最需要它的场景。 规格书 §10 开篇就写它「是跨 App 唯一的控制入口与运行状态可视处」。 2. **球内是图片,淡化会发灰**(§10.1 明写)。球的底是 `#030014`,压到 0.3 后叠在明亮的视频上, 品牌标记基本糊掉 —— 剩下一个"脏兮兮的暗圆",比不淡更难看。 3. **和"运行中"直接冲突**:球最大的价值是"跨 App 也能看见现在跑没跑"。 3 秒不碰就淡,等于把这条价值在 3 秒后自动关掉。 ### 3.3 推荐:**按场景,不按时间** | 场景 | 表现 | 依据 | |---|---|---| | App 在自己前台 | 球 → **0.35**(用 0.3–0.35,理由见下) | §10.10 原话;此时 App 内运行条已经承担了状态显示 | | 在别的 App(TikTok 等) | **1.0,永不淡** | 它是那里唯一的控制入口 | | 扇形展开着 | **1.0**(无论场景) | 展开时用户正在读按钮,淡了没法用 | | 触摸 / 拖动 / 状态切换的瞬间 | 立刻回 1.0 并保持 1.2 秒 | 否则"点了它却几乎看不见" | - **App 前台信号已经有了**:`ui.emitter.on("resume")` 现成在用(main.js 里用它做"回到 TikTok"提醒)。 再加一个 `"pause"` 即可 —— `resume` → 淡,`pause` → 恢复 1.0。 离线桩也已经支持任意事件(`env.emitter.emit("pause")`),所以**可以直接写断言**。 备选信号是轮询 `currentPackage()`(引擎侧就是这么判断"TikTok 在前台"的), 但它多一个 `setInterval`、且依赖无障碍服务;`ui.emitter` 更轻,优先用它, 真机确认它确实在切走时触发(**这是本方案唯一的未知数**)。 - 为什么用 0.35 而不是 §10.10 的 0.3:0.3 在深色底 `#030014` 上叠到亮色视频上几乎看不见。 **这是对规格书的主动偏离**,会写进注释与 README。若你更想严格照规格书,用 0.3 也行。 - **淡化对象是整个窗口根视图**(球 + 外环一起淡)。在"按场景"方案里这不矛盾: App 前台时状态已由运行条承担,所以环一起淡掉是对的。 ### 3.4 "3 秒"这个念头最该落在哪里:**扇形的自动收起** 如果确实想要"一段时间没动静就退让",最合理的落点是**扇形自动收起**,而不是球的淡化: - 用户点了球、摊开扇形,然后**什么都没点**就走了 —— 这时球"一直摊着",既挡屏幕又占着引擎的避让区。 - 现在只有"点任一按钮后自动收起"(上一轮加的),**"展开后不点任何按钮"这条路没管**。 - 建议阈值 **6 秒**,不是 3 秒:扇形上有 4 个标签要读, 真机 dump 显示「隐藏悬浮球」这种 5 字标签读起来要时间,3 秒偏急。 - 实现上要和 `sync()` 的三态刷新共存:`FB.open` 变化 → 重置计时器。 --- ## 4. 三个问题的推荐汇总 | 问题 | 推荐 | 偏离规格书? | 影响面 | |---|---|---|---| | Q1 按钮文字 | **图标升级为 §10.3 线性图标 → 去文字**(窗口 −32.5%);前两次展开仍显示文字做引导。**过渡:缩短文案(−15.6%)** | 是(§10.1 有标签胶囊) | `fanSpecs`/`openFan`/`fanSizeDp`/品牌图资源 + 1 个新字段 | | Q1′ 白捡的 | `labelMax` 96 → 按实际文案算(实测只需 75),窗口 −7% | 否 | `fanSizeDp` 一行 | | Q2 直径 | **球 Ø40 + 热区 Ø56**,边距仍 12 dp(球心 → 32,§10.2 表重算) | 是(§10.1 是 Ø48/Ø64) | 8 处联动(§2.4),其中 2 处是自检断言 | | Q3 空闲 | **按场景淡化**(App 前台 0.35 / 其它 App 1.0)+ **扇形 6 秒自动收起**;**不做**按时间淡化、**不做**自动吸附 | §10.10 是(0.35 vs 0.3);自动收起规格书未提 | `floatball.js` 新增一个"目标透明度"的唯一所有者 + `ui.emitter.on("pause")` | --- ## 5. 若批准,实施顺序(每步都可独立验收) 1. **Q1′ + Q3 的自动收起**(最小、零偏离、纯收益):`fanSizeDp` 按实测文案算;扇形 6 秒无操作自动收起。 2. **Q3 的按场景淡化**:先验 `ui.emitter.on("pause")` 在真机上是否触发(**这是唯一的未知数**), 再落透明度逻辑。注意:**同一时刻只能有一个"透明度的写者"** —— 现有 `fadeIn()` 也在写 `FB.root.setAlpha()`,两处并发会出现"展开动画把淡化值顶掉" 这类没有报错、只有肉眼能看出的小毛病。必须收敛成一个 `setBallAlpha(target)` (这是上一轮"尺寸动画与建视图打架"的同类问题,别再踩一次)。 3. **Q2 直径**:按 §2.4 的 8 处清单一次改完,跑 `smoke.js` 让那两条断言告诉你有没漏。 4. **Q1 图标**:需要先从 Ardot `34:23` / `34:37` 导出四个线性图标(PNG,2x/3x)→ 换掉字符图标 → 收窄窗口 → 加引导计数。 --- ## 6. 验证计划(批准后照做) **离线(PC)** - `smoke.js`:新增/修改断言 —— ① 窗口宽度随 `labelMax` 收敛;② 展开后 6 秒(`env.flushIntervals()` 或假时钟) 未点任何按钮 → `FB.open === false`;③ `emit("pause")` → 球 alpha 1.0,`emit("resume")` → 0.35; ④ 展开态下 `resume` 不淡化;⑤ 直径改动后折叠窗口尺寸断言(现有两条会自动变红,改完再绿)。 - `audit-values.js`:若 Q1 走"引导计数",新字段要登记进 `Store.FIELDS`(场景 A/C/F 会覆盖)。 - 铁律:**新字段必须同时进字段表**,否则重启后静默丢值(详情卷 B.9)。 **真机(Pixel 5a)** - `ball-debug` 打开,`ball-layout.txt` 核对新几何**逐像素**(这是唯一能看出"有点歪"的手段)。 - 切到 TikTok / 回 App,各看一次透明度(`ui.emitter` 是否触发)。 - 半透明状态下**真的点一次球**,确认能正常展开(alpha 不影响命中,但要实证)。 - 引擎跑一轮,确认避让区随窗口收窄而变小(读 `ball-rect.json`)。 - ⚠️ **跑任务时不要 `uiautomator dump`**(会撞掉无障碍服务,把界面盖成系统设置页)。 --- ## 7. 待你拍板 1. **Q1**:走「图标升级 + 去文字」(要动图标资源),还是先走「缩短文案」过渡,还是只降视觉权重? 2. **Q2**:Ø40/Ø56(推荐)、Ø36/Ø48(更小、待实测)、还是保持 Ø48/Ø64?球心取"边距 12(球心 32)"还是"保规格书球心 36"? 3. **Q3**:按场景淡化(推荐)/按时间淡化(你原本的想法)/两者都要/都不做?扇形自动收起要不要一起做(建议 6 秒)?