⛔ 本方案已弃用(2026-10-14)
用户明确表示「上面的方案放弃」,并改为直接要求去掉点击球后的跳动/展开动画 —— 那件事已单独做完(见
README.md§8 的「点击后先跳一下」一节与src/floatball.js)。 本文件仅作为当时的分析留档,不要再照它实施;其中"球的直径 / 按钮文字 / 空闲淡化" 三个议题仍未定,未做任何改动。
2026-10-14。起因:三个观感问题(按钮文字 / 球直径 / 空闲淡化或贴边)。 本文只做方案讨论,一行代码都没改(
dist与设备上的产物仍一致)。 规格书原文取自资料库 docJZf9xTgLXBFgjXJ4dJWVsU§10(本次逐条核对过,不是凭记忆)。
设备 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 就建立在这个读法上。
两个完全不同的诉求,答案也不同:
| 诉求 | 正确的是 |
|---|---|
| 视觉啰嗦(一排"运行 / 回到 App / 隐藏悬浮球 / 退出应用"看着像工具条) | 减视觉噪声 |
| 展开时那块透明窗口太大(挡住内容、引擎要绕开一大片) | 减窗口尺寸 |
关键事实:这两件事在本实现里是耦合的 —— 窗口宽度里那 96 dp 就是给标签留的。
所以"标签"不只是视觉元素,它直接决定展开时窗口多大,而窗口大小又会经
ball-rect.json 影响引擎的避让范围(引擎绕开的是一整个窗口矩形,不是只有按钮)。
| 方案 | 窗口宽 | 遮屏面积变化 | 语义风险 | 前置工程 |
|---|---|---|---|---|
| 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 不算答案。
不能用「按下按钮时才把窗口变大」来浮出文字。 因为改窗口尺寸会让 WindowManager 把进行中的触摸流判为
ACTION_CANCEL—— 那一刻用户的手指还按着,这次点击会被直接取消掉。README §3 第 6 条就是这个坑的真事记录 (早期把 CANCEL 当 UP 处理,表现是"每次展开立刻又收起")。 ⇒ 标签要么常驻在窗口内,要么根本没有;没有第三种"按需变大"。
同时否掉的还有:空闲变小 / 触摸变大这类尺寸动画 —— 同理(改尺寸打断手势), 而且它还要在"手指正按着球"时改尺寸。要"退让"就用透明度,不要用尺寸。
首选「A + 一次性引导」的组合,理由是它同时解决两个诉求且不牺牲语义:
ball.labelsSeen 计数落盘,≥2 后不再显示),
解决"第一次不知道哪个是哪个"。
次选「B 缩短文字」(不想动图标资源时的折中):窗口 −15.6%,代价小但要接受语义折损。
若诉求纯粹是"视觉啰嗦":只做 C 即可,一行都不用改结构。
顺带一个白捡的优化(与选哪个方案无关):fanSizeDp() 里 labelMax = 96 是硬编码,
而实测最长标签只有 75 dp —— 21 dp 是白留的。改成按当前实际文案算(measurePillWidth 已经能算),
窗口立刻从 308 降到 ~287 dp,零语义代价。
FB.DIAM = 视觉球体;FB.HOT = 透明热区(触控目标)。两者独立。
所以"改小好看"与"会不会不好戳"可以分开决定:
球 Ø40(−17%)+ 热区 Ø56(每边 8 dp 余量,仍 > 48 dp 官方最小)。
mark_only_412 已把品牌标记放大到球直径 87.4%,Ø36 时标记只有 ~31 dp
(2x 屏 62 px),笔画细节很可能糊 —— 这一项只能真机看一眼再定,我先给结论:Ø40 稳,Ø36 待验。§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 不变)—— "贴边"是这件东西的观感核心,球心只是个中间量。
| # | 位置 | 为什么 |
|---|---|---|
| 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。要改就改常量。
§10.5 就是"松手吸附最近边缘并记忆位置",已实现并真机验过(ball.side + ball.yRatio 落盘)。
所以 Q3 真正要讨论的是"要不要按时间自动淡化"以及"要不要自动收起扇形"。
顺带明确否掉一个变体:空闲 3 秒自动把球吸到边缘(球不在边上时自己滑过去)。 理由:① 球会在用户眼皮底下自己移动,观感惊悚;② 移动的目标位置可能正是用户下一个要点的地方; ③ §10.5 已经把"吸附"绑在"拖动松手"这个明确的用户动作上,自动吸附会破坏这个语义。
| 条款 | 触发条件 | 透明度 |
|---|---|---|
| §10.1 | 三态之间(空闲/运行/报错) | 统一 1.0,不要用透明度表达状态 |
| §10.9③ | 运行时(防脚本坐标点击误点球) | 建议降 |
| §10.10 | App 在自己前台时(那时 App 内已有运行条,球是重复信息) | 降到 0.3(原话) |
三条的共同点:淡化与否取决于"此刻有没有替代品 / 有没有冲突",与"用户多久没碰它"无关。
而"按时间淡化"有三个实打实的问题:
#030014,压到 0.3 后叠在明亮的视频上,
品牌标记基本糊掉 —— 剩下一个"脏兮兮的暗圆",比不淡更难看。| 场景 | 表现 | 依据 |
|---|---|---|
| App 在自己前台 | 球 → 0.35(用 0.3–0.35,理由见下) | §10.10 原话;此时 App 内运行条已经承担了状态显示 |
| 在别的 App(TikTok 等) | 1.0,永不淡 | 它是那里唯一的控制入口 |
| 扇形展开着 | 1.0(无论场景) | 展开时用户正在读按钮,淡了没法用 |
| 触摸 / 拖动 / 状态切换的瞬间 | 立刻回 1.0 并保持 1.2 秒 | 否则"点了它却几乎看不见" |
ui.emitter.on("resume") 现成在用(main.js 里用它做"回到 TikTok"提醒)。
再加一个 "pause" 即可 —— resume → 淡,pause → 恢复 1.0。
离线桩也已经支持任意事件(env.emitter.emit("pause")),所以可以直接写断言。
备选信号是轮询 currentPackage()(引擎侧就是这么判断"TikTok 在前台"的),
但它多一个 setInterval、且依赖无障碍服务;ui.emitter 更轻,优先用它,
真机确认它确实在切走时触发(这是本方案唯一的未知数)。#030014 上叠到亮色视频上几乎看不见。
这是对规格书的主动偏离,会写进注释与 README。若你更想严格照规格书,用 0.3 也行。如果确实想要"一段时间没动静就退让",最合理的落点是扇形自动收起,而不是球的淡化:
sync() 的三态刷新共存:FB.open 变化 → 重置计时器。| 问题 | 推荐 | 偏离规格书? | 影响面 |
|---|---|---|---|
| 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") |
fanSizeDp 按实测文案算;扇形 6 秒无操作自动收起。ui.emitter.on("pause") 在真机上是否触发(这是唯一的未知数),
再落透明度逻辑。注意:同一时刻只能有一个"透明度的写者" ——
现有 fadeIn() 也在写 FB.root.setAlpha(),两处并发会出现"展开动画把淡化值顶掉"
这类没有报错、只有肉眼能看出的小毛病。必须收敛成一个 setBallAlpha(target)
(这是上一轮"尺寸动画与建视图打架"的同类问题,别再踩一次)。smoke.js 让那两条断言告诉你有没漏。34:23 / 34:37 导出四个线性图标(PNG,2x/3x)→
换掉字符图标 → 收窄窗口 → 加引导计数。离线(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 会覆盖)。真机(Pixel 5a)
ball-debug 打开,ball-layout.txt 核对新几何逐像素(这是唯一能看出"有点歪"的手段)。ui.emitter 是否触发)。ball-rect.json)。uiautomator dump(会撞掉无障碍服务,把界面盖成系统设置页)。