起因
我一开始以为悬浮窗和悬浮球是同一套东西的两种外观。
写代码的时候才发现,它们分别在两个文件里,各有各的判断接口,各有各的创建接口。而在其中一个模块里,还有一个函数叫bind。
翻完声明文件之后,我对这两个能力的理解变了。而且有个发现让我停了很久:悬浮窗的状态枚举里,有一个状态直接叫“在悬浮球里”。
一、两个模块,两张门票
文件有两个,不是同一个:
ets/api/@ohos.window.floatView.d.ts(约 40 KB)ets/api/@ohos.window.floatingBall.d.ts(约 24 KB)
各自的入口函数几乎对称。
悬浮窗模块:
functionisFloatViewEnabled():boolean;// 第 87 行functioncreate(config:FloatViewConfiguration):Promise<FloatViewController>;// 第 105 行悬浮球模块:
functionisFloatingBallEnabled():boolean;// 第 46 行functioncreate(config:FloatingBallConfiguration):Promise<FloatingBallController>;// 第 60 行两个都返回Promise,都有独立的 enabled 判断。
这里有个容易踩的点:isFloatViewEnabled()和isFloatingBallEnabled()是两个独立开关。系统可能支持悬浮窗但不支持悬浮球,反过来也一样。你的代码如果只判断其中一个就去创建另一个,会在运行时失败。
二、bind()把两者绑成一个整体
悬浮窗模块里有这么一对函数,第 150 行和第 173 行:
functionbind(floatViewController:FloatViewController,floatingBallController:floatingBall.FloatingBallController,floatingBallParams:floatingBall.FloatingBallParams):Promise<void>;functionunbind(floatViewController:FloatViewController,floatingBallController:floatingBall.FloatingBallController):Promise<void>;注意参数类型:floatView模块的bind函数,签名里直接引用了floatingBall命名空间下的类型。两个模块在这里合流了。
把它们绑起来之后,得到的是一个组合形态:悬浮球当常驻的小入口,悬浮窗当展开后的内容面板。用户点球出窗、收起成球,两个控制器之间由系统负责同步。
这个设计解释了后面两节会看到的现象。
三、证据在状态机里:第 5 个状态叫"在悬浮球里"
FloatViewState在第 742 行,共六个状态:
enumFloatViewState{STARTED=1,// 第 750 行HIDDEN=2,// 第 760 行STOPPED=3,// 第 768 行IN_SIDEBAR=4,// 第 776 行IN_FLOATING_BALL=5,// 第 784 行ERROR=6// 第 792 行}[配图位 1|@ohos.window.floatView.d.ts 第 742 行,FloatViewState 枚举全貌|导入后用编辑器图片按钮插入,插完删除本行]
IN_FLOATING_BALL这个状态是上一节bind()的直接体现。悬浮窗被收进悬浮球时,它不是"停止"了,也不是一般的"隐藏",而是进入了一个专门的、可以被bind重新唤起的状态。
另外注意:不可见这件事有三种完全不同的情况。
HIDDEN:被隐藏IN_SIDEBAR:被收进了侧边栏IN_FLOATING_BALL:被收进了悬浮球
三者的恢复路径不一样,能不能恢复也不一样。如果你的判断写成"状态不是STARTED就重新创建悬浮窗",那这几种情况你都会处理错。正确做法是分别处理,尤其IN_FLOATING_BALL要走unbind或重新bind的路径,而不是重新create。
顺带说,这个枚举也是从 1 开始,没有 0。和上一篇写画中画时看到的PiPState一致。
四、对比一下:悬浮球只有两个状态
FloatingBallState在第 386 行:
enumFloatingBallState{STARTED=1,// 第 393 行STOPPED=2// 第 400 行}只有两个。
这个对比很说明问题:悬浮窗的生命周期比悬浮球复杂得多。悬浮球有就是有、没有就是没有;而悬浮窗要在屏幕、侧边栏、悬浮球之间来回流转,所以需要六个状态来表达。
如果你的产品里悬浮球只是"一个常驻的入口",那它的状态处理会很简单;复杂度和坑都在悬浮窗这边。
五、getFloatViewLimits():主动查询限制
这是我在这个模块里觉得最实用、也最少被提到的一个函数,第 190 行:
functiongetFloatViewLimits(templateType:FloatViewTemplateType):FloatViewLimits;传入模板类型,返回这个模板下的限制。FloatViewLimits定义在第 667 行,三个字段:
minSize:window.Size;// 第 675 行maxSize:window.Size;// 第 683 行ratioLimits:Array<RatioLimit>;// 第 691 行而RatioLimit在第 642 行只有两个字段:
minRatio:number;// 第 650 行maxRatio:number;// 第 658 行这套 API 的价值在于它是查询式的,不是约定式的。悬浮窗能开多大、宽高比要在什么范围内,不是文档里定死的常量,而是可以运行时问出来的。
ratioLimits是数组,说明同一个模板下可能存在多段合法的宽高比区间,而不是一个连续范围。如果你的窗口尺寸计算假设了"一个最小值到一个最大值",遇到分段限制就会算错。
这类"先问限制再设尺寸"的写法,比"按经验设一个尺寸然后被系统拒绝"靠谱得多。
六、两个模板枚举,起点不一样
悬浮窗的模板,第 545 行:
enumFloatViewTemplateType{ROUNDED_RECTANGLE=0,// 第 553 行HORIZONTAL_BAR=1// 第 561 行}悬浮球的模板,第 408 行:
enumFloatingBallTemplate{STATIC=1,// 第 416 行NORMAL=2,// 第 424 行EMPHATIC=3,// 第 432 行SIMPLE=4// 第 440 行}悬浮窗的从 0 开始,悬浮球的从 1 开始。
两个模块,同一个 SDK,同一个能力域,枚举起点不一致。这已经是本系列第三次遇到这种情况了:断点枚举从 0 开始,画中画状态从 1 开始,现在这两个模板又各走各的。
结论很简单也很实用:不要用真假值判断枚举。用===比较具体成员,或者用switch穷举。
顺便注意一个语义细节:悬浮窗模板是几何形状(圆角矩形、横向条),悬浮球模板是视觉强调程度(静态、普通、强调、简洁)。两套分类逻辑不在一个维度上,不要试图映射。
七、stopReason是字符串
FloatViewStateChangeInfo在第 700 行:
state:FloatViewState;// 第 708 行stopReason:string;// 第 733 行第二个字段的类型是string,不是错误码。
这和上一篇画中画里的stateChange回调一致,那边的第二个参数reason也是字符串。
这个设计有好有坏。好处是信息量大,能直接读到"为什么停"。坏处是不能用switch穷举判断,也不能做本地化映射,只能拿去做日志。
如果你的代码要根据停止原因做分支处理,得自己写字符串匹配。这在对版本升级敏感的代码里是个隐患,因为字符串内容可能随版本调整,而类型系统拦不住这种变化。相对稳妥的做法是只把reason用于日志,分支逻辑仍然基于state。
八、悬浮球的一个细粒度能力
FloatingBallTextUpdateAnimationType在第 465 行:
enumFloatingBallTextUpdateAnimationType{ANIMATION_NONE=0,// 第 473 行ANIMATION_OPACITY=1// 第 481 行}这个枚举是给"悬浮球上的文字发生变化"这个场景用的,有两个选项:不动画,或者淡入淡出。
颗粒度到这一步,说明悬浮球上的文字更新是个被认真对待的场景。如果你的悬浮球要显示未读数、状态文字这类会变的内容,这个参数值得设一下,否则文字会硬切,观感差。
几处和常见理解不一样的地方
- 悬浮窗和悬浮球是两个模块,各有独立的
isFloatViewEnabled()和isFloatingBallEnabled(),判断一个不等于判断另一个 bind()能把两者绑成一个整体,参数签名里直接引用了另一个模块的类型FloatViewState的六个状态里有一个叫IN_FLOATING_BALL,这是绑定的直接体现- "不可见"有三种:
HIDDEN、IN_SIDEBAR、IN_FLOATING_BALL,三者恢复路径不同 - 悬浮球只有两个状态,复杂度远低于悬浮窗的六个
getFloatViewLimits()可以运行时查询尺寸和宽高比限制,ratioLimits还是数组,说明限制可能是分段的- 两个模板枚举起点不一致,一个从 0 一个从 1
stopReason是字符串,只能当日志用,不适合做分支判断
写浮窗功能之前,建议先想清楚一件事:你要的是悬浮窗、悬浮球,还是两者绑定后的组合形态。这三个答案对应的代码结构差别很大,而bind这套 API 的存在,本身就说明官方认为第三种是常见的做法。
补充说明:本篇没有跑通截图。手边没有真机,模拟器也无法验证悬浮窗的实际窗口行为,所以文中只做声明层面的核对,每个结论都标了行号,可以自行打开对照。涉及运行期表现的地方,我按注释原文转述,没有替官方补充结论。
本系列其他文章
- 我把 HarmonyOS 的折叠屏 API 翻了一遍:10 个折叠状态,你大概只处理了 3 个
- 我读了 HarmonyOS 的断点声明:有 5 个档位,高度断点算的其实是宽高比
- 画中画起不来的三种原因:设备支持、设置开关、窗口可见是三件事