ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Auto.js悬浮窗开发实战:从权限配置到扫码识别全流程

Auto.js悬浮窗开发实战:从权限配置到扫码识别全流程 做Android自动化脚本悬浮窗是一个绕不开的功能。Auto.js里它叫floaty几乎能解决所有“脚本跑到哪一步了、结果怎么展示在屏幕上、我想点个按钮手动干预一下”这类问题。这篇日志是我折腾floaty的完整记录从权限配置到第一个窗口从交互到和plugin模块配合做快递条形码识别都会讲到。适合刚接触Auto.js、想用悬浮窗搭控制面板或做自动化工具的朋友照着抄就能跑起来。1. 悬浮窗是什么为什么自动化脚本离不开它1.1 脚本界面输出的几种方式悬浮窗强在哪写过Auto.js脚本的人都知道脚本本身是跑在手机上的JavaScript它没有一个天然的控制台。你要看脚本运行状态最常见的办法是三个log()输出在Logcat里刷日志脚本在后台跑你看不到实时状态出了问题只能翻历史记录。toast()弹提示能显示几秒钟但一闪而过用户可能没看到而且不能交互不能点击。悬浮窗直接在屏幕上画一个独立的小窗口不打断当前操作可以显示文本、图片、按钮还能被拖动。悬浮窗最大的优势是“常驻”加“可交互”。你可以在屏幕上放一个半透明的小面板实时显示脚本进度上面放一个“暂停”按钮和一个“终止”按钮。这在自动化脚本里几乎是刚需因为脚本一旦跑起来你不能切到别的App去看状态悬浮窗就能一直压在其他应用上层。类比一下Toast像便利贴贴上去一会儿就掉悬浮窗像桌面小组件一直在那还能按。手机屏幕就那么大悬浮窗给你一个不抢占焦点、不跳转页面的操作入口这是Activity页面做不到的。1.2 floaty的定位与常见使用场景Auto.js提供的悬浮窗API叫floaty。它不是一个独立的框架更像一个轻量级的UI运行时——你用JavaScript描述界面它帮你在系统窗口层渲染出来并且可以绑定事件。常见的落地场景我总结过几类脚本状态监控批量任务跑进度实时显示“已完成3/20”这类信息。控制面板放一个“开始”“停止”“设置”按钮代替记不住的手势触发。结果展示比如扫码识别结果需要用户确认悬浮窗弹一个卡片上面是识别结果下面两个按钮“确认入库”“重扫”。调试工具开发时在悬浮窗上放一个console输入框和输出框方便在真机上测试。这篇文章其实是以“入门基础”为定位但基础归基础它连接到的能力一点都不基础。后面我会用一个“全屏脚本调用plugin扫码识别快递条形码结果回填到悬浮窗”的例子把几条线串起来。2. 写悬浮窗前必须搞定的事权限与运行环境2.1 悬浮窗权限和无障碍服务两个权限别搞混很多新手最容易卡死在第一步脚本能跑但不出悬浮窗或者弹一下就闪退。十有八九是权限没给对。Auto.js落地悬浮窗最少涉及两个系统权限权限系统英文名作用悬浮窗权限SYSTEM_ALERT_WINDOW允许应用在系统界面之上绘制窗口缺了它窗口创建就直接抛异常无障碍服务权限BIND_ACCESSIBILITY_SERVICE让Auto.js能读取屏幕内容、执行自动点击、监听界面变化注意这两个是独立的。auto.waitFor()负责等待无障碍服务连接floaty.checkPermission()负责检查悬浮窗权限。写代码时最好两个都检查一遍不然你会在“有悬浮窗权限但脚本点不动”“有辅助权限但不出窗”之间反复横跳。我第一次写悬浮窗脚本时只调了auto.waitFor()以为有了无障碍就能随便画窗口。结果屏幕黑了一下控制台报错permission denied。后来才明白悬浮窗是“窗口类权限”由系统窗口管理模块管控和读取屏幕的辅助通道完全是两码事。2.2 不同品牌手机怎么开悬浮窗权限Auto.js的API里有floaty.requestPermission()但这个函数通常只能帮你跳转到系统设置页不能直接替用户打开开关。各厂商ROM对悬浮窗权限的管理路径也不一样。大致路径如下原生Android / 大部分手机设置 → 应用 → 找到Auto.js → 高级 → 显示在其他应用上层 → 允许。小米/MIUI设置 → 应用设置 → 授权管理 → 悬浮窗权限 → 找到Auto.js并开启。MIUI的权限入口经常改版直接搜“悬浮窗”通常更快。华为/荣耀EMUI/HarmonyOS设置 → 应用 → 应用管理 → Auto.js → 权限 → 悬浮窗。部分系统需要额外开启“后台弹出界面”权限否则窗口可能被系统白名单拦截。打开设置后脚本里尽量做一次兜底判断防止用户漏开。if (!floaty.checkPermission()) { floaty.requestPermission(); toast(请去系统设置开启悬浮窗权限); sleep(3000); }这里有一个比较隐蔽的坑floaty.checkPermission()在某些Android 9以下机型上可能返回false即使你已经手动开过权限。这个API底层依赖Settings.canDrawOverlays()个别ROM魔改后返回结果不准。如果遇到这种情况可以设置里重新开关一次悬浮窗权限多半能解决。2.3 版本选择与运行环境建议我用的环境是Auto.js 4.x系列4.1.1之后API相对稳定。社区里还有一些分支项目把开源版本继续维护下去接口大体兼容。如果你刚入门优先选择4.x版本的开源分支资料多、踩坑记录多、问问题有人答。编辑器方面手机自带编辑器写小脚本很方便但写悬浮窗这种带布局的我更推荐用VS Code配合Auto.js插件在电脑上写完通过WiFi或USB推送手机运行。悬浮窗代码一旦涉及复杂布局手机那个小输入框真的能让人崩溃。还有一点调试悬浮窗脚本时不要用云手机和模拟器。悬浮窗权限在模拟器上的行为很怪部分模拟器干脆不支持TYPE_APPLICATION_OVERLAY窗口会始终显示不出来或者显示在错误位置。真机调试虽然麻烦但得到的结果是可靠的。3. 第一个悬浮窗从创建到显示再到存活3.1 最简实现几行代码弹出一个信息窗一个最基础的悬浮窗不需要XML布局不需要复杂的控件直接内联HTML风格的标签就行。auto.waitFor(); if (!floaty.checkPermission()) { floaty.requestPermission(); toast(请开启悬浮窗权限); } // 创建一个悬浮窗窗口 var w floaty.window( frame bg#88000000 gravitycenter text idinfo text自动化脚本运行中 textSize15sp textColor#ffffff/ /frame ); // 设置窗口大小和位置 w.setSize(400, 120); w.setPosition(device.width - 440, 200); // 保持脚本存活 setInterval(() {}, 1000);解释一下每一行的作用。floaty.window()接收一个UI描述对象返回一个悬浮窗实例。标签语法是Auto.js自带的UI DSLframe是根布局text是文本控件id属性用于之后引用控件对象。setSize()是设置宽高单位是像素setPosition()设置窗口左上角在屏幕上的坐标。屏幕坐标系的原点在屏幕左上角x向右增大y向下增大。所以device.width - 440这个x坐标是让窗口紧贴屏幕右侧。3.2 为什么窗口一闪而过以及如何让窗口长时间存活新手最常见的现象是脚本运行完悬浮窗跟着消失屏幕什么都没留下。这不是Bug而是Auto.js的执行模型决定的——脚本作为JavaScript执行环境主线程执行到末尾就结束了进程退出悬浮窗自然被销毁。理解这点很重要悬浮窗的生命周期依赖脚本进程的生命周期。想长时间显示悬浮窗必须让脚本“不要停”。常见的保持存活手段setInterval(() {}, 1000)空定时器每秒执行一次空函数阻止事件循环退出。events.observeForever()监听事件期间脚本不会结束适合悬浮窗和事件联动。threads.start()启动一个子线程跑后台逻辑然后在主线程开一个空循环等待。我建议维护悬浮窗时把主逻辑放在子线程主线程专职保活。这样即使逻辑跑完主线程还有一个阻塞循环在窗口不会意外消失。var w floaty.window(...); // 子线程更新窗口文本 threads.start(function () { for (var i 0; i 10; i) { w.info.setText(进度: i /10); sleep(1000); } }); // 主线程保活 setInterval(() {}, 1000);3.3 用代码拼UI还是用XML布局怎么选Auto.js支持两种方式描述悬浮窗界面一种是上面那种内联标签直接在floaty.window()参数里写另一种是把布局写成XML通过ui.layout()解析后传给floaty.window()。我的习惯是控件不超过五六个用内联标签涉及输入框、按钮、列表、多个嵌套布局一律用XML文件单独维护。悬浮窗本身小但它的布局为了适配不同屏幕需要写weight属性、对齐方式等堆在内联标签里可读性太差。XML布局示例?xml version1.0 encodingutf-8? LinearLayout xmlnshttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:padding12dp android:background#EE333333 LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal TextView android:idid/title android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:text识别结果 android:textColor#FFFFFF android:textSize14sp / Button android:idid/close_btn android:layout_widthwrap_content android:layout_heightwrap_content android:text关闭 android:textSize12sp / /LinearLayout TextView android:idid/content android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop6dp android:text暂无内容 android:textColor#FFE082 android:textSize18sp / /LinearLayout在JavaScript里加载var layout ui.layout(layout.xml); // 也可以是字符串 var w floaty.window(layout); w.close_btn.click(() w.close());用XML布局还有一个额外好处——w.content这些控件对象的结构更稳定不容易因为代码里某行拼接错误导致渲染异常。4. 让悬浮窗真正“好用”交互、移动与更新4.1 给悬浮窗加按钮和输入框并绑定事件悬浮窗的价值不只是“看”更是“点”。给悬浮窗加一个按钮很简单但有几个细节处理不好用户体验会直线下降。var lay vertical padding8 bg#88000000 text idlabel text快递单号 textColor#FFFFFF textSize14sp/ input idinput_box text hint扫描结果将自动填入 textColor#FFFFFF/ button idbtn_confirm text确认 textColor#333333 bg#FFE082/ /vertical; var w floaty.window(lay); w.setSize(420, 220); w.setPosition(device.width - 460, 300); // 点击确认按钮 w.btn_confirm.click(function () { var val w.input_box.text(); if (!val) { toast(内容为空无法确认); return; } // 实际业务逻辑 console.log(确认内容: val); w.close(); });这里有几个细节值得提。一是w.input_box.text()返回的是输入框里的字符串不是某个对象二是click回调里如果直接做耗时操作比如网络请求会阻塞悬浮窗UI导致窗口看起来像卡死了。正确做法是耗时逻辑放到threads.start()里。如果遇到“按钮没反应”先检查两件事一是悬浮窗是否有焦点二是回调里是否抛了异常。Auto.js的悬浮窗事件回调异常很隐蔽常常表现为点击后什么都没发生。可以在回调开头加一行try-catch用toast输出异常开发阶段非常管用。4.2 实现拖拽移动与位置记忆系统自带悬浮窗大多支持拖动但floaty.window创建的窗口默认不可拖。要实现拖动需要监听触摸事件然后手动改窗口坐标。var offsetX 0, offsetY 0; // 在标题栏或整个窗口上绑定触摸事件 w.root.setOnTouchListener(function (view, event) { switch (event.getAction()) { case android.view.MotionEvent.ACTION_DOWN: offsetX event.getRawX() - w.getX(); offsetY event.getRawY() - w.getY(); break; case android.view.MotionEvent.ACTION_MOVE: w.setPosition(event.getRawX() - offsetX, event.getRawY() - offsetY); break; } return true; });event.getRawX()和event.getRawY()返回的是触摸点在屏幕上的绝对坐标w.getX()返回窗口预设的x坐标。两者相减就能得到手指相对窗口左边缘的偏移量后续移动时用新的触摸坐标减去偏移量就是窗口该待的位置。实现拖动后我强烈建议把窗口位置保存下来。可以存到storages里每次启动悬浮窗前读取上一次的位置。别小看这个功能用户自己调好的位置下次被重置会非常恼火。var storage storages.create(floaty_pos); var saved storage.get(pos, null); if (saved) { w.setPosition(saved.x, saved.y); } // 在触摸A CTION_MOVE结束时 storage.put(pos, { x: w.getX(), y: w.getY() });4.3 悬浮窗内部更新与多个悬浮窗之间的通信运行中的悬浮窗更新文本直接拿控件对象调setText()即可。但如果你在子线程更新UI要注意线程归属。Auto.js的UI操作一般要求在UI线程执行跨线程更新偶发会导致动画卡顿或者闪退。稳妥做法是使用ui.run()。ui.run(function () { w.info.setText(当前时间: new Date().toLocaleTimeString()); });需要多个悬浮窗时比如一个主控制面板一个状态监视窗数据通信最简单的方式是用全局变量或storages存储。但全局变量在模块隔离后不一定相通建议用storages统一管理。监视窗通过setInterval周期性读取存储也能做到实时刷新这个方案我实测最稳不会出现双窗互相引用导致生命周期异常的问题。5. 实战案例悬浮窗结合plugin模块完成快递条形码识别5.1 场景需求与整体方案我在现实生活中遇到过这样一个需求朋友做驿站代收每天要录入大量快递单号。手机原相机扫码后手动记单号再录入系统效率低还容易错。他问能不能写个脚本打开相机扫条形码识别出快递单号后自动跳回录入界面。抛开具体商业软件用Auto.js完全可以搭一个原型。核心拆成三块扫码能力调用相机识别条形码/二维码。这部分重活适合封装成独立模块也就是Auto.js里的plugin思路——一个可以被多个脚本复用的功能包。结果整理拿到单号后格式化校验位数必要时拼接时间戳、站点编号。悬浮窗界面整个过程需要一个结果确认窗口显示识别内容提供“确认”“重扫”按钮。之所以用悬浮窗而不是直接跳转页面是因为录单场景下用户可能正泡在录入App里悬浮窗弹出来用户看一眼确认后自动隐藏完全不打断当前App的交互流程。5.2 用plugin封装扫码模块悬浮窗展示结果Auto.js里引入plugin模块的方式类似标准JS模块const codeScanner require(scanner_plugin);这个模块内部封装了相机扫码逻辑对外暴露一个Promise风格的接口返回识别到的字符串。这是我推荐的plugin拆分思路扫码的相机权限申请、图像解析、超时重试全部放在模块里主脚本保持干净只负责拉起扫码和展示结果。主脚本的流程骨架如下auto.waitFor(); var w floaty.window(...); // 悬浮窗显示结果和确认按钮 function startScan() { w.info.setText(正在扫码...); threads.start(function () { try { // 调用plugin等待返回结果 var result codeScanner.scan({ timeout: 8000 }); ui.run(function () { if (result) { w.input_box.setText(result.text); w.info.setText(识别完成); } else { w.info.setText(未识别到条形码); } }); } catch (err) { ui.run(function () { w.info.setText(扫码异常: err.message); }); } }); } w.btn_confirm.click(function () { var code w.input_box.text(); // 写业务逻辑写入本地表格或通过网络API上传 toast(确认: code); w.close(); }); w.btn_rescan.click(function () { startScan(); }); startScan(); setInterval(() {}, 1000);真实场景里扫码模块可能会通过Intent调用系统相机这时有一个衔接问题扫码页面是另一个Activity悬浮窗还压在上面吗这个我之前栽过跟头。部分ROM会把系统相机界面提到悬浮窗之上导致扫码结束后悬浮窗半天不刷新。我的对策是扫码开始前先把悬浮窗暂时隐藏等结果字段回来再重新显示w.hide(); // 隐藏悬浮窗 codeScanner.scan(); // 系统相机界面在前台 w.show(); // 拿到结果后重新显示5.3 这个方案里的几个关键坑第一个坑是相机扫码耗电又发热。调用外部相机App进行扫码一次就是几秒高负载连续录几十单手机能当暖手宝。优化的方向是把扫码模块改成本地图像解析但这需要引入二维码库复杂度一下上来了。入门阶段用plugin封装系统相机足够注意在悬浮窗上提示“连续扫码建议用夹具或支架”。第二个坑是条形码识别结果的准确率。快递面单上的条形码印刷质量参差不齐识别结果偶尔会少一位或串号。业务上必须做校验快递单号一般是13位数字识别结果如果不是数字或者长度不对悬浮窗上提示重新扫码而不是进入确认流程。第三个坑是线程同步。扫码模块返回结果是在自己的线程里而悬浮窗控件更新只能在UI线程忘了用ui.run()就会看到窗口卡死或者报CalledFromWrongThreadException。这个错误信息翻译过来就是“你从错误的线程更新了UI”排查思路很简单所有setText、setVisibility都包进ui.run()。6. 悬浮窗使用中的常见问题排查表与避坑经验6.1 常见问题速查现象原因解决方案floaty.window报permission denied没有开启悬浮窗权限去系统设置开启再用floaty.checkPermission()验证悬浮窗出现后立刻消失脚本主线程执行完毕进程退出用setInterval或events.observeForever保活按钮点击无反应回调异常被吞或窗口无焦点回调里加try-catch输出日志确认窗口requestFocus()悬浮窗文字不更新子线程直接操作UI用ui.run()包裹更新逻辑窗口位置每次启动都重置没有持久化位置用storages保存x、y启动时读取扫码后悬浮窗不恢复系统相机Activity抢占焦点扫码前hide()返回值后show()悬浮窗在部分App上不显示目标App启用了系统级安全层换成floaty.rawWindow并测试或接受局限性6.2 性能与兼容性经验悬浮窗内存占用通常不大但有一个隐形问题——UI刷新频率。不要用setInterval每几十毫秒去操作控件文本频繁的setText会触发重绘悬浮窗层级的重绘开销比其他View更大。实测下来一秒刷新一次或者两秒刷新一次肉眼感受不到差异但CPU占用能降一半以上。Android 8.0之后系统要求悬浮窗类型必须使用TYPE_APPLICATION_OVERLAY。Auto.js的floaty.window内部已经处理了但你如果自己封装原生窗口一定要明确指定这个type否则窗口直接不显示Logcat里会报BadTokenException。另外部分国产ROM有“后台应用清理”机制就算悬浮窗在显示脚本进程一旦被认为无活跃操作就可能被挂起或者杀掉。解决办法是开发阶段把Auto.js加入“自启动白名单”“锁后台任务”“省电策略设为不限制”。这一项我提前踩过不然脚本运行到一半悬浮窗突然消失日志却查不到任何异常。6.3 调试悬浮窗的实用技巧最后分享几个调试技巧。第一开发期不要用悬浮窗调试悬浮窗控制台日志优先用console.log配合Auto.js的日志面板看。否则悬浮窗渲染一旦阻塞你连日志都切不过去。第二给悬浮窗加一个隐藏的调试开关。我在根布局上放了一个几乎透明的TextView连续点击五次会弹出调试面板显示窗口大小、位置、版本号、当前线程状态。这在真机调试时非常有用尤其当问题只在特定手机上复现时。第三窗口崩溃自愈。悬浮窗脚本有时会因为布局解析失败直接闪退但Auto.js进程还在。如果不想每次都在手机端重新打开可以写一个守护脚本监听Auto.js进程是否存活发现脚本异常退出后自动重启脚本。// 守护代码 while (true) { app.startActivity(脚本名.js); sleep(60000); }这种方式不适合正式发布但开发期能节省大量重新点击启动脚本的时间。7. 我的个人实操体会与建议折腾悬浮窗这几年最大的体会是它是整个自动化脚本的“门面”也是“命门”。门面在于用户接触到的界面就是悬浮窗好不好用、稳不稳定直接决定脚本口碑命门在于一旦权限被系统回收、窗口被清理再好的业务逻辑都白搭。所以我在每个项目里都固定做三件事写代码前先确认权限和保活机制写代码时强制所有上行UI操作用ui.run()写完功能后一定在低版本Android和国产ROM上过一遍兼容性。别小看这三步它们至少帮我减少了百分之八十的线上问题反馈。后续如果你想继续深挖可以往三个方向走一是把悬浮窗布局做成可配置的用JSON定义控件和样式二是把扫码plugin升级成本地图像识别彻底摆脱外部相机App的依赖三是在悬浮窗上加手势识别比如从屏幕边缘滑入呼出控制面板。每一条路都有对应的难点但切入点都在本文基础的floaty之上。先用这篇文章里的代码把第一个悬浮窗跑起来后面的事情就好说了。
返回列表