如何在Flutter中实现Android全局悬浮窗?附代码求解决方案
嘿,这个需求我刚好折腾过!要把Flutter控制的View嵌入Android悬浮窗,核心是借助Flutter提供的FlutterEngine和FlutterView来实现——毕竟Flutter在Android端的UI最终是通过原生View渲染的。下面给你一步步拆解具体实现:
1. 初始化FlutterEngine(核心)
首先在你的FloatingWindowService里初始化FlutterEngine,它负责运行Dart代码并渲染Flutter UI。注意要处理好引擎的生命周期,避免内存泄漏:
@RequiresApi(Build.VERSION_CODES.O) class FloatingWindowService : Service() { private lateinit var windowManager: WindowManager private lateinit var flutterEngine: FlutterEngine private lateinit var flutterView: FlutterView private lateinit var windowParams: WindowManager.LayoutParams override fun onCreate() { super.onCreate() windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager // 1. 初始化FlutterEngine flutterEngine = FlutterEngine(this) // 加载指定的Flutter路由(可以在Flutter端配置不同页面) flutterEngine.navigationChannel.setInitialRoute("/floating_avatar") // 执行Dart入口函数 flutterEngine.dartExecutor.executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ) // 可选:将引擎缓存起来,后续其他地方复用(避免重复创建消耗资源) FlutterEngineCache.getInstance().put("floating_engine", flutterEngine) // 2. 获取FlutterView flutterView = FlutterView(this, null, flutterEngine) } }
2. 配置悬浮窗参数并添加View
接下来就是你原本的悬浮窗参数配置,把FlutterView作为要添加的View传入:
override fun onCreate() { // ... 前面的初始化代码 // 配置悬浮窗参数 windowParams = WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // 如果需要交互可以去掉这个flag PixelFormat.TRANSLUCENT ) // 设置初始位置(比如右上角,偏移20dp) windowParams.gravity = Gravity.TOP or Gravity.END windowParams.x = dip2px(this, 20f) windowParams.y = dip2px(this, 20f) // 添加FlutterView到悬浮窗 windowManager.addView(flutterView, windowParams) } // 辅助函数:dp转px private fun dip2px(context: Context, dpValue: Float): Int { val scale = context.resources.displayMetrics.density return (dpValue * scale + 0.5f).toInt() }
3. 处理Service生命周期,释放资源
在Service销毁时,一定要销毁FlutterEngine并移除悬浮窗View,否则会造成内存泄漏:
override fun onDestroy() { super.onDestroy() // 从缓存移除并销毁FlutterEngine FlutterEngineCache.getInstance().remove("floating_engine") flutterEngine.destroy() // 移除悬浮窗View windowManager.removeViewImmediate(flutterView) } override fun onBind(intent: Intent): IBinder? { // 如果不需要绑定,返回null即可 return null }
4. Flutter端适配:渲染悬浮窗UI
在Flutter代码里,你需要配置路由,让/floating_avatar路由对应你想要的悬浮UI(比如圆形头像,支持拖拽):
void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( initialRoute: '/', routes: { '/': (context) => const MainAppScreen(), // 你的主应用页面 '/floating_avatar': (context) => const FloatingAvatarWidget(), // 悬浮窗UI }, ); } } class FloatingAvatarWidget extends StatelessWidget { const FloatingAvatarWidget({super.key}); @override Widget build(BuildContext context) { // 用Material包裹,避免Flutter渲染异常 return Material( color: Colors.transparent, // 背景透明,只显示头像 child: GestureDetector( // 可选:添加拖拽逻辑,需要通过MethodChannel通知Android端更新位置 onPanUpdate: (details) { // 这里可以调用MethodChannel告诉Android更新悬浮窗的x/y坐标 _updateFloatingPosition(details.delta.dx, details.delta.dy); }, child: Container( width: 60, height: 60, decoration: const BoxDecoration( shape: BoxShape.circle, image: DecorationImage( image: NetworkImage("https://your-avatar-url.com/avatar.jpg"), fit: BoxFit.cover, ), boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)] ), ), ), ); } // 借助MethodChannel和Android通信更新位置 Future<void> _updateFloatingPosition(double dx, double dy) async { const channel = MethodChannel("com.yourpackage/floating_window"); try { await channel.invokeMethod("updatePosition", { "deltaX": dx.toInt(), "deltaY": dy.toInt(), }); } catch (e) { debugPrint("Failed to update position: $e"); } } }
5. 可选:实现拖拽功能(Android端)
如果要让悬浮窗能拖拽,需要在Android端的MethodChannel里处理Flutter传来的位置更新请求:
override fun onCreate() { // ... 前面的初始化代码 // 初始化MethodChannel,和Flutter端通信 val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.yourpackage/floating_window") methodChannel.setMethodCallHandler { call, result -> when (call.method) { "updatePosition" -> { val deltaX = call.argument<Int>("deltaX") ?: 0 val deltaY = call.argument<Int>("deltaY") ?: 0 // 更新悬浮窗位置 windowParams.x -= deltaX windowParams.y += deltaY windowManager.updateViewLayout(flutterView, windowParams) result.success(null) } else -> result.notImplemented() } } }
额外注意事项
- 权限问题:一定要确保用户已经授予
SYSTEM_ALERT_WINDOW权限,Android 6.0以上需要动态申请,否则windowManager.addView会抛出异常。 - 交互权限:如果你的悬浮窗需要响应点击、输入等事件,要移除
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE这个标记,不过这样可能会影响底层应用的交互。 - 性能优化:FlutterEngine的创建比较消耗资源,尽量复用(比如用
FlutterEngineCache缓存),避免多次创建销毁。
内容的提问来源于stack exchange,提问作者Goliy Muzhik
相关产品推荐
相关产品推荐

