flutter_floatwing

Version pub points popularity likes License

一个Flutter插件,可更轻松地使用纯Flutter为Android创建浮动/覆盖窗口。仅限Android


功能

  • 纯Flutter:您可以使用纯Flutter编写整个覆盖窗口。
  • 简单:至少仅需1行代码即可启动您的覆盖窗口。
  • 自动调整大小:只需关心您的Flutter小部件的大小,它将自动调整为Android视图。
  • 多窗口:支持在一个应用中创建多个覆盖窗口,并且窗口可以拥有子窗口。
  • 可通信:您的主应用可以与窗口通信,窗口之间也支持通信。
  • 事件机制:触发窗口生命周期和其他操作(如拖动)的事件,您可以更灵活地控制您的窗口。
  • 更多功能即将推出…

预览

夜间模式 简单示例 辅助触摸模拟

安装

打开位于应用文件夹中的pubspec.yaml文件,并在dependencies下添加flutter_floatwing

dependencies:
  flutter_floatwing: <latest_version>

最新版本是 版本

然后您应该安装它,

  • 从终端:运行flutter pub get
  • 从Android Studio/IntelliJ:点击pubspec.yaml顶部操作栏中的Packages get。
  • 从VS Code:点击pubspec.yaml顶部操作栏右侧的Get Packages。

或者直接在命令行中添加

flutter pub add flutter_floatwing

快速入门

我们使用Android的系统警告窗口进行显示,因此首先需要在AndroidManifest.xml中添加权限

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

为将在覆盖窗口中显示的小部件添加一个路由

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      initialRoute: "/",
      routes: {
          "/": (_) => HomePage(),
          // add a route as entrypoint for your overlay window.
          "/my-overlay-window": (_) => MyOverlayWindow(),
      },
    );
  }

在我们启动浮动窗口之前,我们需要在initState或任何按钮的回调函数中检查并请求权限,并初始化flutter_floatwing插件

// check and grant the system alert window permission.
FloatwingPlugin().checkPermission().then((v) {
    if (!v) FloatwingPlugin().openPermissionSetting();
});

// initialize the plugin at first.
FloatwingPlugin().initialize();

接下来开始创建并启动您的覆盖窗口

// define window config and start the window from config.
WindowConfig(route: "/my-overlay-window") 
    .to() // create a window object
    .create(start: true); // create the window and start the overlay window.

