You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 21:37:38