pub 地址:pub.dartlang.org/packages/an…
功能
- 使用 android 原生接口实现,可以让 flutter app 覆盖在其他 app 上面
- 浮窗作为 android 前台服务运行,可完全脱离主应用
- 默认支持拖拽移动浮窗,且可以与 flutter 手势共存
- 提供浮窗大小、位置控制接口
- 提供主应用与浮窗应用相互通信接口
安装与配置
flutter pub add android_window
修改 MainActivity.kt 让 MainActivity 继承 qiuxiang.android_window.AndroidWindowActivity:
class MainActivity : qiuxiang.android_window.AndroidWindowActivity()
创建 MainApplication.kt:
package your_package // 和 MainActivity.kt 保持一致即可
class MainApplication : qiuxiang.android_window.AndroidWindowApplication()
修改 AndroidManifest.xml 的 <application> 新增属性 android:name=".MainApplication":
<application
android:name=".MainApplication"
...
>
用法
main.dart:
import 'package:android_window/main.dart' as android_window;
import 'package:flutter/material.dart';
import 'android_window.dart';
@pragma('vm:entry-point')
void androidWindow() {
runApp(const AndroidWindowApp());
}
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(title: 'Flutter Demo', home: HomePage());
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => android_window.open(size: const Size(300, 200)),
child: const Icon(Icons.add),
),
);
}
}
我们需要用 @pragma('vm:entry-point') 声明一个入口函数,默认函数名是 androidWindow,当然你可以随意指定一个,只是调用 open 的时候需要同时指定参数 entryPoint:。
android_window.dart:
import 'package:android_window/android_window.dart';
import 'package:flutter/material.dart';
class AndroidWindowApp extends StatelessWidget {
const AndroidWindowApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AndroidWindow(
child: Scaffold(
backgroundColor: Colors.lightGreen.withOpacity(0.9),
body: const Padding(
padding: EdgeInsets.all(8),
child: Text('Hello android window'),
),
),
);
}
}
浮窗 app 的写法就和我们平时写的 app 没什么区别了,如果需要支持窗口拖拽移动,则要在最外层使用 AndroidWindow。
最终效果:
更完整的示例请参考:github.com/qiuxiang/fl…
浮窗与主应用的通信
主应用和浮窗都有 post 和 setHandler 方法用于发送消息以及设置监听处理函数。用法举例:
主应用发送消息到浮窗:
final response = await android_window.post('message_name', 'data');
浮窗监听并处理主应用消息:
AndroidWindow.setHandler((name, data) async {
switch(name) {
case 'bar':
return 'data';
}
});
反过来同理。
聊聊这个库的一些实现细节
核心功能参考了官方文档 Adding a Flutter View to an Android app,浮窗 app 通过另一个 entryPoint 运行在独立的 FlutterEngine。
为了让主应用与浮窗进行通信,需要自定义 Application,并存储两个互通的 channel。
这个库并没有直接用 MethodChannel,而是实验性地用了 pigeon,总的来说,MethodChannel 更灵活高效,而 pigeon 更规范且类型安全,之后我应该都会考虑用 pigeon 而不是 MethodChannel。
手势识别是个问题,我们的目标是能让 flutter 手势正常工作,同时支持拖拽移动和浮窗内滚动。只是在原生端做拖拽的话,要么与 flutter 手势冲突,要么会屏蔽掉 flutter 的手势;而只在 flutter 做拖拽识别然后传递 dx、dy 给原生,也是不行的,因为拖拽的过程中浮窗位置就已经改变了,而 flutter 的 dx、dy 是相对自身,要消除这部分误差并不容易。最终我想到的方案是,flutter 告诉原生什么时候开始和结束拖拽,原生就只处理拖拽中的窗口移动,这样就避免了 flutter 手势的冲突。