了解更多信息。

  • 有关更多功能,请参阅用法部分
  • 架构

    在我们详细了解flutter_floatwing如何管理窗口之前,我们需要了解该插件设计的一些内容。

    • id是窗口的唯一标识符,所有对窗口的操作都基于此id,创建前必须提供一个。
    • 我们将打开主应用程序创建的第一个引擎视为主引擎插件引擎。由服务创建的其他引擎是窗口引擎
    • 不同的引擎是不同的线程,无法直接通信。
    • 允许从主引擎订阅所有窗口的事件,也允许在窗口引擎中订阅自身和子窗口的事件。但我们不能订阅兄弟窗口或父窗口的事件。
    • share数据是窗口引擎插件引擎之间通信的唯一方式,它没有限制,除了数据需要可序列化。这意味着您可以将数据从任何地方共享到任何地方。

    一个floatwing窗口对象包含:一个通过runApp运行小部件的Flutter引擎和一个添加到窗口管理器的视图。

    floatwing window

    整个视图层次结构如下

    flutter floatwing architecture

    用法

    在我们详细了解如何在Flutter中使用flutter_floatwing之前,让我们先谈谈flutter_floatwing如何创建一个新的覆盖窗口

    • 首先,我们需要由主应用启动一个作为管理器的服务。
    • 然后创建窗口请求发送给服务。
    • 在服务中,我们使用入口点启动Flutter引擎。
    • 创建一个新的Flutter视图并将其附加到Flutter引擎。
    • 将视图添加到Android窗口管理器。

    窗口与配置

    WindowConfig包含窗口的所有配置。我们可以使用配置来创建窗口,如下所示

    void _createWindow() {
        var config = WindowConfig();
        w = Window(config, id="my-window");
        w.create();
    }

    如果您不需要注册事件或数据处理程序,只需使用配置创建窗口即可。

    void _createWindow() {
        WindowConfig(id="my-window").create();
    }

    但是正如您所见,如果您想为窗口提供一个id,则必须在WindowConfig中提供。

    如果要注册处理程序,可以使用to()函数首先将配置转换为窗口,这对于简化代码非常有用。

    void _createWindow() {
        WindowConfig(id="my-window").to()
            .on(EventType.WindowCreated, (w, _) {})
            .create();
    }

    窗口生命周期

    • 已创建
    • 已启动
    • 已暂停
    • 已恢复
    • 已销毁

    待办事项

    入口点

    入口点是引擎执行的起点。我们支持3种配置模式

    名称 Config(配置) 如何使用
    路由 WindowConfig(route: "/my-overlay") – 在您的主路由中为覆盖窗口添加一个路由– 使用配置启动窗口:WindowConfig(route: "/my-overlay")
    静态函数 WindowConfig(callback: myOverlayMain) – 定义一个调用runApp以启动小部件的静态函数void Function()– 使用配置启动窗口:WindowConfig(callback: myOverlayMain)
    入口点 WindowConfig(entry: "myOverlayMain") – 第一步与静态函数相同。– 在静态函数上方添加@pragma("vm:entry-point")– 使用配置启动窗口:WindowConfig(entry: "myOverlayMain")– *类似于静态函数,但将函数名称字符串作为参数*

    route示例

    1. 为主应用程序中的覆盖小部件添加路由。

      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          initialRoute: "/",
          routes: {
              "/": (_) => HomePage(),
              // add a route as entrypoint for your overlay window.
              "/my-overlay-window": (_) => MyOverlayWindow(),
          },
        );
      }
    1. 使用route作为配置启动窗口。

        void _startWindow() {
            // define window config and start the window from config.
            WindowConfig(route: "/my-overlay-window") 
                .to() // create a window object
                .create(start: true); // create the window and start the overlay window.
        }

    静态函数示例

    1. 定义一个调用runApp的静态函数

    void myOverlayMain() {
        runApp(MaterialApp(
            home: AssistivePannel(),
        ));
        // or simply use `floatwing` method to inject `MaterialApp`
        // runApp(AssistivePannel().floatwing(app: true));
    }
    1. 使用callback作为配置启动窗口。

        void _startWindow() {
            // define window config and start the window from config.
            WindowConfig(callback: myOverlayMain) 
                .to() // create a window object
                .create(start: true); // create the window and start the overlay window.
        }

    entry-point示例

    1. 定义为调用runApp并添加prama的静态函数

    @pragma("vm:entry-point")
    void myOverlayMain() {
        runApp(MaterialApp(
            home: AssistivePannel(),
        ));
        // or simply use `floatwing` method to inject `MaterialApp`
        // runApp(AssistivePannel().floatwing(app: true));
    }
    1. 使用entry作为配置启动窗口。

        void _startWindow() {
            // define window config and start the window from config.
            WindowConfig(entry: "myOverlayMain") 
                .to() // create a window object
                .create(start: true); // create the window and start the overlay window.
        }

    包装您的小部件

    对于简单的小部件,您无需处理您的小部件。但是,如果您需要更多功能并使您的代码整洁,我们为您的小部件提供了一个注入器。

    目前有以下列出的功能,

    • 自动调整窗口视图大小。
    • 自动同步并确保窗口。
    • 包装MaterialApp
    • 更多功能即将推出

    之前,我们像这样编写我们的覆盖主函数,

    void overlayMain() {
      runApp(MaterialApp(
        home: MyOverView(),
      ))
    }

    现在,我们可以简洁地编码,

    void overlayMain() {
      runApp(MyOverView().floatwing(app: true)))
    }

    我们可以包装成WidgetWidgetBuilder,包装WidgetBuilder,我们可以通过Window.of(context)访问窗口实例,而FloatwingPlugin().currentWindow是获取窗口实例以包装Widget的唯一方法。

    如果我们要通过Window.of(context)访问窗口,请像这样更改代码,

    void overlayMain() {
      runApp(((_)=>MyOverView()).floatwing(app: true).make()))
    }

    在覆盖窗口中访问窗口

    在您的窗口引擎中,您可以通过两种方式访问窗口对象

    • 直接访问插件的缓存字段:FloatwingPlugin().currentWindow
    • 如果小部件通过.floatwing()注入,您可以通过Window.of(context)获取窗口。

    FloatwingPlugin().currentWindow将返回null,除非初始化已完成。

    如果您使用.floatwing(debug: true)注入WidgetBuilder,则可以访问当前窗口。它将始终返回非空值,除非您启用调试.floatwing(debug: true)

    例如,如果我们想获取当前窗口的id,我们可以这样做,

    /// ...
    import 'package:flutter_floatwing/flutter_floatwing.dart';
    
    class _ExampleViewState extends State<ExampleView> {
        Window? w;
    
        @override
        void initState() {
            super.initState();
            SchedulerBinding.instance?.addPostFrameCallback((_) {
                w = Window.of(context);
                print("my window id is ${w.id}");
            });
        }
    }

    订阅事件

    我们可以订阅来自窗口的事件,并在事件触发时触发操作。窗口的事件将发送到主引擎、自身窗口引擎和父窗口引擎。这意味着您可以从主应用程序的Flutter、覆盖窗口和父覆盖窗口订阅窗口的事件。

    目前我们支持窗口生命周期和拖动操作的事件。

    enum EventType {
      WindowCreated,
      WindowStarted,
      WindowPaused,
      WindowResumed,
      WindowDestroy,
    
      WindowDragStart,
      WindowDragging,
      WindowDragEnd,
    }

    更多事件类型即将推出,欢迎贡献!

    例如,我们想在窗口启动时执行一些操作,我们可以像这样编码,

      @override
      void initState() {
        super.initState();
    
        SchedulerBinding.instance?.addPostFrameCallback((_) {
          w = Window.of(context);
          w?.on(EventType.WindowStarted, (window, _) {
              print("$w has been started.");
          }).on(EventType.WindowDestroy, (window, data) {
              // data is a boolean value, which means that the window
              // are destroyed force or not.
              print("$w has been destroy, force $data");
          });
        });
      }

    与窗口共享数据

    共享数据是与窗口通信的唯一方式。我们提供了一种简单的方法来实现这一点:window.share(data)

    例如,如果您想从主应用程序共享数据到覆盖窗口。

    首先在主应用程序中获取目标窗口,通常可以使用创建的窗口,或者您可以从windows缓存中按id获取一个,

        Window w;
    
        void _startWindow() {
            w = WindowConfig(route: "/my-overlay-window").to();
        }
    
        void _shareData(dynamic data) {
            w.share(data).then((value) {
                // and window can return value.
            });
            // or just take one from cache
            // FloatwingPlugin().windows["default"]?.share(data);
        }

    如果你想为数据命名,可以添加名称参数:`“w.share(data, name=\”name-1\”)"`。

    然后您应该在窗口中通过注册数据处理程序来监听数据。

      @override
      void initState() {
        super.initState();
    
        SchedulerBinding.instance?.addPostFrameCallback((_) {
          w = Window.of(context);
          w?.onData((source, name, data) async {
              print("get $name data from $source: $data");
          });
        });
      }

    处理程序的函数签名是Future<dynamic> Function(String? source, String? name, dynamic data)

    • source是数据的来源,如果是来自主应用程序则为null,来自窗口则是窗口的id
    • name是数据名称,您可以出于不同目的共享数据。
    • data是您获取的数据。
    • 如果您想执行一些操作,则可以返回值。

    通信方向存在限制,除非您将数据发送给自己,否则不允许。这意味着只要您知道窗口的id,就可以发送数据。*目前尚未实现共享到主应用程序。*

    注意:您共享的数据应该是可序列化的。

    API参考

    FloatwingPlugin实例

    FloatwingPlugin是一个单例类,每次调用FloatwingPlugin()工厂方法时都会返回相同的实例。

    WindowConfig对象

    待办事项

    Window对象

    待办事项

    事件

    窗口生命周期

    操作

    更多事件类型即将推出,欢迎贡献!

    支持

    您觉得这个插件有用吗?请考虑捐款以帮助改进它!

    贡献

    随时欢迎贡献!

    GitHub

    查看 Github