floaty 悬浮窗
floaty 悬浮窗
floaty 是 ScriptX 里的悬浮窗模块。它现在既支持系统级悬浮窗,也支持挂在无障碍服务上的无障碍悬浮窗。你可以直接用和 ui 类似的 XML 去创建窗口,然后在脚本里继续改位置、改尺寸、找子控件、绑定点击事件。
如果你用过 Auto.js,那你可以把它先理解成:
floaty.window(xml):可调节版本floaty.rawWindow(xml):不带调节拖拽层的版本floaty.accessibilityWindow(xml):无障碍悬浮窗里的可调节版本floaty.accessibilityRawWindow(xml):无障碍悬浮窗里的原始版本
但当前实现细节还是以本地源码为准,不是照搬别家的。
先记住这 16 条
floaty依赖系统悬浮窗权限,没有权限会直接抛错或返回false。floaty.window(xml)返回的是一个悬浮窗对象,不是普通View。floaty.rawWindow(xml)和window(xml)的最大区别是默认不支持“调整模式拖拽 / 拉伸”。floaty.accessibilityWindow(xml)/floaty.accessibilityRawWindow(xml)不走系统悬浮窗权限,它们走的是无障碍服务的TYPE_ACCESSIBILITY_OVERLAY能力。floaty.checkAccessibilityPermission()检查的是“系统里有没有启用 ScriptX 这项无障碍服务”,不是“当前这一刻服务对象一定已经连上”。floaty.requestAccessibilityPermission()只是拉起系统无障碍设置页,不会自动替你打开开关。- 真正创建无障碍悬浮窗时,除了服务已启用,还要求服务对象已经连上;否则会直接抛错。
floaty.window(xml)/floaty.accessibilityWindow(xml)默认都允许后续进入调整模式;rawWindow(...)/accessibilityRawWindow(...)默认都不允许。floaty.window(xml)里返回对象上的x、y是暴露给脚本的正常坐标,不用自己减状态栏。window.setPosition(x, y)和直接写window.x = x是等价思路。window.setSize(width, height)支持普通数字,也支持-1/-2这种 AndroidMATCH_PARENT/WRAP_CONTENT。window.getWidth()/getHeight()返回的是“当前实际尺寸”,不是你上次传进去的原始 spec。window.requestFocus()适合需要输入框抢焦点、弹软键盘的场景。window.disableFocus()会清焦点,并尝试收起软键盘。window.setTouchable(false)后,这个悬浮窗会变成不可触摸穿透态。window.exitOnClose(true)后,手动close()这个窗口会顺手把当前脚本停掉。
系统悬浮窗 vs 无障碍悬浮窗
如果你第一眼还没决定该用哪组 API,先看这个对照:
| 目标 | 更适合选哪组 |
|---|---|
| 你已经有系统悬浮窗权限,想按最常见方式创建窗口 | floaty.window(...) / floaty.rawWindow(...) |
| 你不想依赖系统悬浮窗权限,而是准备走无障碍能力 | floaty.accessibilityWindow(...) / floaty.accessibilityRawWindow(...) |
| 你需要后面手动开调整模式 | floaty.window(...) 或 floaty.accessibilityWindow(...) |
| 你明确不希望窗口进入调整模式 | floaty.rawWindow(...) 或 floaty.accessibilityRawWindow(...) |
| 你只想先判断系统悬浮窗权限 | floaty.checkPermission() |
| 你只想先判断无障碍服务有没有启用 | floaty.checkAccessibilityPermission() |
直接做决定时最该看的 4 点
| 对比项 | 系统悬浮窗 | 无障碍悬浮窗 |
|---|---|---|
| 创建入口 | floaty.window(...) / floaty.rawWindow(...) | floaty.accessibilityWindow(...) / floaty.accessibilityRawWindow(...) |
| 前提 | 系统悬浮窗权限 | 无障碍服务启用并连上 |
| 窗口类型 | TYPE_APPLICATION_OVERLAY / TYPE_PHONE | TYPE_ACCESSIBILITY_OVERLAY |
| 权限引导 | floaty.requestPermission() | floaty.requestAccessibilityPermission() |
最直接的判断方法
如果你满足下面这条,优先走系统悬浮窗:
- 只是想做普通悬浮按钮、工具面板、调试窗
如果你满足下面这条,优先走无障碍悬浮窗:
- 脚本本来就依赖无障碍,而且你希望整套窗口能力都挂在无障碍链路上
两套最常见模板
系统悬浮窗:
if (!floaty.checkPermission()) {
floaty.requestPermission();
throw new Error("请先授予悬浮窗权限");
}
const win = floaty.window(
<frame>
<text text="system floaty" />
</frame>
);
无障碍悬浮窗:
if (!floaty.checkAccessibilityPermission()) {
floaty.requestAccessibilityPermission();
throw new Error("请先启用无障碍服务");
}
const win = floaty.accessibilityWindow(
<frame>
<text text="accessibility floaty" />
</frame>
);
floaty.checkPermission()
作用
检查当前是否已经有悬浮窗权限。
返回值
boolean
示例
log(floaty.checkPermission());
floaty.requestPermission()
作用
拉起系统悬浮窗权限页。
返回值
boolean
返回规则
| 场景 | 返回值 |
|---|---|
| 已有权限 | true |
| 成功拉起系统设置页 | true |
| 拉起失败 | false |
示例
if (!floaty.checkPermission()) {
floaty.requestPermission();
}
floaty.checkAccessibilityPermission()
作用
检查 ScriptX 的无障碍服务是否已经在系统设置里被启用。
返回值
boolean
它检查的到底是什么
这里检查的是系统无障碍服务启用列表里,是否已经包含 ScriptX 这项服务。
也就是说,它判断的是“服务开关有没有打开”,不是“当前这一刻服务对象是不是已经连上”。
所以这 3 种情况要分开看:
| 场景 | 返回值 |
|---|---|
| 无障碍服务没启用 | false |
| 无障碍服务已启用,但当前服务对象还没连上 | true |
| 无障碍服务已启用,且当前已经连上 | true |
示例
log(floaty.checkAccessibilityPermission());
if (!floaty.checkAccessibilityPermission()) {
toast("请先启用 ScriptX 的无障碍服务");
}
floaty.requestAccessibilityPermission()
作用
拉起系统无障碍设置页。
返回值
boolean
返回规则
| 场景 | 返回值 |
|---|---|
| 成功拉起无障碍设置页 | true |
| 拉起失败 | false |
它不会帮你做什么
它只负责打开:
Settings.ACTION_ACCESSIBILITY_SETTINGS
不会自动:
- 替你打开 ScriptX 这一项
- 替你切换服务开关
- 保证服务立刻连接完成
示例
if (!floaty.checkAccessibilityPermission()) {
floaty.requestAccessibilityPermission();
}
floaty.window(xml)
作用
创建一个可调节型悬浮窗。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
xml | string | XML | 布局 XML;内部走的是 ui 那套 inflater |
返回值
FloatyWindow
失败行为
如果没有悬浮窗权限,会直接抛:
floaty 需要悬浮窗权限
默认行为
创建时会:
- 用
ui模块那套 XML 解析器把布局 inflate 出来 - 创建系统 overlay 窗口
- 默认把窗口放到屏幕左上附近的一个偏移位置
- 默认
touchable=true - 默认
adjustEnabled=false - 默认
exitOnClose=false
示例
const win = floaty.window(
<vertical id="root" padding="12">
<text id="title" text="Hello Floaty" textSize="16sp" />
<button id="closeBtn" text="关闭" />
</vertical>
);
win.closeBtn.click(function () {
win.close();
});
floaty.rawWindow(xml)
作用
创建一个原始型悬浮窗。
返回值
FloatyWindow
和 floaty.window(xml) 的区别
当前最核心的差别只有一条:
floaty.window(...)创建的窗口允许进入调整模式floaty.rawWindow(...)创建的窗口内部allowAdjust=false
这意味着你就算后面手动调用:
win.setAdjustEnabled(true);
在 rawWindow(...) 这条线上也不会真的变成可拖拽 / 可拉伸调整模式。
什么时候选它
当你只想要一个安静的悬浮层,不希望脚本侧再误触发“调节模式拖拽”,可以优先用它。
floaty.accessibilityWindow(xml)
作用
创建一个挂在无障碍悬浮层上的可调节悬浮窗。
它和 floaty.window(xml) 的脚本侧用法很像,但底层窗口类型不一样:
floaty.window(...):系统 overlay 窗口floaty.accessibilityWindow(...):TYPE_ACCESSIBILITY_OVERLAY
参数
| 参数 | 类型 | 说明 |
|---|---|---|
xml | string | XML | 布局 XML;内部同样走 ui 那套 inflater |
返回值
FloatyWindow
前提条件
调用它前,至少要满足两层条件:
- ScriptX 的无障碍服务已经在系统设置里启用
- 当前无障碍服务对象已经真正连上
失败行为
如果系统里压根没启用无障碍服务,当前会抛:
floaty requires accessibility service
如果系统里虽然启用了,但当前服务对象还没连上、拿不到可用的无障碍 WindowManager,当前会抛:
floaty accessibility window requires a connected accessibility service
和 floaty.window(xml) 的差别
| 项目 | floaty.window(...) | floaty.accessibilityWindow(...) |
|---|---|---|
| 前提 | 系统悬浮窗权限 | 无障碍服务启用并连上 |
| 窗口类型 | TYPE_APPLICATION_OVERLAY / TYPE_PHONE | TYPE_ACCESSIBILITY_OVERLAY |
| 默认允许进入调整模式 | 是 | 是 |
| 默认坐标偏移 | 有 | 有 |
示例
if (!floaty.checkAccessibilityPermission()) {
floaty.requestAccessibilityPermission();
throw new Error("请先启用无障碍服务后再重试");
}
const win = floaty.accessibilityWindow(
<vertical padding="12">
<text id="title" text="Accessibility Floaty" textSize="16sp" />
<button id="closeBtn" text="关闭" />
</vertical>
);
win.closeBtn.click(function () {
win.close();
});
floaty.accessibilityRawWindow(xml)
作用
创建一个挂在无障碍悬浮层上的原始型悬浮窗。
返回值
FloatyWindow
它和 floaty.accessibilityWindow(xml) 的区别
核心差别只有一条:
floaty.accessibilityWindow(...):allowAdjust=truefloaty.accessibilityRawWindow(...):allowAdjust=false
所以后面即使你再调:
win.setAdjustEnabled(true);
在这条线上也不会真的进入可拖拽 / 可拉伸的调整模式。
什么时候选它
- 你只想挂一个稳定的无障碍悬浮层
- 你不希望脚本误进入调整模式
- 你只是把它当一个轻量按钮条、状态条或信息面板
示例
if (!floaty.checkAccessibilityPermission()) {
floaty.requestAccessibilityPermission();
throw new Error("请先启用无障碍服务");
}
const win = floaty.accessibilityRawWindow(
<horizontal padding="10">
<text id="label" text="无障碍悬浮条" />
<button id="closeBtn" text="X" />
</horizontal>
);
win.closeBtn.click(function () {
win.close();
});
floaty.closeAll()
作用
关闭当前脚本创建的所有悬浮窗。
返回值
number
返回这次实际尝试关闭的窗口数量。
注意
当前 closeAll() 内部调用的是“不触发 exitOnClose 停脚本”的关闭路径,所以它只是关窗,不会因为某个窗口开过 exitOnClose(true) 就把脚本一并停掉。
它会关掉哪些窗口
按当前实现,它会把当前脚本 runtime 创建出来的这几类窗口都一起关掉:
floaty.window(...)floaty.rawWindow(...)floaty.accessibilityWindow(...)floaty.accessibilityRawWindow(...)
悬浮窗对象 FloatyWindow
下面这一节里的 win 都是指:
const win = floaty.window(...);
先看最常用的 4 个能力
win.x/win.ywin.width/win.heightwin.findView("idName")- 直接
win.someId
因为当前实现里,悬浮窗对象会把自己内部带 id 的子控件也暴露出来,所以很多时候你可以直接:
win.closeBtn.click(function () {
win.close();
});
win.close()
作用
关闭当前悬浮窗。
返回值
undefined
真实行为
- 底层 controller 的关闭结果是
boolean - 但脚本对象这一层当前没有把它再显式返回出来,所以你日常就把它当“执行关闭动作”用就行
和 exitOnClose(true) 的关系
如果你之前开过:
win.exitOnClose(true);
那这次 win.close() 还会顺手请求停止当前脚本。
win.setPosition(x, y)
作用
设置窗口位置。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
x | number | 目标横坐标 |
y | number | 目标纵坐标 |
返回值
悬浮窗对象本身,支持链式。
坐标语义
这里暴露给脚本的是“看起来正常”的窗口左上角坐标:
getX()/setPosition()已经帮你处理了内部的默认偏移和状态栏偏移- 你不需要自己再减状态栏高度
示例
win.setPosition(40, 180);
win.getX() / win.getY()
作用
读取当前窗口位置。
返回值
number
示例
log(`x=${win.getX()} y=${win.getY()}`);
win.setSize(width, height)
作用
设置窗口尺寸。
参数
| 参数 | 类型 | 说明 |
|---|---|---|
width | number | 普通像素值,或 Android 宽度 spec |
height | number | 普通像素值,或 Android 高度 spec |
当前支持的特殊值
| 值 | 含义 |
|---|---|
-1 | MATCH_PARENT |
-2 | WRAP_CONTENT |
> 0 | 固定像素 |
不是这三类时,会被修正成至少 1 的整数。
返回值
悬浮窗对象本身。
示例
win.setSize(300, 180);
win.setSize(-1, -2);
win.getWidth() / win.getHeight()
作用
读取当前窗口实际尺寸。
返回值
number
和 setSize(-2, -2) 的关系
如果你用了 WRAP_CONTENT,这里返回的是当前测量后的真实尺寸,而不是 -2。
win.x / win.y
作用
位置属性形式的读写接口。
示例
win.x = 24;
win.y = 160;
log(win.x);
log(win.y);
win.width / win.height
作用
尺寸属性形式的读写接口。
注意
- 写入时底层走的仍然是
setSize(...) - 读出来的是当前实际尺寸
示例
win.width = 320;
win.height = 200;
win.setTouchable(enabled)
作用
设置窗口是否可触摸。
参数
boolean
返回值
悬浮窗对象本身。
效果
| 值 | 效果 |
|---|---|
true | 正常可点、可交互 |
false | 窗口不可触摸,事件会穿透 |
相关属性
win.touchable
示例
win.setTouchable(false);
sleep(2000);
win.setTouchable(true);
win.requestFocus()
作用
让悬浮窗进入可聚焦状态。
返回值
悬浮窗对象本身。
什么时候需要它
最典型是悬浮窗里有输入框、需要弹软键盘的时候:
const win = floaty.window(
<vertical>
<input id="keyword" hint="请输入关键词" />
</vertical>
);
win.requestFocus();
win.disableFocus()
作用
取消悬浮窗焦点,并尝试收起输入法。
返回值
悬浮窗对象本身。
win.setAdjustEnabled(enabled)
作用
打开或关闭“调整模式”。
返回值
悬浮窗对象本身。
调整模式到底是什么
当前实现里,调整模式打开以后:
- 悬浮窗会拦截触摸
- 拖动主体区域时,移动整个窗口
- 拖右下角附近区域时,拉伸窗口尺寸
只对谁生效
真正有意义的是这两类窗口:
floaty.window(...)floaty.accessibilityWindow(...)
下面这两类窗口底层 allowAdjust=false,所以就算你传 true,最终也不会真的进入可调节状态:
floaty.rawWindow(...)floaty.accessibilityRawWindow(...)
相关方法
win.isAdjustEnabled()win.adjustEnabled
示例
win.setAdjustEnabled(true);
win.isAdjustEnabled()
返回值
boolean
win.exitOnClose(enabled)
作用
设置这个窗口关闭时是否顺手停止当前脚本。
返回值
悬浮窗对象本身。
示例
win.exitOnClose(true);
什么时候适合开
当这个悬浮窗本身就是脚本唯一主界面时,用它很顺:
const win = floaty.window(
<vertical>
<button id="quit" text="退出脚本" />
</vertical>
);
win.exitOnClose(true);
win.quit.click(function () {
win.close();
});
win.findView(idName) / win.findById(idName) / win.findViewById(idName)
作用
在当前悬浮窗根布局里按 id 查子控件。
参数支持两种写法
| 传法 | 说明 |
|---|---|
| 字符串 id 名称 | 例如 "closeBtn" |
| 数字 id | 原生 Android view id 数值 |
返回值
UiViewProxy | null
示例
const btn = win.findView("closeBtn");
if (btn) {
btn.click();
}
win.view / win.contentView
作用
获取这个悬浮窗根布局对应的 ui 视图代理对象。
返回值
UiViewProxy | undefined
什么时候用它
当你想直接把整个悬浮窗根视图当成 ui 代理来改属性时很方便:
win.view.attr("padding", "16dp");
直接按 id 取子控件
当前实现里,悬浮窗对象会把内部带 id 的子控件也暴露成属性,所以:
win.closeBtn
win.title
win.keyword
这类写法都是有效的,只要 XML 里确实有对应的 id。
示例
win.closeBtn.click(function () {
win.close();
});
win.title.text("运行中");
win.toString()
作用
返回一段调试字符串,当前大概长这样:
FloatyWindow(id=..., x=..., y=..., width=..., height=..., touchable=..., adjustEnabled=...)
示例
log(String(win));
一个完整例子:最常见的可点击悬浮按钮
if (!floaty.checkPermission()) {
floaty.requestPermission();
throw new Error("请先授予悬浮窗权限后再运行");
}
const win = floaty.window(
<frame padding="8">
<button id="action" text="关闭" />
</frame>
);
win.setPosition(32, 180);
win.action.click(function () {
win.close();
});
一个完整例子:输入框悬浮窗
const win = floaty.window(
<vertical padding="12" bg="#CC222222">
<input id="keyword" hint="请输入关键词" />
<button id="submit" text="提交" />
</vertical>
);
win.requestFocus();
win.submit.click(function () {
log(`keyword=${win.keyword.text()}`);
win.disableFocus();
});
一个完整例子:调节模式
const win = floaty.window(
<vertical padding="12">
<text text="拖动窗口主体可移动" />
<text text="拖右下角可缩放" />
</vertical>
);
win.setAdjustEnabled(true);
一个完整例子:点击关闭时顺手停脚本
const win = floaty.window(
<vertical padding="12">
<button id="quit" text="退出脚本" />
</vertical>
);
win.exitOnClose(true);
win.quit.click(function () {
win.close();
});
