├── .github └── FUNDING.yml ├── .gitignore ├── .idea ├── libraries │ ├── Dart_SDK.xml │ ├── Flutter_Plugins.xml │ └── Flutter_for_Android.xml ├── modules.xml ├── runConfigurations │ └── example_lib_main_dart.xml └── workspace.xml ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── im │ └── zoe │ └── labs │ └── flutter_floatwing │ ├── FloatWindow.kt │ ├── FloatwingService.kt │ ├── FlutterFloatwingPlugin.kt │ └── Utils.kt ├── assets ├── flutter-floatwing-arch.png ├── flutter-floatwing-example-1.gif ├── flutter-floatwing-example-2.gif ├── flutter-floatwing-example-3.gif └── flutter-floatwing-window.png ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── im │ │ │ │ │ └── zoe │ │ │ │ │ └── labs │ │ │ │ │ └── flutter_floatwing_example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── lib │ ├── main.dart │ └── views │ │ ├── assistive_touch.dart │ │ ├── night.dart │ │ └── normal.dart ├── pubspec.lock ├── pubspec.yaml └── test │ └── widget_test.dart ├── flutter_floatwing.iml ├── lib ├── flutter_floatwing.dart └── src │ ├── constants.dart │ ├── event.dart │ ├── plugin.dart │ ├── provider.dart │ ├── utils.dart │ └── window.dart ├── pubspec.lock ├── pubspec.yaml └── test └── flutter_floatwing_test.dart /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: zoeim # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | - 'https://payone.wencai.app/s/zoe' 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | -------------------------------------------------------------------------------- /.idea/libraries/Dart_SDK.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/libraries/Flutter_Plugins.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/libraries/Flutter_for_Android.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/runConfigurations/example_lib_main_dart.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f7a6a7906be96d2288f5d63a5a54c515a6e987fe 8 | channel: unknown 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.1 2 | 3 | - fix: fix build error for version of kotlin 4 | 5 | ## 0.1.0 6 | 7 | - feature: basic support for overlay window 8 | - chore: add exmaples 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 wellwell.work, LLC by Zoe 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # flutter_floatwing 4 | 5 | [![Version](https://img.shields.io/pub/v/flutter_floatwing.svg)](https://pub.dartlang.org/packages/flutter_floatwing) 6 | [![pub points](https://badges.bar/flutter_floatwing/pub%20points)](https://pub.dev/packages/flutter_floatwing/score) 7 | [![popularity](https://badges.bar/flutter_floatwing/popularity)](https://pub.dev/packages/flutter_floatwing/score) 8 | [![likes](https://badges.bar/flutter_floatwing/likes)](https://pub.dev/packages/flutter_floatwing/score) 9 | [![License](https://img.shields.io/badge/license-AL2-blue.svg)](https://github.com/jiusanzhou/flutter_floatwing/blob/master/LICENSE) 10 | 11 | A Flutter plugin that makes it easier to make floating/overlay windows for Android with pure Flutter. **Android only** 12 | 13 |
14 | 15 | --- 16 | 17 | ## Features 18 | 19 | - **Pure Flutter**: you can write your whole overlay windows in pure Flutter. 20 | - **Simple**: at least only 1 line of code to start your overlay window. 21 | - **Auto Resize**: just care about your Flutter widget size, it will auto resize for Android view. 22 | - **Multi-window**: support create multiple overlay windows in one App, and window can has child windows. 23 | - **Communicable**: your main App can talk with windows, and also supported between windows. 24 | - **Event Mechanism**: fire the events of window lifecyle and other actions like drag, you can controll your window more flexible. 25 | - *and more features are coming ...* 26 | 27 | ## Previews 28 | 29 | |Night mode|Simpe example|Assistive touch mock| 30 | |:-:|:-:|:-:| 31 | |![](./assets/flutter-floatwing-example-1.gif)|![](./assets/flutter-floatwing-example-2.gif)|![](./assets/flutter-floatwing-example-3.gif)| 32 | 33 | ## Installtion 34 | 35 | Open the `pubspec.yaml` file located inside the app folder, and add `flutter_floatwing` under `dependencies`. 36 | ```yaml 37 | dependencies: 38 | flutter_floatwing: 39 | ``` 40 | 41 | The latest version is 42 | [![Version](https://img.shields.io/pub/v/flutter_floatwing.svg)](https://pub.dartlang.org/packages/flutter_floatwing) 43 | 44 | Then you should install it, 45 | - From the terminal: Run `flutter pub get`. 46 | - From Android Studio/IntelliJ: Click Packages get in the action ribbon at the top of `pubspec.yaml`. 47 | - From VS Code: Click Get Packages located in right side of the action ribbon at the top of `pubspec.yaml`. 48 | 49 | 50 | Or simply add it in your command line: 51 | ```bash 52 | flutter pub add flutter_floatwing 53 | ``` 54 | 55 | ## Quick Started 56 | 57 | We use the android's system alert window to display, so need to add the permission in `AndroidManifest.xml` first: 58 | ```xml 59 | 60 | ``` 61 | 62 | Add a route for your widget which will be displayed in the overlay window: 63 | ```dart 64 | @override 65 | Widget build(BuildContext context) { 66 | return MaterialApp( 67 | debugShowCheckedModeBanner: false, 68 | initialRoute: "/", 69 | routes: { 70 | "/": (_) => HomePage(), 71 | // add a route as entrypoint for your overlay window. 72 | "/my-overlay-window": (_) => MyOverlayWindow(), 73 | }, 74 | ); 75 | } 76 | ``` 77 | 78 | Before we start the floating window, 79 | we need to check and request the permission, and initialize the `flutter_floatwing` plugin in `initState` or any button's callback function: 80 | ```dart 81 | // check and grant the system alert window permission. 82 | FloatwingPlugin().checkPermission().then((v) { 83 | if (!v) FloatwingPlugin().openPermissionSetting(); 84 | }); 85 | 86 | // initialize the plugin at first. 87 | FloatwingPlugin().initialize(); 88 | ``` 89 | 90 | Next to start create and start your overlay window: 91 | ```dart 92 | // define window config and start the window from config. 93 | WindowConfig(route: "/my-overlay-window") 94 | .to() // create a window object 95 | .create(start: true); // create the window and start the overlay window. 96 | ``` 97 | 98 | --- 99 | 100 | Notes: 101 | 102 | - `route` is one of 3 ways to define entrypoint for overlay window. 103 | Please check the [Entrypoint section](#entrypoint) for more information. 104 | - See [Usage section](#usage) for more functions. 105 | 106 | ## Architecture 107 | 108 | Before we see how `flutter_floatwing` manage windows in detail, 109 | we need to know some things about the design of the plugin. 110 | - `id` is the unique identifier for the window, and all operations on the window are based on this `id`, you must provide one before creating. 111 | - We consider the first engine created by opening the main application as the `main engine` or `plugin engine`. The other engines created by service are `window engine`. 112 | - Different `engine` are different `threads` and cannot communicate directly. 113 | - Subscribe events of all windows from `main engine` is allowed, it's also allowed to subscribe events of own and child windows in `window engine`. But we can not subscribe events of sibling or parent windows. 114 | - `share` data is the only way to communicate between `window engine` or `plugin engine`, there are no restrictions on it, except that the data needs to be serializable. Which means you can share data from anywhere to anywhere. 115 | 116 | 117 | A floatwing window object contains: a flutter engine which run a widget by `runApp` and a view which add to window manager. 118 | 119 | ![floatwing window](./assets/flutter-floatwing-window.png) 120 | 121 | The whole view hierarchy like below: 122 | 123 | ![flutter floatwing architecture](./assets/flutter-floatwing-arch.png) 124 | 125 | ## Usage 126 | 127 | Before we start how to use `flutter_floatwing` in detail, 128 | let's talk about how the `flutter_floatwing` create a new overlay window: 129 | - First of all we need to start a service as manager by main app. 130 | - Then create window request send to the service. 131 | - In the service, we start the flutter engine with entrypoint. 132 | - Create a new flutter view and attach it to the flutter engine. 133 | - Add the view to android window manager. 134 | 135 | ### Window & Config 136 | 137 | `WindowConfig` contains all configuration for window. 138 | We can use configuration to create a window like below: 139 | ```dart 140 | void _createWindow() { 141 | var config = WindowConfig(); 142 | w = Window(config, id="my-window"); 143 | w.create(); 144 | } 145 | ``` 146 | 147 | If you have no need to register event or data handler, 148 | you can just use config to create a window. 149 | ```dart 150 | void _createWindow() { 151 | WindowConfig(id="my-window").create(); 152 | } 153 | ``` 154 | 155 | But as you can see, if you want to provide a id for window, 156 | must provide in `WindowConfig`. 157 | 158 | If want to register handler, you can use a `to()` function to turn a config to a window at first, 159 | this is every useful when you want to make code simple. 160 | ```dart 161 | void _createWindow() { 162 | WindowConfig(id="my-window").to() 163 | .on(EventType.WindowCreated, (w, _) {}) 164 | .create(); 165 | } 166 | ``` 167 | 168 | #### Lifecyle of Window 169 | 170 | - created 171 | - started 172 | - paused 173 | - resumed 174 | - destroy 175 | 176 | *TODO* 177 | 178 | ### Entrypoint 179 | 180 | Entrypoint is where the engine execute from. We support 3 modes of configuration: 181 | 182 | |Name|Config|How to use| 183 | |:--|:--|:--| 184 | |`route`|`WindowConfig(route: "/my-overlay")`|- Add a route for overlay window in your main routes
- Start window with config: `WindowConfig(route: "/my-overlay")`| 185 | |`staic function`|`WindowConfig(callback: myOverlayMain)`|- Define a static function `void Function()` which calling `runApp` to start a widget.
- Start window with config: `WindowConfig(callback: myOverlayMain)`| 186 | |`entry-point`|`WindowConfig(entry: "myOverlayMain")`|- First step is same as `staic function`.
- Add `@pragma("vm:entry-point")` above the static function.
- Start window with config: `WindowConfig(entry: "myOverlayMain")`
- *like `static function`, but use string of function name as parameter*| 187 | 188 | #### Example for `route` 189 | 190 | 1. Add route for your overlay widget in the main application. 191 | ```dart 192 | @override 193 | Widget build(BuildContext context) { 194 | return MaterialApp( 195 | debugShowCheckedModeBanner: false, 196 | initialRoute: "/", 197 | routes: { 198 | "/": (_) => HomePage(), 199 | // add a route as entrypoint for your overlay window. 200 | "/my-overlay-window": (_) => MyOverlayWindow(), 201 | }, 202 | ); 203 | } 204 | ``` 205 | 206 | 2. Start window with `route` as config. 207 | ```dart 208 | void _startWindow() { 209 | // define window config and start the window from config. 210 | WindowConfig(route: "/my-overlay-window") 211 | .to() // create a window object 212 | .create(start: true); // create the window and start the overlay window. 213 | } 214 | ``` 215 | 216 | #### Example for `static function` 217 | 218 | 1. Define a static function which called `runApp` 219 | ```dart 220 | void myOverlayMain() { 221 | runApp(MaterialApp( 222 | home: AssistivePannel(), 223 | )); 224 | // or simply use `floatwing` method to inject `MaterialApp` 225 | // runApp(AssistivePannel().floatwing(app: true)); 226 | } 227 | ``` 228 | 229 | 2. Start window with `callback` as config. 230 | ```dart 231 | void _startWindow() { 232 | // define window config and start the window from config. 233 | WindowConfig(callback: myOverlayMain) 234 | .to() // create a window object 235 | .create(start: true); // create the window and start the overlay window. 236 | } 237 | ``` 238 | 239 | #### Example for `entry-point` 240 | 241 | 1. Define as static function which called `runApp` and add `prama` 242 | ```dart 243 | @pragma("vm:entry-point") 244 | void myOverlayMain() { 245 | runApp(MaterialApp( 246 | home: AssistivePannel(), 247 | )); 248 | // or simply use `floatwing` method to inject `MaterialApp` 249 | // runApp(AssistivePannel().floatwing(app: true)); 250 | } 251 | ``` 252 | 253 | 2. Start window with `entry` as config. 254 | ```dart 255 | void _startWindow() { 256 | // define window config and start the window from config. 257 | WindowConfig(entry: "myOverlayMain") 258 | .to() // create a window object 259 | .create(start: true); // create the window and start the overlay window. 260 | } 261 | ``` 262 | 263 | ### Wrap your widget 264 | 265 | For simple widget, you have no need to do with your widget. 266 | But if you want more functions and make your code clean, 267 | we provide a injector for your widget. 268 | 269 | For now there are some functions listed below, 270 | - Auto resize the window view. 271 | - Auto sync and ensure the window. 272 | - Wrap a `MaterialApp` 273 | - *more features are coming* 274 | 275 | Before, we write our overlay main function, like below, 276 | ```dart 277 | void overlayMain() { 278 | runApp(MaterialApp( 279 | home: MyOverView(), 280 | )) 281 | } 282 | ``` 283 | 284 | Now, we can code simply, 285 | ```dart 286 | void overlayMain() { 287 | runApp(MyOverView().floatwing(app: true))) 288 | } 289 | ``` 290 | 291 | We can wrap to a `Widget` and a `WidgetBuilder`, 292 | wrap the `WidgetBuilder`, we can access the window instance 293 | with `Window.of(context)`, while `FloatwingPlugin().currentWindow` 294 | is the only to get window instance for wrap `Widget`. 295 | 296 | If we want to access the window with `Window.of(context)`, 297 | change the code like below, 298 | ```dart 299 | void overlayMain() { 300 | runApp(((_)=>MyOverView()).floatwing(app: true).make())) 301 | } 302 | ``` 303 | 304 | ### Access window in overlay window 305 | 306 | In your window engine, you can access the window object in 2 ways: 307 | - Directly access the cache field of plugin: `FloatwingPlugin().currentWindow`. 308 | - If widget injected by `.floatwing()`, you can take window with `Window.of(context)`. 309 | 310 | `FloatwingPlugin().currentWindow` will return `null` unless initialize has been completed. 311 | 312 | If you inject `WidgetBuilder` with `.floatwing()`, 313 | then you can access the current window. 314 | It will always return non-value, unless you enable debug with `.floatwing(debug: true)`. 315 | 316 | For example, if we want to get the `id` of current window, 317 | we can do it like below: 318 | ```dart 319 | /// ... 320 | import 'package:flutter_floatwing/flutter_floatwing.dart'; 321 | 322 | class _ExampleViewState extends State { 323 | Window? w; 324 | 325 | @override 326 | void initState() { 327 | super.initState(); 328 | SchedulerBinding.instance?.addPostFrameCallback((_) { 329 | w = Window.of(context); 330 | print("my window id is ${w.id}"); 331 | }); 332 | } 333 | } 334 | ``` 335 | 336 | ### Subscribe events 337 | 338 | We can subscribe events from windows, and trigger actions when events fired. 339 | Events of window will be sent to `main engine`, self `window engine` and the parent `window engine. 340 | Which means you can subscribe events of window from flutter of main application, 341 | overlay window and parent overlay window. 342 | 343 | Currently we support events for window lifecycle and drag action. 344 | ```dart 345 | enum EventType { 346 | WindowCreated, 347 | WindowStarted, 348 | WindowPaused, 349 | WindowResumed, 350 | WindowDestroy, 351 | 352 | WindowDragStart, 353 | WindowDragging, 354 | WindowDragEnd, 355 | } 356 | ``` 357 | 358 | *More events type are coming, and contributtions are welcome!* 359 | 360 | For example, we want to do something when the window is started, 361 | we can code like below: 362 | ```dart 363 | @override 364 | void initState() { 365 | super.initState(); 366 | 367 | SchedulerBinding.instance?.addPostFrameCallback((_) { 368 | w = Window.of(context); 369 | w?.on(EventType.WindowStarted, (window, _) { 370 | print("$w has been started."); 371 | }).on(EventType.WindowDestroy, (window, data) { 372 | // data is a boolean value, which means that the window 373 | // are destroyed force or not. 374 | print("$w has been destroy, force $data"); 375 | }); 376 | }); 377 | } 378 | ``` 379 | 380 | ### Share data with windows 381 | 382 | Sharing data is the only way to communicate with windows. 383 | We provide a simple way to do this: `window.share(data)`. 384 | 385 | For example, if you want to share data to overlay window from main application. 386 | 387 | First get the target window in main application, 388 | usually the created one can be used or you can get one from `windows` cache by `id`, 389 | ```dart 390 | 391 | Window w; 392 | 393 | void _startWindow() { 394 | w = WindowConfig(route: "/my-overlay-window").to(); 395 | } 396 | 397 | void _shareData(dynamic data) { 398 | w.share(data).then((value) { 399 | // and window can return value. 400 | }); 401 | // or just take one from cache 402 | // FloatwingPlugin().windows["default"]?.share(data); 403 | } 404 | ``` 405 | 406 | If you want to share data with a name, yon can add the name parameter: 407 | ``w.share(data, name="name-1")`. 408 | 409 | And then you should listen the data in window by register the data handler. 410 | ```dart 411 | @override 412 | void initState() { 413 | super.initState(); 414 | 415 | SchedulerBinding.instance?.addPostFrameCallback((_) { 416 | w = Window.of(context); 417 | w?.onData((source, name, data) async { 418 | print("get $name data from $source: $data"); 419 | }); 420 | }); 421 | } 422 | ``` 423 | 424 | The function signature of handler is `Future Function(String? source, String? name, dynamic data)`. 425 | - `source` is where the data comes from, `null` if from main application, from window will be the `id` of window. 426 | - `name` is the data name, you can share data for different purposes. 427 | - `data` is the data that you get. 428 | - return some value if you want to do. 429 | 430 | There are restrictions for directions of communication, unless you send data to self, which will not be allowed. Which means you can send data as long as you know the id of window. *Currently share to main application is not implemented.* 431 | 432 | **Note: The data you are sharing should be serializable.** 433 | 434 | ## API References 435 | 436 | ### `FloatwingPlugin` instance 437 | 438 | `FloatwingPlugin` is a singleton class that returns the same instance every time it called `FloatwingPlugin()` factory method. 439 | 440 | 441 | 442 | ### `WindowConfig` Object 443 | 444 | *TODO* 445 | 446 | ### `Window` Object 447 | 448 | *TODO* 449 | 450 | ### Events 451 | 452 | #### Window lifecycle 453 | 454 | #### Action 455 | 456 | 457 | *More events type are coming, and comtributions are welcome!* 458 | 459 | ## Support 460 | 461 | Did you find this plugin useful? Please consider to make a donation to help improve it! 462 | 463 | ## Contributing 464 | 465 | Contributions are always welcome! 466 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'im.zoe.labs.flutter_floatwing' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.8.22' 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:8.0.2' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | 27 | android { 28 | namespace 'im.zoe.labs.flutter_floatwing' 29 | compileSdkVersion 28 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | test.java.srcDirs += 'src/test/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | minSdkVersion 16 47 | } 48 | lintOptions { 49 | disable 'InvalidPackage' 50 | } 51 | } 52 | 53 | dependencies { 54 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 55 | } 56 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | package android 2 | 3 | rootProject.name = 'flutter_floatwing' 4 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/kotlin/im/zoe/labs/flutter_floatwing/FloatWindow.kt: -------------------------------------------------------------------------------- 1 | package im.zoe.labs.flutter_floatwing 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.graphics.PixelFormat 7 | import android.os.Build 8 | import android.util.Log 9 | import android.view.Gravity 10 | import android.view.MotionEvent 11 | import android.view.View 12 | import android.view.WindowManager 13 | import android.view.WindowManager.LayoutParams 14 | import android.view.WindowManager.LayoutParams.* 15 | import io.flutter.embedding.android.FlutterTextureView 16 | import io.flutter.embedding.android.FlutterView 17 | import io.flutter.embedding.engine.FlutterEngine 18 | import io.flutter.embedding.engine.FlutterEngineCache 19 | import io.flutter.plugin.common.BasicMessageChannel 20 | import io.flutter.plugin.common.JSONMessageCodec 21 | import io.flutter.plugin.common.MethodCall 22 | import io.flutter.plugin.common.MethodChannel 23 | 24 | @SuppressLint("ClickableViewAccessibility") 25 | class FloatWindow( 26 | context: Context, 27 | wmr: WindowManager, 28 | engKey: String, 29 | eng: FlutterEngine, 30 | cfg: Config): View.OnTouchListener, MethodChannel.MethodCallHandler, 31 | BasicMessageChannel.MessageHandler { 32 | 33 | var parent: FloatWindow? = null 34 | 35 | var config = cfg 36 | 37 | var key: String = "default" 38 | 39 | var engineKey = engKey 40 | var engine = eng 41 | 42 | var wm = wmr 43 | 44 | var subscribedEvents: HashMap = HashMap() 45 | 46 | var view: FlutterView = FlutterView(context, FlutterTextureView(context)) 47 | 48 | lateinit var layoutParams: LayoutParams 49 | 50 | lateinit var service: FloatwingService 51 | 52 | // method and message channel for window engine call 53 | var _channel: MethodChannel = MethodChannel(eng.dartExecutor.binaryMessenger, 54 | "${FloatwingService.METHOD_CHANNEL}/window").also { 55 | it.setMethodCallHandler(this) } 56 | var _message: BasicMessageChannel = BasicMessageChannel(eng.dartExecutor.binaryMessenger, 57 | "${FloatwingService.MESSAGE_CHANNEL}/window_msg", JSONMessageCodec.INSTANCE) 58 | .also { it.setMessageHandler(this) } 59 | 60 | var _started = false 61 | 62 | fun init(): FloatWindow { 63 | layoutParams = config.to() 64 | 65 | config.focusable?.let{ 66 | view.isFocusable = it 67 | view.isFocusableInTouchMode = it 68 | } 69 | 70 | view.setBackgroundColor(Color.TRANSPARENT) 71 | view.fitsSystemWindows = true 72 | 73 | config.visible?.let{ setVisible(it) } 74 | 75 | view.setOnTouchListener(this) 76 | 77 | // view.attachToFlutterEngine(engine) 78 | return this 79 | } 80 | 81 | fun destroy(force: Boolean = true): Boolean { 82 | Log.i(TAG, "[window] destroy window: $key force: $force") 83 | 84 | // remote from manager must be first 85 | if (_started) wm.removeView(view) 86 | 87 | view.detachFromFlutterEngine() 88 | 89 | 90 | // TODO: should we stop the engine for flutter? 91 | if (force) { 92 | // stop engine and remove from cache 93 | FlutterEngineCache.getInstance().remove(engineKey) 94 | engine.destroy() 95 | service.windows.remove(key) 96 | emit("destroy", null) 97 | } else { 98 | _started = false 99 | engine.lifecycleChannel.appIsPaused() 100 | emit("paused", null) 101 | } 102 | return true 103 | } 104 | 105 | fun setVisible(visible: Boolean = true): Boolean { 106 | Log.d(TAG, "[window] set window $key => $visible") 107 | emit("visible", visible) 108 | view.visibility = if (visible) View.VISIBLE else View.GONE 109 | return visible 110 | } 111 | 112 | fun update(cfg: Config): Map { 113 | Log.d(TAG, "[window] update window $key => $cfg") 114 | config = config.update(cfg).also { 115 | layoutParams = it.to() 116 | if (_started) wm.updateViewLayout(view, layoutParams) 117 | } 118 | return toMap() 119 | } 120 | 121 | fun start(): Boolean { 122 | if (_started) { 123 | Log.d(TAG, "[window] window $key already started") 124 | return true 125 | } 126 | 127 | _started = true 128 | Log.d(TAG, "[window] start window: $key") 129 | 130 | engine.lifecycleChannel.appIsResumed() 131 | 132 | // if engine is paused, send re-render message 133 | // make sure reuse engine can be re-render 134 | emit("resumed") 135 | 136 | view.attachToFlutterEngine(engine) 137 | 138 | wm.addView(view, layoutParams) 139 | 140 | emit("started") 141 | 142 | return true 143 | } 144 | 145 | fun shareData(data: Map<*, *>, source: String? = null, result: MethodChannel.Result? = null) { 146 | shareData(_channel, data, source, result) 147 | } 148 | 149 | fun simpleEmit(msgChannel: BasicMessageChannel, name: String, data: Any?=null) { 150 | val map = HashMap() 151 | map["name"] = name 152 | map["id"] = key // this is special for main engine 153 | map["data"] = data 154 | msgChannel.send(map) 155 | } 156 | 157 | fun emit(name: String, data: Any? = null, prefix: String?="window", pluginNeed: Boolean = true) { 158 | val evtName = "$prefix.$name" 159 | // Log.i(TAG, "[window] emit event: Window[$key] $name ") 160 | 161 | // check if need to send to my self 162 | if (true||subscribedEvents.containsKey(name)||subscribedEvents.containsKey("*")) { 163 | // emit to window engine 164 | simpleEmit(_message, evtName, data) 165 | } 166 | 167 | // plugin 168 | // check if we need to fire to plugin 169 | if (pluginNeed&&(true||service.subscribedEvents.containsKey("*")||service.subscribedEvents.containsKey(evtName))) { 170 | simpleEmit(service._message, evtName, data) 171 | } 172 | 173 | // emit parent engine 174 | // if fire to parent need have no need to fire to service again 175 | if(parent!=null&&parent!=this) { 176 | parent!!.simpleEmit(parent!!._message, evtName, data) 177 | } 178 | 179 | // _channel.invokeMethod("window.$name", data) 180 | // we need to send to man engine 181 | // service._channel.invokeMethod("window.$name", key) 182 | } 183 | 184 | fun toMap(): Map { 185 | // must not null if success created 186 | val map = HashMap() 187 | map["id"] = key 188 | map["pixelRadio"] = service.pixelRadio 189 | map["system"] = service.systemConfig 190 | map["config"] = config.toMap().filter { it.value != null } 191 | return map 192 | } 193 | 194 | override fun toString(): String { 195 | return "${toMap()}" 196 | } 197 | 198 | // return window from svc.windows by id 199 | fun take(id: String): FloatWindow? { 200 | return service.windows[id] 201 | } 202 | 203 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 204 | return when (call.method) { 205 | // just take current engine's window 206 | "window.sync" -> { 207 | // when flutter is ready should call this to sync the window object. 208 | Log.i(TAG, "[window] window.sync from flutter side: $key") 209 | result.success(toMap()) 210 | } 211 | 212 | // we need to support call window.* in window engine 213 | // but the window engine register as window channel 214 | // so we should take the id first and then get window from windows cache 215 | // TODO: those code should move to service 216 | 217 | "window.create_child" -> { 218 | val id = call.argument("id") ?: "default" 219 | val cfg = call.argument>("config")!! 220 | val start = call.argument("start") ?: false 221 | val config = FloatWindow.Config.from(cfg) 222 | Log.d(TAG, "[service] window.create_child request_id: $id") 223 | return result.success(FloatwingService.createWindow(service.applicationContext, id, 224 | config, start, this)) 225 | } 226 | "window.close" -> { 227 | val id = call.argument("id")?:"" 228 | Log.d(TAG, "[window] window.close request_id: $id, my_id: $key") 229 | val force = call.argument("force") ?: false 230 | return result.success(take(id)?.destroy(force)) 231 | } 232 | "window.destroy" -> { 233 | val id = call.argument("id")?:"" 234 | Log.d(TAG, "[window] window.destroy request_id: $id, my_id: $key") 235 | return result.success(take(id)?.destroy(true)) 236 | } 237 | "window.start" -> { 238 | val id = call.argument("id")?:"" 239 | Log.d(TAG, "[window] window.start request_id: $id, my_id: $key") 240 | return result.success(take(id)?.start()) 241 | } 242 | "window.update" -> { 243 | val id = call.argument("id")?:"" 244 | Log.d(TAG, "[window] window.update request_id: $id, my_id: $key") 245 | val config = Config.from(call.argument>("config")!!) 246 | return result.success(take(id)?.update(config)) 247 | } 248 | "window.show" -> { 249 | val id = call.argument("id")?:"" 250 | Log.d(TAG, "[window] window.show request_id: $id, my_id: $key") 251 | val visible = call.argument("visible") ?: true 252 | return result.success(take(id)?.setVisible(visible)) 253 | } 254 | "window.launch_main" -> { 255 | Log.d(TAG, "[window] window.launch_main") 256 | return result.success(service.launchMainActivity()) 257 | } 258 | "window.lifecycle" -> { 259 | 260 | } 261 | "event.subscribe" -> { 262 | val id = call.argument("id")?:"" 263 | 264 | } 265 | "data.share" -> { 266 | // communicate with other window, only 1 - 1 with id 267 | val args = call.arguments as Map<*, *> 268 | val targetId = call.argument("target") 269 | Log.d(TAG, "[window] share data from $key with $targetId: $args") 270 | if (targetId == null) { 271 | Log.d(TAG, "[window] share data with plugin") 272 | return result.success(shareData(service._channel, args, source=key, result=result)) 273 | } 274 | if (targetId == key) { 275 | Log.d(TAG, "[window] can't share data with self") 276 | return result.error("no allow", "share data from $key to $targetId", "") 277 | } 278 | val target = service.windows[targetId] 279 | ?: return result.error("not found", "target window $targetId not exits", ""); 280 | return target.shareData(args, source=key, result=result) 281 | } 282 | else -> { 283 | result.notImplemented() 284 | } 285 | } 286 | } 287 | 288 | override fun onMessage(msg: Any?, reply: BasicMessageChannel.Reply) { 289 | // stream message 290 | } 291 | 292 | companion object { 293 | private const val TAG = "FloatWindow" 294 | 295 | fun shareData(channel: MethodChannel, data: Map<*, *>, source: String? = null, 296 | result: MethodChannel.Result? = null): Any? { 297 | // id is the data comes from 298 | // invoke the method channel 299 | val map = HashMap() 300 | map["source"] = source 301 | data.forEach { map[it.key as String] = it.value } 302 | channel.invokeMethod("data.share", map, result) 303 | // how to get data back 304 | return null 305 | } 306 | } 307 | 308 | // window is dragging 309 | private var dragging = false 310 | 311 | // start point 312 | private var lastX = 0f 313 | private var lastY = 0f 314 | 315 | // border around 316 | // TODO: support generate around edge 317 | 318 | override fun onTouch(view: View?, event: MotionEvent?): Boolean { 319 | // default draggable should be false 320 | if (config.draggable != true) return false 321 | when (event?.action) { 322 | MotionEvent.ACTION_DOWN -> { 323 | // touch start 324 | dragging = false 325 | lastX = event.rawX 326 | lastY = event.rawY 327 | // TODO: support generate around edge 328 | } 329 | MotionEvent.ACTION_MOVE -> { 330 | // touch move 331 | val dx = event.rawX - lastX 332 | val dy = event.rawY - lastY 333 | 334 | // ignore too small fist start moving(some time is click) 335 | if (!dragging && dx*dx+dy*dy < 25) { 336 | return false 337 | } 338 | 339 | // update the last point 340 | lastX = event.rawX 341 | lastY = event.rawY 342 | 343 | val xx = layoutParams.x + dx.toInt() 344 | val yy = layoutParams.y + dy.toInt() 345 | 346 | if (!dragging) { 347 | // first time dragging 348 | emit("drag_start", listOf(xx, yy)) 349 | } 350 | 351 | dragging = true 352 | // update x, y, need to update config so use config to update 353 | update(Config().apply { 354 | // calculate with the border 355 | x = xx 356 | y = yy 357 | }) 358 | 359 | emit("dragging", listOf(xx, yy)) 360 | } 361 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { 362 | // touch end 363 | if (dragging) emit("drag_end", listOf(event.rawX, event.rawY)) 364 | return dragging 365 | } 366 | else -> { 367 | return false 368 | } 369 | } 370 | return false 371 | } 372 | 373 | class Config { 374 | // this three fields can not be changed 375 | // var id: String = "default" 376 | var entry: String? = null 377 | var route: String? = null 378 | var callback: Long? = null 379 | 380 | var autosize: Boolean? = null 381 | 382 | var width: Int? = null 383 | var height: Int? = null 384 | var x: Int? = null 385 | var y: Int? = null 386 | 387 | var format: Int? = null 388 | var gravity: Int? = null 389 | var type: Int? = null 390 | 391 | var clickable: Boolean? = null 392 | var draggable: Boolean? = null 393 | var focusable: Boolean? = null 394 | 395 | var immersion: Boolean? = null 396 | 397 | var visible: Boolean? = null 398 | 399 | 400 | // inline fun to(): T { 401 | fun to(): LayoutParams { 402 | val cfg = this 403 | return LayoutParams().apply { 404 | // set size 405 | width = cfg.width ?: 1 // we must have 1 pixel, let flutter can generate the pixel radio 406 | height = cfg.height ?: 1 // we must have 1 pixel, let flutter can generate the pixel radio 407 | 408 | // set position fixed if with (x, y) 409 | cfg.x?.let { x = it } // default not set 410 | cfg.y?.let { y = it } // default not set 411 | 412 | // format 413 | format = cfg.format ?: PixelFormat.TRANSPARENT 414 | 415 | // default start from center 416 | gravity = cfg.gravity ?: Gravity.TOP or Gravity.LEFT 417 | 418 | // default flags 419 | flags = FLAG_LAYOUT_IN_SCREEN or FLAG_NOT_TOUCH_MODAL 420 | // if immersion add flag no limit 421 | cfg.immersion?.let{ if (it) flags = flags or FLAG_LAYOUT_NO_LIMITS } 422 | // default we should be clickable 423 | // if not clickable, add flag not touchable 424 | cfg.clickable?.let{ if (!it) flags = flags or FLAG_NOT_TOUCHABLE } 425 | // default we should be no focusable 426 | if (cfg.focusable == null) { cfg.focusable = false } 427 | // if not focusable, add no focusable flag 428 | cfg.focusable?.let { if (!it) flags = flags or FLAG_NOT_FOCUSABLE } 429 | 430 | // default type is overlay 431 | type = cfg.type ?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) TYPE_APPLICATION_OVERLAY else TYPE_PHONE 432 | } 433 | } 434 | 435 | fun toMap(): Map { 436 | val map = HashMap() 437 | map["entry"] = entry 438 | map["route"] = route 439 | map["callback"] = callback 440 | 441 | map["autosize"] = autosize 442 | 443 | map["width"] = width 444 | map["height"] = height 445 | map["x"] = x 446 | map["y"] = y 447 | 448 | map["format"] = format 449 | map["gravity"] = gravity 450 | map["type"] = type 451 | 452 | map["clickable"] = clickable 453 | map["draggable"] = draggable 454 | map["focusable"] = focusable 455 | 456 | map["immersion"] = immersion 457 | 458 | map["visible"] = visible 459 | 460 | return map 461 | } 462 | 463 | fun update(cfg: Config): Config { 464 | // entry, route, callback shouldn't be updated 465 | 466 | cfg.autosize?.let { autosize = it } 467 | 468 | cfg.width?.let { width = it } 469 | cfg.height?.let { height = it } 470 | cfg.x?.let { x = it } 471 | cfg.y?.let { y = it } 472 | 473 | cfg.format?.let { format = it } 474 | cfg.gravity?.let { gravity = it } 475 | cfg.type?.let { type = it } 476 | 477 | cfg.clickable?.let{ clickable = it } 478 | cfg.draggable?.let { draggable = it } 479 | cfg.focusable?.let { focusable = it } 480 | 481 | cfg.immersion?.let { immersion = it } 482 | 483 | cfg.visible?.let { visible = it } 484 | 485 | return this 486 | } 487 | 488 | override fun toString(): String { 489 | val map = toMap()?.filter { it.value != null } 490 | return "$map" 491 | } 492 | 493 | companion object { 494 | 495 | fun from(data: Map): Config { 496 | val cfg = Config() 497 | 498 | // (data["id"]?.let { it as String } ?: "default").also { cfg.id = it } 499 | cfg.entry = data["entry"] as String? 500 | cfg.route = data["route"] as String? 501 | 502 | val int_callback = data["callback"] as Number? 503 | cfg.callback = int_callback?.toLong() 504 | 505 | cfg.autosize = data["autosize"] as Boolean? 506 | 507 | cfg.width = data["width"] as Int? 508 | cfg.height = data["height"] as Int? 509 | cfg.x = data["x"] as Int? 510 | cfg.y = data["y"] as Int? 511 | 512 | cfg.gravity = data["gravity"] as Int? 513 | cfg.format = data["format"] as Int? 514 | cfg.type = data["type"] as Int? 515 | 516 | cfg.clickable = data["clickable"] as Boolean? 517 | cfg.draggable = data["draggable"] as Boolean? 518 | cfg.focusable = data["focusable"] as Boolean? 519 | 520 | cfg.immersion = data["immersion"] as Boolean? 521 | 522 | cfg.visible = data["visible"] as Boolean? 523 | 524 | return cfg 525 | } 526 | } 527 | } 528 | 529 | } 530 | -------------------------------------------------------------------------------- /android/src/main/kotlin/im/zoe/labs/flutter_floatwing/FloatwingService.kt: -------------------------------------------------------------------------------- 1 | package im.zoe.labs.flutter_floatwing 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Activity 5 | import android.app.NotificationChannel 6 | import android.app.NotificationManager 7 | import android.app.Service 8 | import android.content.Context 9 | import android.content.Intent 10 | import android.content.Intent.ACTION_SHUTDOWN 11 | import android.os.Build 12 | import android.os.IBinder 13 | import android.os.PowerManager 14 | import android.util.Log 15 | import android.view.WindowManager 16 | import androidx.core.app.NotificationCompat 17 | import im.zoe.labs.flutter_floatwing.Utils.Companion.toMap 18 | import io.flutter.FlutterInjector 19 | import io.flutter.embedding.engine.FlutterEngine 20 | import io.flutter.embedding.engine.FlutterEngineCache 21 | import io.flutter.embedding.engine.FlutterEngineGroup 22 | import io.flutter.embedding.engine.dart.DartExecutor 23 | import io.flutter.plugin.common.BasicMessageChannel 24 | import io.flutter.plugin.common.JSONMessageCodec 25 | import io.flutter.plugin.common.MethodCall 26 | import io.flutter.plugin.common.MethodChannel 27 | import io.flutter.view.FlutterCallbackInformation 28 | import org.json.JSONObject 29 | import java.lang.Exception 30 | 31 | class FloatwingService : MethodChannel.MethodCallHandler, BasicMessageChannel.MessageHandler, Service() { 32 | 33 | private lateinit var mContext: Context 34 | private lateinit var windowManager: WindowManager 35 | 36 | private lateinit var engGroup: FlutterEngineGroup 37 | 38 | lateinit var _channel: MethodChannel 39 | lateinit var _message: BasicMessageChannel 40 | 41 | var subscribedEvents: HashMap = HashMap() 42 | 43 | var pixelRadio = 2.0 44 | var systemConfig = emptyMap() 45 | 46 | // store the window object use the id as key 47 | val windows = HashMap() 48 | 49 | override fun onCreate() { 50 | super.onCreate() 51 | 52 | // set the instance 53 | instance = this 54 | 55 | mContext = applicationContext 56 | 57 | engGroup = FlutterEngineGroup(mContext) 58 | 59 | Log.i(TAG, "[service] the background service onCreate") 60 | 61 | // get the window manager and store 62 | (getSystemService(WINDOW_SERVICE) as WindowManager).also { windowManager = it } 63 | 64 | // load pixel from store 65 | pixelRadio = mContext.getSharedPreferences(FlutterFloatwingPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) 66 | .getFloat(FlutterFloatwingPlugin.PIXEL_RADIO_KEY, 2F).toDouble() 67 | Log.d(TAG, "[service] load the pixel radio: $pixelRadio") 68 | 69 | // load system config from store 70 | try { 71 | val str = mContext.getSharedPreferences(FlutterFloatwingPlugin.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) 72 | .getString(FlutterFloatwingPlugin.SYSTEM_CONFIG_KEY, "{}") 73 | val map = JSONObject(str) 74 | systemConfig = map.toMap() 75 | }catch (e: Exception) { 76 | e.printStackTrace() 77 | } 78 | 79 | // install this method channel for the main engine 80 | FlutterEngineCache.getInstance().get(FlutterFloatwingPlugin.FLUTTER_ENGINE_CACHE_KEY) 81 | ?.also { 82 | Log.d(TAG, "[service] install the service handler for main engine") 83 | installChannel(it) 84 | } 85 | } 86 | 87 | override fun onBind(p0: Intent?): IBinder? { 88 | return null 89 | } 90 | 91 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 92 | when (intent?.action) { 93 | ACTION_SHUTDOWN -> { 94 | (getSystemService(Context.POWER_SERVICE) as PowerManager).run { 95 | newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply { 96 | if (isHeld) release() 97 | } 98 | } 99 | Log.d(TAG, "[service] stop the background service!") 100 | stopSelf() 101 | } 102 | else -> { 103 | 104 | } 105 | } 106 | return START_STICKY 107 | } 108 | 109 | override fun onDestroy() { 110 | super.onDestroy() 111 | 112 | // clean up: remove all views in the window manager 113 | windows.forEach { 114 | it.value.destroy() 115 | Log.d(TAG, "[service] service destroy: remove the float window ${it.key}") 116 | } 117 | 118 | } 119 | 120 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 121 | when (call.method) { 122 | "service.stop_service" -> { 123 | Log.d(TAG, "[service] stop the service") 124 | val closed = stopService(Intent(baseContext, this.javaClass)) 125 | return result.success(closed) 126 | } 127 | "service.promote" -> { 128 | Log.d(TAG, "[service] promote service") 129 | val map = call.arguments as Map<*, *>? 130 | return result.success(promoteService(map)) 131 | } 132 | "service.demote" -> { 133 | Log.d(TAG, "[service] demote service") 134 | return result.success(demoteService()) 135 | } 136 | "service.create_window" -> { 137 | val id = call.argument("id") ?: "default" 138 | val cfg = call.argument>("config")!! 139 | val start = call.argument("start") ?: false 140 | val config = FloatWindow.Config.from(cfg) 141 | Log.d(TAG, "[service] window.create request_id: $id") 142 | return result.success(createWindow(mContext, id, config, start, null)) 143 | } 144 | 145 | // call for windows 146 | "window.close" -> { 147 | val id = call.argument("id")!! 148 | Log.d(TAG, "[service] window.close request_id: $id") 149 | val force = call.argument("force") ?: false 150 | return result.success(windows[id]?.destroy(force)) 151 | } 152 | "window.start" -> { 153 | val id = call.argument("id") ?: "default" 154 | Log.d(TAG, "[service] window.start request_id: $id ${windows[id]}") 155 | return result.success(windows[id]?.start()) 156 | } 157 | "window.show" -> { 158 | val id = call.argument("id")!! 159 | val visible = call.argument("visible") ?: true 160 | Log.d(TAG, "[service] window.show request_id: $id") 161 | return result.success(windows[id]?.setVisible(visible)) 162 | } 163 | "window.update" -> { 164 | val id = call.argument("id")!! 165 | Log.d(TAG, "[service] window.update request_id: $id") 166 | val config = FloatWindow.Config.from(call.argument>("config")!!) 167 | return result.success(windows[id]?.update(config)) 168 | } 169 | "window.sync" -> { 170 | Log.d(TAG, "[service] fake window.sync") 171 | return result.success(null) 172 | } 173 | "data.share" -> { 174 | // communicate with other window, only 1 - 1 with id 175 | val args = call.arguments as Map<*, *> 176 | val targetId = call.argument("target") 177 | Log.d(TAG, "[service] share data from with $targetId: $args") 178 | if (targetId == null) { 179 | Log.d(TAG, "[service] can't share data with self") 180 | return result.error("no allow", "share data from plugin to plugin", "") 181 | } 182 | val target = windows[targetId] 183 | ?: return result.error("not found", "target window $targetId not exits", ""); 184 | return target.shareData(args, result=result) 185 | } 186 | else -> { 187 | Log.d(TAG, "[service] unknown method ${call.method}") 188 | result.notImplemented() 189 | } 190 | } 191 | } 192 | 193 | override fun onMessage(message: Any?, reply: BasicMessageChannel.Reply) { 194 | // update the windows from message 195 | } 196 | 197 | private fun promoteService(map: Map<*, *>?): Boolean { 198 | Log.i(TAG, "[service] promote service to foreground") 199 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { 200 | Log.e(TAG, "[service] promoteToForeground need sdk >= 26") 201 | return false 202 | } 203 | 204 | if (map == null) { 205 | Log.e(TAG, "[service] promote service config is null") 206 | return false 207 | } 208 | 209 | /* 210 | (getSystemService(Context.POWER_SERVICE) as PowerManager).run { 211 | newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG).apply { 212 | setReferenceCounted(false) 213 | acquire() 214 | } 215 | } 216 | */ 217 | 218 | val title = map["title"] as String? ?: "Floatwing Service" 219 | val description = map["description"] as String? ?: "Floatwing service is running" 220 | val showWhen = map["showWhen"] as Boolean? ?: false 221 | val ticker = map["ticker"] as String? 222 | val subText = map["subText"] as String? 223 | 224 | 225 | val channel = NotificationChannel("flutter_floatwing", "Floatwing Service", NotificationManager.IMPORTANCE_HIGH) 226 | val imageId = resources.getIdentifier("ic_launcher", "mipmap", packageName) 227 | (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel) 228 | 229 | val notification = NotificationCompat.Builder(this, "flutter_floatwing") 230 | .setContentTitle(title) 231 | .setContentText(description) 232 | .setShowWhen(showWhen) 233 | .setTicker(ticker) 234 | .setSubText(subText) 235 | .setSmallIcon(imageId) 236 | .setPriority(NotificationCompat.PRIORITY_HIGH) 237 | .setCategory(NotificationCompat.CATEGORY_SERVICE) 238 | .build() 239 | 240 | startForeground(1, notification) 241 | return true 242 | } 243 | 244 | private fun demoteService(): Boolean { 245 | Log.i(TAG, "[service] demote service to background") 246 | stopForeground(true) 247 | return true 248 | } 249 | 250 | fun launchMainActivity(): Boolean { 251 | if (mActivityClass == null) { 252 | Log.e(TAG, "[service] the main activity is null, maybe the service start from background") 253 | return false 254 | } 255 | Log.d(TAG, "[service] launch the main activity") 256 | val intent = Intent(this, mActivityClass) 257 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 258 | startActivity(intent) 259 | return true 260 | } 261 | 262 | private fun createWindow(id: String, config: FloatWindow.Config, start: Boolean = false, 263 | p: FloatWindow?): Map? { 264 | // check if id exits 265 | if (windows.contains(id)) { 266 | Log.e(TAG, "[service] window with id $id exits") 267 | return null 268 | } 269 | 270 | // get flutter engine 271 | val fKey = id.flutterKey() 272 | val (eng, fromCache) = getFlutterEngine(fKey, config.entry, config.route, config.callback) 273 | 274 | val svc = this 275 | return FloatWindow(mContext, windowManager, fKey, eng, config).apply { 276 | key = id 277 | service = svc 278 | parent = p 279 | Log.d(TAG, "[service] set window as handler $METHOD_CHANNEL/window for $eng") 280 | }.init().also { 281 | Log.d(TAG, "[service] created window: $id $config") 282 | it.emit("created", !fromCache) 283 | windows[it.key] = it 284 | if (start) it.start() 285 | }.toMap() 286 | } 287 | 288 | // this function is useful when we want to start service automatically 289 | private fun getFlutterEngine(key: String, entryName: String?, route: String?, callback: Long?): Pair { 290 | // first take from cache 291 | var eng = FlutterEngineCache.getInstance().get(key) 292 | if (eng != null) { 293 | Log.i(TAG, "[service] use the flutter exits in cache, id: $key") 294 | return Pair(eng, true) 295 | } 296 | 297 | Log.d(TAG, "[service] miss from cache need to create a new flutter engine") 298 | 299 | // then create a flutter engine 300 | 301 | // ensure initialization 302 | // FlutterInjector.instance().flutterLoader().startInitialization(mContext) 303 | // FlutterInjector.instance().flutterLoader().ensureInitializationComplete(mContext, arrayOf()) 304 | 305 | // first let's use callback to start engine first 306 | if (callback!=null&&callback>0L) { 307 | Log.i(TAG, "[service] start flutter engine, id: $key callback: $callback") 308 | 309 | eng = FlutterEngine(mContext) 310 | val info = FlutterCallbackInformation.lookupCallbackInformation(callback) 311 | val args = DartExecutor.DartCallback(mContext.assets, FlutterInjector.instance().flutterLoader().findAppBundlePath(), info) 312 | // execute the callback function 313 | eng.dartExecutor.executeDartCallback(args) 314 | 315 | // store the engine to cache 316 | FlutterEngineCache.getInstance().put(key, eng) 317 | 318 | return Pair(eng, false) 319 | } 320 | 321 | var entry = entryName 322 | if (entry==null) { 323 | // try use the main entrypoint 324 | entry = "main" 325 | Log.w(TAG, "[service] recommend to use a entrypoint") 326 | } 327 | 328 | // check the main and default route 329 | if (entry == "main" && route == null) { 330 | Log.w(TAG, "[service] use the main entrypoint and default route") 331 | } 332 | 333 | Log.i(TAG, "[service] start flutter engine, id: $key entrypoint: $entry, route: $route") 334 | 335 | // make sure the entrypoint exits 336 | val entrypoint = DartExecutor.DartEntrypoint( 337 | FlutterInjector.instance().flutterLoader().findAppBundlePath(), entry) 338 | 339 | // start the dart executor with special entrypoint 340 | eng = engGroup.createAndRunEngine(mContext, entrypoint, route) 341 | 342 | // store the engine to cache 343 | FlutterEngineCache.getInstance().put(key, eng) 344 | 345 | return Pair(eng, false) 346 | } 347 | 348 | // window engine won't call this, so just window method 349 | private fun installChannel(eng: FlutterEngine): Boolean { 350 | Log.d(TAG, "[service] set service as handler $METHOD_CHANNEL/window for $eng") 351 | // set the method and message channel 352 | // this must be same as window, because we use the same method to call invoke 353 | _channel = MethodChannel(eng.dartExecutor.binaryMessenger, 354 | "$METHOD_CHANNEL/window").also { it.setMethodCallHandler(this) } 355 | _message = BasicMessageChannel(eng.dartExecutor.binaryMessenger, 356 | "$METHOD_CHANNEL/window_msg", JSONMessageCodec.INSTANCE).also { it.setMessageHandler(this) } 357 | return true 358 | } 359 | 360 | private fun String.flutterKey(): String { 361 | return FLUTTER_ENGINE_KEY + this 362 | } 363 | 364 | companion object { 365 | @JvmStatic 366 | private val TAG = "FloatwingService" 367 | 368 | // TODO: improve 369 | @SuppressLint("StaticFieldLeak") 370 | var mActivity: Activity? = null 371 | var mActivityClass: Class? = null 372 | 373 | @SuppressLint("StaticFieldLeak") 374 | var instance: FloatwingService? = null 375 | 376 | const val WAKELOCK_TAG = "FloatwingService::WAKE_LOCK" 377 | const val FLUTTER_ENGINE_KEY = "floatwing_flutter_engine_" 378 | const val METHOD_CHANNEL = "im.zoe.labs/flutter_floatwing" 379 | const val MESSAGE_CHANNEL = "im.zoe.labs/flutter_floatwing" 380 | 381 | fun initialize(): Boolean { 382 | Log.i(TAG, "[service] initialize") 383 | return true 384 | } 385 | 386 | fun createWindow(context: Context, id: String, config: FloatWindow.Config, 387 | start: Boolean = false, parent: FloatWindow?): Map? { 388 | Log.i(TAG, "[service] create a window: $id $config") 389 | // make sure the service started 390 | if (!ensureService(context)) return null 391 | 392 | // start the window 393 | return instance?.createWindow(id, config, start, parent) 394 | } 395 | 396 | // ensure the service is started 397 | private fun ensureService(context: Context): Boolean { 398 | if (instance != null) return true 399 | 400 | 401 | // let's start the service 402 | 403 | // make sure we granted permission 404 | if (!FlutterFloatwingPlugin.permissionGiven(context)) { 405 | Log.e(TAG, "[service] don't have permission to create overlay window") 406 | return false 407 | } 408 | 409 | // start the service 410 | val intent = Intent(context, FloatwingService::class.java) 411 | context.startService(intent) 412 | 413 | // TODO: start foreground service if need 414 | 415 | // TODO: waiting for service is running use a better way 416 | while (instance==null) { 417 | Log.d(TAG, "[service] wait for service created") 418 | Thread.sleep(100 * 1) 419 | break 420 | } 421 | 422 | return true 423 | } 424 | 425 | fun onActivityAttached(activity: Activity) { 426 | Log.i(TAG, "[service] activity attached") 427 | // maybe instance is null, so set failed 428 | if (mActivity != null) { 429 | Log.w(TAG, "[service] main activity already set") 430 | return 431 | } 432 | mActivity = activity 433 | // store the class 434 | mActivityClass = mActivity?.javaClass 435 | } 436 | 437 | fun installChannel(eng: FlutterEngine): Boolean { 438 | Log.i(TAG, "[service] install the service channel for engine") 439 | return instance?.installChannel(eng) ?: false 440 | } 441 | 442 | fun isRunning(context: Context): Boolean { 443 | return Utils.getRunningService(context, FloatwingService::class.java) != null 444 | } 445 | 446 | fun start(context: Context): Boolean { 447 | return ensureService(context) 448 | } 449 | } 450 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/im/zoe/labs/flutter_floatwing/FlutterFloatwingPlugin.kt: -------------------------------------------------------------------------------- 1 | package im.zoe.labs.flutter_floatwing 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.provider.Settings 9 | import android.util.Log 10 | import androidx.annotation.NonNull; 11 | import io.flutter.embedding.engine.FlutterEngine 12 | import io.flutter.embedding.engine.FlutterEngineCache 13 | import io.flutter.embedding.engine.plugins.FlutterPlugin 14 | import io.flutter.embedding.engine.plugins.activity.ActivityAware 15 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 16 | import io.flutter.plugin.common.MethodCall 17 | import io.flutter.plugin.common.MethodChannel 18 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 19 | import io.flutter.plugin.common.MethodChannel.Result 20 | import io.flutter.plugin.common.PluginRegistry 21 | import io.flutter.plugin.common.PluginRegistry.Registrar 22 | import org.json.JSONObject 23 | import java.lang.Exception 24 | 25 | /** FlutterFloatwingPlugin */ 26 | class FlutterFloatwingPlugin: FlutterPlugin, ActivityAware, MethodCallHandler, PluginRegistry.ActivityResultListener { 27 | 28 | private lateinit var mContext: Context 29 | private lateinit var mActivity: Activity 30 | private lateinit var channel : MethodChannel 31 | private lateinit var engine: FlutterEngine 32 | private lateinit var waitPermissionResult: Result 33 | 34 | private var serviceChannelInstalled = false 35 | private var isMain = false 36 | 37 | override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { 38 | mContext = binding.applicationContext 39 | 40 | // should window's engine install method channel? 41 | channel = MethodChannel(binding.binaryMessenger, CHANNEL_NAME) 42 | channel.setMethodCallHandler(this) 43 | 44 | // how to known i'm a window engine not the main one? 45 | // if contains engine already, means we are coming from window 46 | // TODO: take first engine as main, but if service auto start the window 47 | // this will cause error 48 | if (FlutterEngineCache.getInstance().contains(FLUTTER_ENGINE_CACHE_KEY)) { 49 | Log.d(TAG, "[plugin] on attached to window engine") 50 | } else { 51 | // update the main flag 52 | isMain = true 53 | // store the flutter engine @only main 54 | engine = binding.flutterEngine 55 | FlutterEngineCache.getInstance().put(FLUTTER_ENGINE_CACHE_KEY, engine) 56 | // should install service handler for every engine? @only main 57 | // window has already set in this own logic 58 | serviceChannelInstalled = FloatwingService.installChannel(engine) 59 | .also { r -> if (!r) { 60 | MethodChannel(engine.dartExecutor.binaryMessenger, 61 | "${FloatwingService.METHOD_CHANNEL}/window").also { it.setMethodCallHandler(this) } 62 | } } 63 | 64 | Log.d(TAG, "[plugin] on attached to main engine") 65 | } 66 | } 67 | 68 | private fun saveSystemConfig(data: Map<*, *>?): Boolean { 69 | // if not exit should save 70 | val old = mContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) 71 | .getString(SYSTEM_CONFIG_KEY, null) 72 | if (old != null) { 73 | Log.d(TAG, "[plugin] system config already exits: $old") 74 | return false 75 | } 76 | 77 | FloatwingService.instance?.systemConfig = data as Map 78 | 79 | return try { 80 | val str = JSONObject(data).toString() 81 | // json encode map to string 82 | // try to save 83 | mContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit() 84 | .putString(SYSTEM_CONFIG_KEY, str) 85 | .apply() 86 | true 87 | } catch (e: Exception) { 88 | e.printStackTrace() 89 | false 90 | } 91 | } 92 | 93 | private fun savePixelRadio(pixelRadio: Double): Boolean { 94 | val old = mContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) 95 | .getFloat(PIXEL_RADIO_KEY, 0F) 96 | if (old > 1F) { 97 | Log.d(TAG, "[plugin] pixel radio already exits") 98 | return false 99 | } 100 | 101 | FloatwingService.instance?.pixelRadio = pixelRadio 102 | 103 | // we need to save pixel radio 104 | Log.d(TAG, "[plugin] pixel radio need to be saved") 105 | mContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit() 106 | .putFloat(PIXEL_RADIO_KEY, pixelRadio.toFloat()) 107 | .apply() 108 | return true 109 | } 110 | 111 | private fun cleanCache(): Boolean { 112 | // delete all of cache files 113 | Log.w(TAG, "[plugin] will delete all of contents") 114 | mContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE).edit() 115 | .clear().apply() 116 | return true 117 | } 118 | 119 | override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { 120 | when (call.method) { 121 | "plugin.initialize" -> { 122 | // the main engine should call initialize? 123 | // but the sub engine don't 124 | val pixelRadio = call.argument("pixelRadio") ?: 1.0 125 | val systemConfig = call.argument?>("system") as Map<*, *> 126 | 127 | val map = HashMap() 128 | map["permission_grated"] = permissionGiven(mContext) 129 | map["service_running"] = FloatwingService.isRunning(mContext) 130 | map["windows"] = FloatwingService.instance?.windows?.map { it.value.toMap() } 131 | 132 | map["pixel_radio_updated"] = savePixelRadio(pixelRadio) 133 | map["system_config_updated"] = saveSystemConfig(systemConfig) 134 | 135 | return result.success(map) 136 | } 137 | "plugin.has_permission" -> { 138 | return result.success(permissionGiven(mContext)) 139 | } 140 | "plugin.open_permission_setting" -> { 141 | return result.success(requestPermissions()) 142 | } 143 | "plugin.grant_permission" -> { 144 | return grantPermission(result) 145 | } 146 | // remove 147 | "plugin.create_window" -> { 148 | val id = call.argument("id") ?: "default" 149 | val cfg = call.argument>("config")!! 150 | val start = call.argument("start") ?: false 151 | val config = FloatWindow.Config.from(cfg) 152 | return result.success(FloatwingService.createWindow(mContext, id, config, start, null)) 153 | } 154 | "plugin.is_service_running" -> { 155 | return result.success(FloatwingService.isRunning(mContext)) 156 | } 157 | "plugin.start_service" -> { 158 | return result.success(FloatwingService.isRunning(mContext) 159 | .or(FloatwingService.start(mContext))) 160 | } 161 | "plugin.clean_cache" -> { 162 | return result.success(cleanCache()) 163 | } 164 | "plugin.sync_windows" -> { 165 | return result.success(FloatwingService.instance?.windows?.map { it.value.toMap() }) 166 | } 167 | "window.sync" -> { 168 | Log.d(TAG, "[plugin] fake window.sync") 169 | return result.success(null) 170 | } 171 | else -> { 172 | Log.d(TAG, "[plugin] method ${call.method} not implement") 173 | result.notImplemented() 174 | } 175 | } 176 | } 177 | 178 | override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { 179 | channel.setMethodCallHandler(null) 180 | } 181 | 182 | override fun onAttachedToActivity(binding: ActivityPluginBinding) { 183 | mActivity = binding.activity 184 | 185 | // TODO: notify the window to show and return the result? 186 | 187 | Log.d(TAG, "[plugin] on attached to activity") 188 | 189 | // how to known are the main 190 | FloatwingService.onActivityAttached(mActivity) 191 | } 192 | 193 | override fun onDetachedFromActivityForConfigChanges() { 194 | 195 | } 196 | 197 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { 198 | mActivity = binding.activity 199 | } 200 | 201 | override fun onDetachedFromActivity() { 202 | 203 | } 204 | 205 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { 206 | if (requestCode == ALERT_WINDOW_PERMISSION) { 207 | waitPermissionResult.success(permissionGiven(mContext)) 208 | return true 209 | } 210 | return false 211 | } 212 | 213 | private fun requestPermissions(): Boolean { 214 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 215 | mActivity.startActivityForResult(Intent( 216 | Settings.ACTION_MANAGE_OVERLAY_PERMISSION, 217 | Uri.parse("package:${mContext.packageName}") 218 | ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), ALERT_WINDOW_PERMISSION) 219 | return true 220 | } 221 | return false 222 | } 223 | 224 | private fun grantPermission(result: Result) { 225 | waitPermissionResult = result 226 | requestPermissions() 227 | } 228 | 229 | companion object { 230 | @JvmStatic 231 | fun registerWith(registrar: Registrar) { 232 | val channel = MethodChannel(registrar.messenger(), CHANNEL_NAME) 233 | channel.setMethodCallHandler(FlutterFloatwingPlugin()) 234 | } 235 | 236 | private const val TAG = "FloatwingPlugin" 237 | private const val CHANNEL_NAME = "im.zoe.labs/flutter_floatwing/method" 238 | private const val ALERT_WINDOW_PERMISSION = 1248 239 | 240 | const val FLUTTER_ENGINE_CACHE_KEY = "flutter_engine_main" 241 | const val SHARED_PREFERENCES_KEY = "flutter_floatwing_cache" 242 | const val CALLBACK_KEY = "callback_key" 243 | const val PIXEL_RADIO_KEY = "pixel_radio" 244 | const val SYSTEM_CONFIG_KEY = "system_config" 245 | 246 | fun permissionGiven(context: Context): Boolean { 247 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 248 | return Settings.canDrawOverlays(context) 249 | } 250 | return false 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /android/src/main/kotlin/im/zoe/labs/flutter_floatwing/Utils.kt: -------------------------------------------------------------------------------- 1 | package im.zoe.labs.flutter_floatwing 2 | 3 | import android.app.ActivityManager 4 | import android.content.Context 5 | import org.json.JSONArray 6 | import org.json.JSONObject 7 | import java.math.BigInteger 8 | import java.security.MessageDigest 9 | 10 | class Utils { 11 | companion object { 12 | fun getRunningService(context: Context, serviceClass: Class<*>): ActivityManager.RunningServiceInfo? { 13 | val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager? 14 | for (service in manager!!.getRunningServices(Int.MAX_VALUE)) { 15 | if (serviceClass.name == service.service.className) { 16 | return service 17 | } 18 | } 19 | 20 | return null 21 | } 22 | 23 | fun md5(input:String): String { 24 | val md = MessageDigest.getInstance("MD5") 25 | return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') 26 | } 27 | 28 | fun genKey(vararg items: Any?): String { 29 | return Utils.md5(items.joinToString(separator="-"){ "$it" }).slice(IntRange(0, 12)) 30 | } 31 | 32 | fun JSONObject.toMap(): Map = keys().asSequence().associateWith { 33 | when (val value = this[it]) 34 | { 35 | is JSONArray -> 36 | { 37 | val map = (0 until value.length()).associate { Pair(it.toString(), value[it]) } 38 | JSONObject(map).toMap().values.toList() 39 | } 40 | is JSONObject -> value.toMap() 41 | JSONObject.NULL -> null 42 | else -> value 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /assets/flutter-floatwing-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiusanzhou/flutter_floatwing/74e4bda9b203c2963c2f6b776c08b3830ec1c484/assets/flutter-floatwing-arch.png -------------------------------------------------------------------------------- /assets/flutter-floatwing-example-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiusanzhou/flutter_floatwing/74e4bda9b203c2963c2f6b776c08b3830ec1c484/assets/flutter-floatwing-example-1.gif -------------------------------------------------------------------------------- /assets/flutter-floatwing-example-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiusanzhou/flutter_floatwing/74e4bda9b203c2963c2f6b776c08b3830ec1c484/assets/flutter-floatwing-example-2.gif -------------------------------------------------------------------------------- /assets/flutter-floatwing-example-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiusanzhou/flutter_floatwing/74e4bda9b203c2963c2f6b776c08b3830ec1c484/assets/flutter-floatwing-example-3.gif -------------------------------------------------------------------------------- /assets/flutter-floatwing-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiusanzhou/flutter_floatwing/74e4bda9b203c2963c2f6b776c08b3830ec1c484/assets/flutter-floatwing-window.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Exceptions to above rules. 43 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 44 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f7a6a7906be96d2288f5d63a5a54c515a6e987fe 8 | channel: unknown 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flutter_floatwing_example 2 | 3 | Demonstrates how to use the flutter_floatwing plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | namespace 'im.zoe.labs.flutter_floatwing_example' 30 | compileSdkVersion 31 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | test.java.srcDirs += 'src/test/kotlin' 44 | } 45 | 46 | lintOptions { 47 | disable 'InvalidPackage' 48 | } 49 | 50 | defaultConfig { 51 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 52 | applicationId "im.zoe.labs.flutter_floatwing_example" 53 | minSdkVersion flutter.minSdkVersion 54 | targetSdkVersion 31 55 | versionCode flutterVersionCode.toInteger() 56 | versionName flutterVersionName 57 | } 58 | 59 | buildTypes { 60 | release { 61 | // TODO: Add your own signing config for the release build. 62 | // Signing with the debug keys for now, so `flutter run --release` works. 63 | signingConfig signingConfigs.debug 64 | } 65 | } 66 | } 67 | 68 | flutter { 69 | source '../..' 70 | } 71 | 72 | dependencies { 73 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 74 | } 75 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 13 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/im/zoe/labs/flutter_floatwing_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package im.zoe.labs.flutter_floatwing_example 2 | 3 | import android.os.Bundle 4 | import android.os.PersistableBundle 5 | import io.flutter.embedding.android.FlutterActivity 6 | 7 | class MainActivity: FlutterActivity() { 8 | } 9 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiusanzhou/flutter_floatwing/74e4bda9b203c2963c2f6b776c08b3830ec1c484/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiusanzhou/flutter_floatwing/74e4bda9b203c2963c2f6b776c08b3830ec1c484/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiusanzhou/flutter_floatwing/74e4bda9b203c2963c2f6b776c08b3830ec1c484/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiusanzhou/flutter_floatwing/74e4bda9b203c2963c2f6b776c08b3830ec1c484/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiusanzhou/flutter_floatwing/74e4bda9b203c2963c2f6b776c08b3830ec1c484/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.8.22' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:8.0.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | tasks.register("clean", Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.defaults.buildfeatures.buildconfig=true 5 | android.nonTransitiveRClass=false 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat May 07 12:01:26 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'package:flutter_floatwing/flutter_floatwing.dart'; 4 | import 'package:flutter_floatwing_example/views/assistive_touch.dart'; 5 | import 'package:flutter_floatwing_example/views/night.dart'; 6 | import 'package:flutter_floatwing_example/views/normal.dart'; 7 | 8 | void main() { 9 | runApp(MyApp()); 10 | } 11 | 12 | @pragma("vm:entry-point") 13 | void floatwing() { 14 | runApp(((_) => NonrmalView()).floatwing().make()); 15 | } 16 | 17 | void floatwing2(Window w) { 18 | runApp(MaterialApp( 19 | // floatwing on widget can't use Window.of(context) 20 | // to access window instance 21 | // should use FloatwingPlugin().currentWindow 22 | home: NonrmalView().floatwing(), 23 | )); 24 | } 25 | 26 | class MyApp extends StatefulWidget { 27 | @override 28 | _MyAppState createState() => _MyAppState(); 29 | } 30 | 31 | class _MyAppState extends State { 32 | var _configs = [ 33 | WindowConfig( 34 | id: "normal", 35 | // entry: "floatwing", 36 | route: "/normal", 37 | draggable: true, 38 | ), 39 | WindowConfig( 40 | id: "assitive_touch", 41 | // entry: "floatwing", 42 | route: "/assitive_touch", 43 | draggable: true, 44 | ), 45 | WindowConfig( 46 | id: "night", 47 | // entry: "floatwing", 48 | route: "/night", 49 | width: WindowSize.MatchParent, height: WindowSize.MatchParent, 50 | clickable: false, 51 | ) 52 | ]; 53 | 54 | Map _builders = { 55 | "normal": (_) => NonrmalView(), 56 | "assitive_touch": (_) => AssistiveTouch(), 57 | "night": (_) => NightView(), 58 | }; 59 | 60 | Map _routes = {}; 61 | 62 | @override 63 | void initState() { 64 | super.initState(); 65 | 66 | _routes["/"] = (_) => HomePage(configs: _configs); 67 | 68 | _configs.forEach((c) => { 69 | if (c.route != null && _builders[c.id] != null) 70 | {_routes[c.route!] = _builders[c.id]!.floatwing(debug: false)} 71 | }); 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | return MaterialApp( 77 | debugShowCheckedModeBanner: false, 78 | initialRoute: "/", 79 | routes: _routes, 80 | ); 81 | } 82 | } 83 | 84 | class HomePage extends StatefulWidget { 85 | final List configs; 86 | const HomePage({Key? key, required this.configs}) : super(key: key); 87 | 88 | @override 89 | State createState() => _HomePageState(); 90 | } 91 | 92 | class _HomePageState extends State { 93 | @override 94 | void initState() { 95 | super.initState(); 96 | 97 | widget.configs.forEach((c) => _windows.add(c.to())); 98 | 99 | FloatwingPlugin().initialize(); 100 | 101 | initAsyncState(); 102 | } 103 | 104 | List _windows = []; 105 | 106 | Map _readys = {}; 107 | 108 | bool _ready = false; 109 | 110 | initAsyncState() async { 111 | var p1 = await FloatwingPlugin().checkPermission(); 112 | var p2 = await FloatwingPlugin().isServiceRunning(); 113 | 114 | // get permission first 115 | if (!p1) { 116 | FloatwingPlugin().openPermissionSetting(); 117 | return; 118 | } 119 | 120 | // start service 121 | if (!p2) { 122 | FloatwingPlugin().startService(); 123 | } 124 | 125 | _createWindows(); 126 | 127 | setState(() { 128 | _ready = true; 129 | }); 130 | } 131 | 132 | _createWindows() async { 133 | await FloatwingPlugin().isServiceRunning().then((v) async { 134 | if (!v) 135 | await FloatwingPlugin().startService().then((_) { 136 | print("start the backgroud service success."); 137 | }); 138 | }); 139 | 140 | _windows.forEach((w) { 141 | var _w = FloatwingPlugin().windows[w.id]; 142 | if (null != _w) { 143 | // replace w with _w 144 | _readys[w] = true; 145 | return; 146 | } 147 | w.on(EventType.WindowCreated, (window, data) { 148 | _readys[window] = true; 149 | setState(() {}); 150 | }).create(); 151 | }); 152 | } 153 | 154 | @override 155 | Widget build(BuildContext context) { 156 | return Scaffold( 157 | appBar: AppBar( 158 | title: const Text('Floatwing example app'), 159 | ), 160 | body: _ready 161 | ? ListView( 162 | children: _windows.map((e) => _item(e)).toList(), 163 | ) 164 | : Center( 165 | child: ElevatedButton( 166 | onPressed: () { 167 | initAsyncState(); 168 | }, 169 | child: Text("Start")), 170 | ), 171 | ); 172 | } 173 | 174 | _debug(Window w) { 175 | Navigator.of(context).pushNamed(w.config!.route!); 176 | } 177 | 178 | Widget _item(Window w) { 179 | return Card( 180 | margin: EdgeInsets.all(10), 181 | child: Padding( 182 | padding: EdgeInsets.all(10), 183 | child: Column( 184 | children: [ 185 | Text(w.id, 186 | style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), 187 | SizedBox(height: 10), 188 | Container( 189 | width: double.infinity, 190 | padding: EdgeInsets.all(5), 191 | decoration: BoxDecoration( 192 | color: Color.fromARGB(255, 214, 213, 213), 193 | borderRadius: BorderRadius.all(Radius.circular(4))), 194 | child: Text(w.config?.toString() ?? ""), 195 | ), 196 | SizedBox(height: 10), 197 | Row( 198 | mainAxisAlignment: MainAxisAlignment.end, 199 | children: [ 200 | TextButton( 201 | onPressed: (_readys[w] == true) ? () => w.start() : null, 202 | child: Text("Open"), 203 | ), 204 | TextButton( 205 | onPressed: 206 | w.config?.route != null ? () => _debug(w) : null, 207 | child: Text("Debug")), 208 | TextButton( 209 | onPressed: (_readys[w] == true) 210 | ? () => {w.close(), w.share("close")} 211 | : null, 212 | child: Text("Close", style: TextStyle(color: Colors.red)), 213 | ), 214 | ], 215 | ) 216 | ], 217 | )), 218 | ); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /example/lib/views/assistive_touch.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/scheduler.dart'; 6 | import 'package:flutter_floatwing/flutter_floatwing.dart'; 7 | 8 | class AssistiveTouch extends StatefulWidget { 9 | const AssistiveTouch({Key? key}) : super(key: key); 10 | 11 | @override 12 | State createState() => _AssistiveTouchState(); 13 | } 14 | 15 | void _pannelMain() { 16 | runApp(((_) => AssistivePannel()).floatwing(app: true).make()); 17 | } 18 | 19 | class _AssistiveTouchState extends State { 20 | /// The state of the touch state 21 | bool expend = false; 22 | bool pannelReady = false; 23 | Window? pannelWindow; 24 | Window? touchWindow; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | 30 | initAsyncState(); 31 | 32 | SchedulerBinding.instance?.addPostFrameCallback((_) { 33 | touchWindow = Window.of(context); 34 | touchWindow?.on(EventType.WindowStarted, (window, data) { 35 | print("touch window start ..."); 36 | expend = false; 37 | setState(() {}); 38 | }); 39 | }); 40 | } 41 | 42 | void initAsyncState() async { 43 | // create the pannel window 44 | pannelWindow = WindowConfig( 45 | id: "assistive_pannel", 46 | callback: _pannelMain, 47 | width: WindowSize.MatchParent, 48 | height: WindowSize.MatchParent, 49 | autosize: false, 50 | ).to(); 51 | 52 | pannelWindow?.create(); 53 | // we can't subscribe the events from other windows 54 | // that means pannelWindow's events can't be fired to here. 55 | // This is a feature, make sure window only care about events 56 | // from self. If we want to communicate with the other windows, 57 | // we can use the data communicatting method. 58 | pannelWindow?.on(EventType.WindowCreated, (window, data) { 59 | pannelReady = true; 60 | setState(() {}); 61 | }).on(EventType.WindowPaused, (window, data) { 62 | // open the assitive_touch 63 | touchWindow?.start(); 64 | }); 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return AssistiveButton(onTap: _onTap); 70 | } 71 | 72 | void _onTap() { 73 | var w = Window.of(context); 74 | // send the postion to pannel window 75 | var x = w?.config?.x ?? 0 / (w?.pixelRadio ?? 1); 76 | var y = w?.config?.y ?? 0 / (w?.pixelRadio ?? 1); 77 | pannelWindow?.share([x, y]); 78 | // send touchWindow postion to pannel window 79 | pannelWindow?.start(); 80 | setState(() { 81 | // hide button 82 | expend = true; 83 | }); 84 | } 85 | } 86 | 87 | @immutable 88 | class AssistiveButton extends StatefulWidget { 89 | const AssistiveButton({ 90 | Key? key, 91 | this.child = const _DefaultChild(), 92 | this.visible = true, 93 | this.shouldStickToSide = true, 94 | this.margin = const EdgeInsets.all(8.0), 95 | this.initialOffset = Offset.infinite, 96 | this.onTap, 97 | this.animatedBuilder, 98 | }) : super(key: key); 99 | 100 | /// The widget below this widget in the tree. 101 | final Widget child; 102 | 103 | /// Switches between showing the [child] or hiding it. 104 | final bool visible; 105 | 106 | /// Whether it sticks to the side. 107 | final bool shouldStickToSide; 108 | 109 | /// Empty space to surround the [child]. 110 | final EdgeInsets margin; 111 | 112 | final Offset initialOffset; 113 | 114 | /// A tap with a primary button has occurred. 115 | final VoidCallback? onTap; 116 | 117 | /// Custom animated builder. 118 | final Widget Function( 119 | BuildContext context, 120 | Widget child, 121 | bool visible, 122 | )? animatedBuilder; 123 | 124 | @override 125 | _AssistiveButtonState createState() => _AssistiveButtonState(); 126 | } 127 | 128 | class _AssistiveButtonState extends State 129 | with TickerProviderStateMixin { 130 | bool isInitialized = false; 131 | late Offset offset = widget.initialOffset; 132 | late Offset largerOffset = offset; 133 | Size size = Size.zero; 134 | bool isDragging = false; 135 | bool isIdle = true; 136 | Timer? timer; 137 | late final AnimationController _scaleAnimationController = 138 | AnimationController( 139 | duration: const Duration(milliseconds: 200), 140 | vsync: this, 141 | )..addListener(() { 142 | setState(() {}); 143 | }); 144 | late final Animation _scaleAnimation = CurvedAnimation( 145 | parent: _scaleAnimationController, 146 | curve: Curves.easeInOut, 147 | ); 148 | Timer? scaleTimer; 149 | 150 | Window? window; 151 | 152 | @override 153 | void initState() { 154 | super.initState(); 155 | scaleTimer = Timer.periodic(const Duration(milliseconds: 60), (_) { 156 | if (mounted == false) { 157 | return; 158 | } 159 | 160 | if (widget.visible) { 161 | _scaleAnimationController.forward(); 162 | } else { 163 | _scaleAnimationController.reverse(); 164 | } 165 | }); 166 | FocusManager.instance.addListener(listener); 167 | } 168 | 169 | @override 170 | void didChangeDependencies() { 171 | super.didChangeDependencies(); 172 | if (isInitialized == false) { 173 | isInitialized = true; 174 | _setOffset(offset); 175 | } 176 | } 177 | 178 | @override 179 | void dispose() { 180 | timer?.cancel(); 181 | scaleTimer?.cancel(); 182 | _scaleAnimationController.dispose(); 183 | FocusManager.instance.removeListener(listener); 184 | super.dispose(); 185 | } 186 | 187 | void listener() { 188 | Timer(const Duration(milliseconds: 200), () { 189 | if (mounted == false) return; 190 | largerOffset = Offset( 191 | max(largerOffset.dx, offset.dx), 192 | max(largerOffset.dy, offset.dy), 193 | ); 194 | 195 | _setOffset(largerOffset, false); 196 | }); 197 | } 198 | 199 | @override 200 | Widget build(BuildContext context) { 201 | var child = widget.child; 202 | 203 | if (window == null) { 204 | window = Window.of(context); 205 | window?.on(EventType.WindowDragStart, (window, data) => _onDragStart()); 206 | window?.on(EventType.WindowDragging, (window, data) { 207 | var p = data as List; 208 | _onDragUpdate(p[0], p[1]); 209 | }); 210 | window?.on(EventType.WindowDragEnd, (window, windowdata) => _onDragEnd()); 211 | } 212 | 213 | child = GestureDetector( 214 | onTap: _onTap, 215 | child: child, 216 | ); 217 | 218 | child = widget.animatedBuilder != null 219 | ? widget.animatedBuilder!(context, child, widget.visible) 220 | : ScaleTransition( 221 | scale: _scaleAnimation, 222 | child: AnimatedOpacity( 223 | opacity: isIdle ? .3 : 1, 224 | duration: const Duration(milliseconds: 300), 225 | child: child, 226 | ), 227 | ); 228 | 229 | return child; 230 | } 231 | 232 | void _onTap() async { 233 | if (widget.onTap != null) { 234 | setState(() { 235 | isIdle = false; 236 | }); 237 | _scheduleIdle(); 238 | widget.onTap!(); 239 | } 240 | } 241 | 242 | void _onDragStart() { 243 | setState(() { 244 | isDragging = true; 245 | isIdle = false; 246 | }); 247 | timer?.cancel(); 248 | } 249 | 250 | Offset _old = Offset.zero; 251 | void _onDragUpdate(int x, int y) { 252 | _old = Offset(x.toDouble(), y.toDouble()); 253 | _setOffset(_old); 254 | } 255 | 256 | void _onDragEnd() { 257 | setState(() { 258 | isDragging = false; 259 | }); 260 | _scheduleIdle(); 261 | 262 | _setOffset(offset); 263 | } 264 | 265 | void _scheduleIdle() { 266 | timer?.cancel(); 267 | timer = Timer(const Duration(seconds: 2), () { 268 | if (isDragging == false) { 269 | setState(() { 270 | isIdle = true; 271 | }); 272 | } 273 | }); 274 | } 275 | 276 | void _updatePosition() { 277 | window?.update(WindowConfig( 278 | x: offset.dx.toInt(), 279 | y: offset.dy.toInt(), 280 | )); 281 | } 282 | 283 | /// TODO: this function should depend on the gravity to calcute the position 284 | void _setOffset(Offset offset, [bool shouldUpdateLargerOffset = true]) { 285 | if (shouldUpdateLargerOffset) { 286 | largerOffset = offset; 287 | } 288 | 289 | if (isDragging) { 290 | this.offset = offset; 291 | return; 292 | } 293 | 294 | final screenSize = 295 | window?.system?.screenSize ?? MediaQuery.of(context).size; 296 | final screenPadding = MediaQuery.of(context).padding; 297 | final viewInsets = MediaQuery.of(context).viewInsets; 298 | final left = screenPadding.left + viewInsets.left + widget.margin.left; 299 | final top = screenPadding.top + viewInsets.top + widget.margin.top; 300 | final right = screenSize.width - 301 | screenPadding.right - 302 | viewInsets.right - 303 | widget.margin.right - 304 | size.width; 305 | final bottom = screenSize.height - 306 | screenPadding.bottom - 307 | viewInsets.bottom - 308 | widget.margin.bottom - 309 | size.height; 310 | 311 | final halfWidth = (right - left) / 2; 312 | 313 | if (widget.shouldStickToSide) { 314 | final normalizedTop = max(min(offset.dy, bottom), top); 315 | final normalizedLeft = max( 316 | min( 317 | normalizedTop == bottom || normalizedTop == top 318 | ? offset.dx 319 | : offset.dx < halfWidth 320 | ? left 321 | : right, 322 | right, 323 | ), 324 | left, 325 | ); 326 | this.offset = Offset(normalizedLeft, normalizedTop); 327 | } else { 328 | final normalizedTop = max(min(offset.dy, bottom), top); 329 | final normalizedLeft = max(min(offset.dx, right), left); 330 | this.offset = Offset(normalizedLeft, normalizedTop); 331 | } 332 | _updatePosition(); 333 | } 334 | 335 | // Offset _applyGravity(Offset o) { 336 | // return window?.config?.gravity.apply(o) ?? o; 337 | // } 338 | } 339 | 340 | class AssistivePannel extends StatefulWidget { 341 | const AssistivePannel({ 342 | Key? key, 343 | }) : super(key: key); 344 | 345 | @override 346 | State createState() => _AssistivePannelState(); 347 | } 348 | 349 | class _AssistivePannelState extends State 350 | with SingleTickerProviderStateMixin { 351 | late AnimationController _animationController = AnimationController( 352 | duration: _duration, 353 | vsync: this, 354 | ); 355 | 356 | Duration _duration = Duration(milliseconds: 200); 357 | 358 | Window? window; 359 | 360 | @override 361 | void dispose() { 362 | _animationController.dispose(); 363 | super.dispose(); 364 | } 365 | 366 | @override 367 | void initState() { 368 | super.initState(); 369 | 370 | SchedulerBinding.instance?.addPostFrameCallback((_) { 371 | window = Window.of(context); 372 | window?.on(EventType.WindowStarted, (window, data) { 373 | // if start just show 374 | print("pannel just start ..."); 375 | setState(() { 376 | _show = true; 377 | }); 378 | }).onData((source, name, data) async { 379 | int x = data[0]; 380 | int y = data[1]; 381 | _updatePostion(x, y); 382 | return; 383 | }); 384 | }); 385 | } 386 | 387 | double? _left; 388 | double? _right; 389 | double? _top; 390 | bool _isLeft = false; 391 | _updatePostion(int x, int y) { 392 | _isLeft = x < 50; 393 | if (y <= size) { 394 | _top = fixed; 395 | } else if (y >= screenHeight - size) { 396 | _top = screenHeight - size - fixed; 397 | } else { 398 | _top = y - size / 2; 399 | } 400 | } 401 | 402 | var fixed = 100.0; 403 | var factor = 0.8; 404 | 405 | double screenWidth = 0.0; 406 | double screenHeight = 0.0; 407 | double size = 0.0; 408 | 409 | @override 410 | Widget build(BuildContext context) { 411 | screenWidth = MediaQuery.of(context).size.width; 412 | screenHeight = MediaQuery.of(context).size.height; 413 | size = screenWidth * factor; 414 | 415 | return GestureDetector( 416 | onTap: _onTap, 417 | child: Container( 418 | color: Colors.transparent, 419 | child: Stack( 420 | children: [ 421 | // Container(color: Colors.red), 422 | AnimatedPositioned( 423 | bottom: _bottom, 424 | height: _show ? size : 0, 425 | left: _show ? 0 : screenWidth / 2, 426 | right: _show ? 0 : screenWidth / 2, 427 | curve: Curves.easeIn, 428 | duration: _duration, 429 | child: FractionallySizedBox( 430 | widthFactor: factor, 431 | child: GestureDetector( 432 | onTap: () => null, 433 | child: Container( 434 | decoration: BoxDecoration( 435 | borderRadius: BorderRadius.all(Radius.circular(16)), 436 | color: Color.fromARGB(255, 25, 24, 24), 437 | ), 438 | child: Stack( 439 | children: [], 440 | ), 441 | )), 442 | )), 443 | ], 444 | ), 445 | ), 446 | ); 447 | } 448 | 449 | bool _show = false; 450 | 451 | double? _bottom = 200; 452 | 453 | _onTap() { 454 | setState(() { 455 | _show = false; 456 | }); 457 | Timer(_duration, () => window?.close()); 458 | } 459 | } 460 | 461 | class _DefaultChild extends StatelessWidget { 462 | const _DefaultChild({ 463 | Key? key, 464 | }) : super(key: key); 465 | 466 | @override 467 | Widget build(BuildContext context) { 468 | return Container( 469 | height: 56, 470 | width: 56, 471 | alignment: Alignment.center, 472 | decoration: BoxDecoration( 473 | color: Colors.grey[900], 474 | borderRadius: const BorderRadius.all(Radius.circular(28)), 475 | ), 476 | child: Container( 477 | height: 40, 478 | width: 40, 479 | alignment: Alignment.center, 480 | decoration: BoxDecoration( 481 | color: Colors.grey[400]!.withOpacity(.6), 482 | borderRadius: const BorderRadius.all(Radius.circular(28)), 483 | ), 484 | child: Container( 485 | height: 32, 486 | width: 32, 487 | alignment: Alignment.center, 488 | decoration: BoxDecoration( 489 | color: Colors.grey[300]!.withOpacity(.6), 490 | borderRadius: const BorderRadius.all(Radius.circular(28)), 491 | ), 492 | child: Container( 493 | height: 24, 494 | width: 24, 495 | alignment: Alignment.center, 496 | decoration: const BoxDecoration( 497 | color: Colors.white, 498 | borderRadius: BorderRadius.all(Radius.circular(28)), 499 | ), 500 | child: const SizedBox.expand(), 501 | ), 502 | ), 503 | ), 504 | ); 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /example/lib/views/night.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_floatwing/flutter_floatwing.dart'; 3 | 4 | class NightView extends StatefulWidget { 5 | const NightView({Key? key}) : super(key: key); 6 | 7 | @override 8 | State createState() => _NightViewState(); 9 | } 10 | 11 | class _NightViewState extends State { 12 | Color color = Color.fromARGB(255, 192, 200, 41).withOpacity(0.20); 13 | 14 | @override 15 | void initState() { 16 | super.initState(); 17 | } 18 | 19 | Window? w; 20 | 21 | var _show = true; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Container( 26 | height: _show ? MediaQuery.of(context).size.height : 0, 27 | width: _show ? MediaQuery.of(context).size.width : 0, 28 | color: color, 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/lib/views/normal.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | import 'package:flutter_floatwing/flutter_floatwing.dart'; 4 | 5 | class NonrmalView extends StatefulWidget { 6 | const NonrmalView({Key? key}) : super(key: key); 7 | 8 | @override 9 | State createState() => _NonrmalViewState(); 10 | } 11 | 12 | class _NonrmalViewState extends State { 13 | bool _expend = false; 14 | double _size = 150; 15 | 16 | @override 17 | void initState() { 18 | super.initState(); 19 | 20 | SchedulerBinding.instance?.addPostFrameCallback((_) { 21 | w = Window.of(context); 22 | w?.on(EventType.WindowDragStart, (window, data) { 23 | if (mounted) setState(() => {dragging = true}); 24 | }).on(EventType.WindowDragEnd, (window, data) { 25 | if (mounted) setState(() => {dragging = false}); 26 | }); 27 | }); 28 | } 29 | 30 | Window? w; 31 | bool dragging = false; 32 | 33 | _changeSize() { 34 | _expend = !_expend; 35 | _size = _expend ? 250 : 150; 36 | setState(() {}); 37 | } 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return Center( 42 | child: Container( 43 | width: _size, 44 | height: _size, 45 | color: dragging ? Colors.yellowAccent : null, 46 | child: Card( 47 | child: Stack( 48 | children: [ 49 | Center( 50 | child: ElevatedButton( 51 | onPressed: () { 52 | w?.launchMainActivity(); 53 | }, 54 | child: Text("Start Activity"))), 55 | Positioned( 56 | right: 5, top: 5, child: Icon(Icons.drag_handle_rounded)), 57 | Positioned( 58 | right: 5, 59 | bottom: 5, 60 | child: RotationTransition( 61 | turns: AlwaysStoppedAnimation(-45 / 360), 62 | child: InkWell( 63 | onTap: _changeSize, 64 | child: Icon(Icons.unfold_more_rounded)))) 65 | ], 66 | )), 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.18.0" 44 | cupertino_icons: 45 | dependency: "direct main" 46 | description: 47 | name: cupertino_icons 48 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.0.8" 52 | fake_async: 53 | dependency: transitive 54 | description: 55 | name: fake_async 56 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.1" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_floatwing: 66 | dependency: "direct main" 67 | description: 68 | path: ".." 69 | relative: true 70 | source: path 71 | version: "0.2.1" 72 | flutter_test: 73 | dependency: "direct dev" 74 | description: flutter 75 | source: sdk 76 | version: "0.0.0" 77 | leak_tracker: 78 | dependency: transitive 79 | description: 80 | name: leak_tracker 81 | sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" 82 | url: "https://pub.dev" 83 | source: hosted 84 | version: "10.0.0" 85 | leak_tracker_flutter_testing: 86 | dependency: transitive 87 | description: 88 | name: leak_tracker_flutter_testing 89 | sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 90 | url: "https://pub.dev" 91 | source: hosted 92 | version: "2.0.1" 93 | leak_tracker_testing: 94 | dependency: transitive 95 | description: 96 | name: leak_tracker_testing 97 | sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 98 | url: "https://pub.dev" 99 | source: hosted 100 | version: "2.0.1" 101 | matcher: 102 | dependency: transitive 103 | description: 104 | name: matcher 105 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 106 | url: "https://pub.dev" 107 | source: hosted 108 | version: "0.12.16+1" 109 | material_color_utilities: 110 | dependency: transitive 111 | description: 112 | name: material_color_utilities 113 | sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" 114 | url: "https://pub.dev" 115 | source: hosted 116 | version: "0.8.0" 117 | meta: 118 | dependency: transitive 119 | description: 120 | name: meta 121 | sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 122 | url: "https://pub.dev" 123 | source: hosted 124 | version: "1.11.0" 125 | path: 126 | dependency: transitive 127 | description: 128 | name: path 129 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 130 | url: "https://pub.dev" 131 | source: hosted 132 | version: "1.9.0" 133 | sky_engine: 134 | dependency: transitive 135 | description: flutter 136 | source: sdk 137 | version: "0.0.99" 138 | source_span: 139 | dependency: transitive 140 | description: 141 | name: source_span 142 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 143 | url: "https://pub.dev" 144 | source: hosted 145 | version: "1.10.0" 146 | stack_trace: 147 | dependency: transitive 148 | description: 149 | name: stack_trace 150 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 151 | url: "https://pub.dev" 152 | source: hosted 153 | version: "1.11.1" 154 | stream_channel: 155 | dependency: transitive 156 | description: 157 | name: stream_channel 158 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 159 | url: "https://pub.dev" 160 | source: hosted 161 | version: "2.1.2" 162 | string_scanner: 163 | dependency: transitive 164 | description: 165 | name: string_scanner 166 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 167 | url: "https://pub.dev" 168 | source: hosted 169 | version: "1.2.0" 170 | term_glyph: 171 | dependency: transitive 172 | description: 173 | name: term_glyph 174 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 175 | url: "https://pub.dev" 176 | source: hosted 177 | version: "1.2.1" 178 | test_api: 179 | dependency: transitive 180 | description: 181 | name: test_api 182 | sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" 183 | url: "https://pub.dev" 184 | source: hosted 185 | version: "0.6.1" 186 | vector_math: 187 | dependency: transitive 188 | description: 189 | name: vector_math 190 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 191 | url: "https://pub.dev" 192 | source: hosted 193 | version: "2.1.4" 194 | vm_service: 195 | dependency: transitive 196 | description: 197 | name: vm_service 198 | sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 199 | url: "https://pub.dev" 200 | source: hosted 201 | version: "13.0.0" 202 | sdks: 203 | dart: ">=3.2.0-0 <4.0.0" 204 | flutter: ">=1.20.0" 205 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_floatwing_example 2 | description: Demonstrates how to use the flutter_floatwing plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: '>=3.0.0 <4.0.0' 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | flutter_floatwing: 16 | # When depending on this package from a real application you should use: 17 | # flutter_floatwing: ^x.y.z 18 | # See https://dart.dev/tools/pub/dependencies#version-constraints 19 | # The example app is bundled with the plugin so we use a path dependency on 20 | # the parent directory to use the current plugin's version. 21 | path: ../ 22 | 23 | # The following adds the Cupertino Icons font to your application. 24 | # Use with the CupertinoIcons class for iOS style icons. 25 | cupertino_icons: ^1.0.8 26 | 27 | dev_dependencies: 28 | flutter_test: 29 | sdk: flutter 30 | 31 | # For information on the generic Dart part of this file, see the 32 | # following page: https://dart.dev/tools/pub/pubspec 33 | 34 | # The following section is specific to Flutter. 35 | flutter: 36 | 37 | # The following line ensures that the Material Icons font is 38 | # included with your application, so that you can use the icons in 39 | # the material Icons class. 40 | uses-material-design: true 41 | 42 | # To add assets to your application, add an assets section, like this: 43 | # assets: 44 | # - images/a_dot_burr.jpeg 45 | # - images/a_dot_ham.jpeg 46 | 47 | # An image asset can refer to one or more resolution-specific "variants", see 48 | # https://flutter.dev/assets-and-images/#resolution-aware. 49 | 50 | # For details regarding adding assets from package dependencies, see 51 | # https://flutter.dev/assets-and-images/#from-packages 52 | 53 | # To add custom fonts to your application, add a fonts section here, 54 | # in this "flutter" section. Each entry in this list should have a 55 | # "family" key with the font family name, and a "fonts" key with a 56 | # list giving the asset and other descriptors for the font. For 57 | # example: 58 | # fonts: 59 | # - family: Schyler 60 | # fonts: 61 | # - asset: fonts/Schyler-Regular.ttf 62 | # - asset: fonts/Schyler-Italic.ttf 63 | # style: italic 64 | # - family: Trajan Pro 65 | # fonts: 66 | # - asset: fonts/TrajanPro.ttf 67 | # - asset: fonts/TrajanPro_Bold.ttf 68 | # weight: 700 69 | # 70 | # For details regarding fonts from package dependencies, 71 | # see https://flutter.dev/custom-fonts/#from-packages 72 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_floatwing_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /flutter_floatwing.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/flutter_floatwing.dart: -------------------------------------------------------------------------------- 1 | export 'src/plugin.dart'; 2 | export 'src/window.dart'; 3 | export 'src/provider.dart'; 4 | export 'src/event.dart'; 5 | export 'src/utils.dart'; 6 | export 'src/constants.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// window size 4 | /// 5 | class WindowSize { 6 | static const int MatchParent = -1; 7 | static const int WrapContent = -2; 8 | } 9 | 10 | enum GravityType { 11 | Center, 12 | CenterTop, 13 | CenterBottom, 14 | LeftTop, 15 | LeftCenter, 16 | LeftBottom, 17 | RightTop, 18 | RightCenter, 19 | RightBottom, 20 | 21 | Unknown, 22 | } 23 | 24 | extension GravityTypeConverter on GravityType { 25 | // ignore: slash_for_doc_comments 26 | /** 27 | public static final int AXIS_CLIP = 8; 28 | public static final int AXIS_PULL_AFTER = 4; 29 | public static final int AXIS_PULL_BEFORE = 2; 30 | public static final int AXIS_SPECIFIED = 1; 31 | public static final int AXIS_X_SHIFT = 0; 32 | public static final int AXIS_Y_SHIFT = 4; 33 | public static final int BOTTOM = 80; 34 | public static final int CENTER = 17; 35 | public static final int CENTER_HORIZONTAL = 1; 36 | public static final int CENTER_VERTICAL = 16; 37 | public static final int CLIP_HORIZONTAL = 8; 38 | public static final int CLIP_VERTICAL = 128; 39 | public static final int DISPLAY_CLIP_HORIZONTAL = 16777216; 40 | public static final int DISPLAY_CLIP_VERTICAL = 268435456; 41 | public static final int END = 8388613; 42 | public static final int FILL = 119; 43 | public static final int FILL_HORIZONTAL = 7; 44 | public static final int FILL_VERTICAL = 112; 45 | public static final int HORIZONTAL_GRAVITY_MASK = 7; 46 | public static final int LEFT = 3; 47 | public static final int NO_GRAVITY = 0; 48 | public static final int RELATIVE_HORIZONTAL_GRAVITY_MASK = 8388615; 49 | public static final int RELATIVE_LAYOUT_DIRECTION = 8388608; 50 | public static final int RIGHT = 5; 51 | public static final int START = 8388611; 52 | public static final int TOP = 48; 53 | public static final int VERTICAL_GRAVITY_MASK = 112; 54 | */ 55 | 56 | // 0001 0001 57 | static const Center = 17; 58 | // 0011 0000 59 | static const Top = 48; 60 | // 0101 0000 61 | static const Bottom = 80; 62 | // 0000 0011 63 | static const Left = 3; 64 | // 0000 0101 65 | static const Right = 5; 66 | 67 | static final _values = { 68 | GravityType.Center: Center, 69 | GravityType.CenterTop: Top | Center, 70 | GravityType.CenterBottom: Bottom | Center, 71 | GravityType.LeftTop: Top | Left, 72 | GravityType.LeftCenter: Center | Left, 73 | GravityType.LeftBottom: Bottom | Left, 74 | GravityType.RightTop: Top | Right, 75 | GravityType.RightCenter: Center | Right, 76 | GravityType.RightBottom: Bottom | Right, 77 | }; 78 | 79 | int? toInt() { 80 | return _values[this]; 81 | } 82 | 83 | GravityType? fromInt(int? v) { 84 | if (v == null) return null; 85 | var r = _values.keys 86 | .firstWhere((e) => _values[e] == v, orElse: () => GravityType.Unknown); 87 | return r == GravityType.Unknown ? null : r; 88 | } 89 | 90 | /// convert offset in topleft to others 91 | Offset apply( 92 | Offset o, { 93 | required double width, 94 | required double height, 95 | }) { 96 | var v = this.toInt(); 97 | if (v == null) return o; 98 | 99 | var dx = o.dx; 100 | var dy = o.dy; 101 | 102 | var halfWidth = width / 2; 103 | var halfHeight = height / 2; 104 | 105 | // calcute the x: & 0000 1111 = 15 106 | // 3 1 5 => -1 0 1 => 0 1 2 107 | // dx += ((v&15) / 2) * halfWidth; 108 | // if (v&15 == 1) { 109 | // dx += halfWidth; 110 | // } else if (v&15 == 2) { 111 | // dx += width; 112 | // } 113 | 114 | // // calcute the y: & 1111 0000 = 240 115 | // // 48 16 80 => 0 1 2 116 | // dy += ((v&240) / 2) * halfHeight; 117 | 118 | return Offset(dx, dy); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/src/event.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_floatwing/flutter_floatwing.dart'; 5 | 6 | typedef WindowListener = dynamic Function(Window window, dynamic data); 7 | 8 | /// events name 9 | enum EventType { 10 | WindowCreated, 11 | WindowStarted, 12 | WindowPaused, 13 | WindowResumed, 14 | WindowDestroy, 15 | 16 | WindowDragStart, 17 | WindowDragging, 18 | WindowDragEnd, 19 | } 20 | 21 | extension _EventType on EventType { 22 | static final _names = { 23 | EventType.WindowCreated: "window.created", 24 | EventType.WindowStarted: "window.started", 25 | EventType.WindowPaused: "window.paused", 26 | EventType.WindowResumed: "window.resumed", 27 | EventType.WindowDestroy: "window.destroy", 28 | EventType.WindowDragStart: "window.drag_start", 29 | EventType.WindowDragging: "window.dragging", 30 | EventType.WindowDragEnd: "window.drag_end", 31 | }; 32 | 33 | static EventType? fromString(String v) { 34 | EventType.values.firstWhere((e) => e.name == v); 35 | } 36 | 37 | String get name => _names[this]!; 38 | } 39 | 40 | /// Event is a common event 41 | class Event { 42 | /// id is window id 43 | String? id; 44 | 45 | /// name is the event name 46 | String? name; 47 | 48 | /// data is the payload for event 49 | dynamic data; 50 | 51 | Event({ 52 | this.id, 53 | this.name, 54 | this.data, 55 | }); 56 | 57 | factory Event.fromMap(Map map) { 58 | return Event(id: map["id"], name: map["name"], data: map["data"]); 59 | } 60 | } 61 | 62 | // final SendPort? _send = IsolateNameServer.lookupPortByName(SEND_PORT_NAME); 63 | class EventManager { 64 | EventManager._(this._msgChannel) { 65 | // set just for window, so window have no need to do this 66 | _msgChannel.setMessageHandler((msg) { 67 | var map = msg as Map?; 68 | if (map == null) { 69 | log("[event] unsupported message, we except a map"); 70 | } 71 | var evt = Event.fromMap(map!); 72 | var rs = sink(evt); 73 | log("[event] handled event: ${evt.name}, handlers: ${rs.length}"); 74 | return Future.value(null); 75 | }); 76 | } 77 | 78 | // event listenders 79 | // because enum from string O(n), so just use string 80 | // Map>> _listeners = {}; 81 | // w.id -> type -> w -> [cb] 82 | Map>>> _listeners = {}; 83 | 84 | Map> _windows = {}; 85 | 86 | BasicMessageChannel _msgChannel; 87 | 88 | // make sure one channel must only have one instance 89 | static final Map _instances = {}; 90 | 91 | factory EventManager( 92 | BasicMessageChannel _msgChannel, { 93 | Window? window, 94 | }) { 95 | if (_instances[_msgChannel.name] == null) { 96 | _instances[_msgChannel.name] = EventManager._(_msgChannel); 97 | } 98 | 99 | var current = _instances[_msgChannel.name]!; 100 | 101 | // store the window which create the event manager 102 | if (window != null) { 103 | if (current._windows[window.id] == null) current._windows[window.id] = []; 104 | current._windows[window.id]!.add(window); 105 | } 106 | 107 | // make sure one message channel only one event manager 108 | return current; 109 | } 110 | 111 | List sink(Event evt) { 112 | var res = []; 113 | // w.id -> type -> w -> [cb] 114 | 115 | // get windows 116 | var ws = (_listeners[evt.id] ?? {})[evt.name] ?? {}; 117 | ws.forEach((w, cbs) { 118 | (cbs).forEach((c) { 119 | res.add(c(w, evt.data)); 120 | }); 121 | }); 122 | return res; 123 | } 124 | 125 | EventManager on(Window window, EventType type, WindowListener callback) { 126 | var key = type.name; 127 | log("[event] register listener $key for $window"); 128 | // w.id -> w -> type -> [cb] 129 | if (_listeners[window.id] == null) _listeners[window.id] = {}; 130 | if (_listeners[window.id]![key] == null) _listeners[window.id]![key] = {}; 131 | if (_listeners[window.id]![key]![window] == null) 132 | _listeners[window.id]![key]![window] = []; 133 | if (!_listeners[window.id]![key]![window]!.contains(callback)) 134 | _listeners[window.id]![key]![window]!.add(callback); 135 | return this; 136 | } 137 | 138 | @override 139 | String toString() { 140 | return "EventManager@${super.hashCode}"; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /lib/src/plugin.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | import 'dart:ui'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:flutter_floatwing/flutter_floatwing.dart'; 8 | import 'package:flutter_floatwing/src/utils.dart'; 9 | import 'package:flutter_floatwing/src/window.dart'; 10 | 11 | class FloatwingPlugin { 12 | FloatwingPlugin._() { 13 | WidgetsFlutterBinding.ensureInitialized(); 14 | 15 | // make sure this only be called once 16 | // what happens when multiple window instances 17 | // are created and register event handlers? 18 | // Window().on(): id -> [Window, Window] 19 | // _eventManager = EventManager(_msgChannel); 20 | 21 | // _bgChannel.setMethodCallHandler((call) { 22 | // var id = call.arguments as String; 23 | // // if we are window egine, should call main engine 24 | // FloatwingPlugin().windows[id]?.eventManager?.sink(call.method, call.arguments); 25 | // switch (call.method) { 26 | 27 | // } 28 | // return Future.value(null); 29 | // }); 30 | } 31 | 32 | static const String channelID = "im.zoe.labs/flutter_floatwing"; 33 | 34 | static final MethodChannel _channel = MethodChannel('$channelID/method'); 35 | 36 | static final MethodChannel _bgChannel = MethodChannel('$channelID/bg_method'); 37 | 38 | static final BasicMessageChannel _msgChannel = 39 | BasicMessageChannel('$channelID/bg_message', JSONMessageCodec()); 40 | 41 | static final FloatwingPlugin _instance = FloatwingPlugin._(); 42 | 43 | /// event manager 44 | // EventManager? _eventManager; 45 | 46 | /// flag for inited 47 | bool _inited = false; 48 | 49 | /// permission granted already 50 | /// 51 | bool? _permissionGranted; 52 | 53 | /// service running already 54 | /// 55 | bool? _serviceRunning; 56 | 57 | /// _windows for the main engine to manage the windows started 58 | /// items added by start function 59 | Map _windows = {}; 60 | 61 | /// reutrn all windows only works for main engine 62 | Map get windows => 63 | _windows; // _windows.entries.map((e) => e.value).toList(); 64 | 65 | /// _window for the sub window engine to manage it's self 66 | /// setted after window's engine start and initital call 67 | Window? _window; 68 | 69 | /// return current window for window's engine 70 | Window? get currentWindow => _window; 71 | 72 | /// i'm window engine, default is the main engine 73 | /// if we sync success, we set to true. 74 | bool get isWindow => _isWindow; 75 | bool _isWindow = false; 76 | 77 | factory FloatwingPlugin() { 78 | return _instance; 79 | } 80 | 81 | FloatwingPlugin get instance { 82 | return _instance; 83 | } 84 | 85 | /// sync make the plugin to sync windows from services 86 | Future syncWindows() async { 87 | var _ws = await _channel.invokeListMethod("plugin.sync_windows"); 88 | _ws?.forEach((e) { 89 | var w = Window.fromMap(e); 90 | _windows[w.id] = w; 91 | }); 92 | return true; 93 | } 94 | 95 | Future initialize() async { 96 | if (_inited) return false; 97 | _inited = true; 98 | 99 | // get the callback id 100 | // final CallbackHandle _cbId = PluginUtilities.getCallbackHandle(_callback)!; 101 | // if service started will return all windows 102 | var map = await _channel.invokeMapMethod("plugin.initialize", { 103 | // "start_service": true, 104 | // "callback": _callback, 105 | 106 | // DEPRECATED: use system 107 | "pixelRadio": window.devicePixelRatio, 108 | 109 | "system": SystemConfig().toMap(), 110 | }); 111 | 112 | log("[plugin] initialize result: $map"); 113 | 114 | _serviceRunning = map?["service_running"]; 115 | _permissionGranted = map?["permission_grated"]; 116 | 117 | var _ws = map?["windows"] as List?; 118 | _ws?.forEach((e) { 119 | var w = Window.fromMap(e); 120 | _windows[w.id] = w; 121 | }); 122 | 123 | log("[plugin] there are ${_windows.length} windows already started"); 124 | 125 | return true; 126 | } 127 | 128 | Future checkPermission() async { 129 | return await _channel.invokeMethod("plugin.has_permission"); 130 | } 131 | 132 | Future openPermissionSetting() async { 133 | return await _channel.invokeMethod("plugin.open_permission_setting"); 134 | } 135 | 136 | Future isServiceRunning() async { 137 | return await _channel.invokeMethod("plugin.is_service_running"); 138 | } 139 | 140 | Future startService() async { 141 | return await _channel.invokeMethod("plugin.start_service"); 142 | } 143 | 144 | Future cleanCache() async { 145 | return await _channel.invokeMethod("plugin.clean_cache"); 146 | } 147 | 148 | /// create window to create a window 149 | Future createWindow( 150 | String? id, 151 | WindowConfig config, { 152 | bool start = false, // start immediately if true 153 | Window? window, 154 | }) async { 155 | var w = isWindow 156 | ? await currentWindow?.createChildWindow(id, config, 157 | start: start, window: window) 158 | : await internalCreateWindow(id, config, 159 | start: start, window: window, channel: _channel); 160 | if (w == null) return null; 161 | // store current window for window engine 162 | // for window engine use, update the current window 163 | // if we use create_window first? 164 | // _window = w; // we should don't use create_window first!!! 165 | // store the window to cache 166 | _windows[w.id] = w; 167 | return w; 168 | } 169 | 170 | // create window object for main engine 171 | Future internalCreateWindow( 172 | String? id, 173 | WindowConfig config, { 174 | bool start = false, // start immediately if true 175 | Window? window, 176 | required MethodChannel channel, 177 | String name = "plugin.create_window", 178 | }) async { 179 | // check permission first 180 | if (!await checkPermission()) { 181 | throw Exception("no permission to create window"); 182 | } 183 | 184 | // store the window first 185 | // window.id can't be updated 186 | // for main engine use 187 | // if (window != null) _windows[window.id] = window; 188 | var updates = await channel.invokeMapMethod(name, { 189 | "id": id, 190 | "config": config.toMap(), 191 | "start": start, 192 | }); 193 | // if window is not created, new one 194 | return updates == null ? null : (window ?? Window()).applyMap(updates); 195 | } 196 | 197 | /// ensure window make sure the window object sync from android 198 | /// call this as soon at posible when engine start 199 | /// you should only call this in the window engine 200 | /// if only main as entry point, it's ok to call this 201 | /// and return nothing 202 | // only window engine call this 203 | // make sure window engine return only one window from every where 204 | Future ensureWindow() async { 205 | // window object don't have sync method, we must do at here 206 | // assert if you are in main engine should call this 207 | var map = await Window.sync(); 208 | log("[window] sync window object from android: $map"); 209 | if (map == null) return null; 210 | // store current window if needed 211 | // use the static window first 212 | // so sync will return only one instance of window 213 | // improve this logic 214 | // means first time call sync, just create a new window 215 | if (_window == null) _window = Window(); 216 | _window!.applyMap(map); 217 | _isWindow = true; 218 | return _window; 219 | } 220 | 221 | /// `on` register event handlers for all windows 222 | /// or we can use stream mode 223 | FloatwingPlugin on(EventType type, WindowListener callback) { 224 | // TODO: 225 | return this; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /lib/src/provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/scheduler.dart'; 5 | import 'package:flutter_floatwing/flutter_floatwing.dart'; 6 | 7 | // typedef TransitionBuilder = Widget Function(BuildContext context, Widget? child); 8 | // typedef WidgetBuilder = Widget Function(BuildContext context); 9 | 10 | class FloatwingProvider extends InheritedWidget { 11 | final Window? window; 12 | final Widget child; 13 | 14 | FloatwingProvider({ 15 | Key? key, 16 | required this.child, 17 | required this.window, 18 | }) : super(key: key, child: child); 19 | 20 | @override 21 | bool updateShouldNotify(FloatwingProvider oldWidget) { 22 | return true; 23 | } 24 | } 25 | 26 | class FloatwingContainer extends StatefulWidget { 27 | final Widget? child; 28 | final WidgetBuilder? builder; 29 | final bool debug; 30 | final bool app; 31 | 32 | const FloatwingContainer({ 33 | Key? key, 34 | this.child, 35 | this.builder, 36 | this.debug = false, 37 | this.app = false, 38 | }) : assert(child != null || builder != null), 39 | super(key: key); 40 | 41 | @override 42 | State createState() => _FloatwingContainerState(); 43 | } 44 | 45 | class _FloatwingContainerState extends State { 46 | Window? _window = FloatwingPlugin().currentWindow; 47 | 48 | var _ignorePointer = false; 49 | var _autosize = true; 50 | 51 | @override 52 | void initState() { 53 | super.initState(); 54 | initSyncState(); 55 | } 56 | 57 | initSyncState() async { 58 | // send started message to service 59 | // this make sure ui already 60 | if (_window == null) { 61 | log("[provider] have not sync window at init, need to do at here"); 62 | await FloatwingPlugin().ensureWindow().then((w) => _window = w); 63 | } 64 | // init window from engine and save, only call this int here 65 | // sync a window from engine 66 | _changed(); 67 | _window?.on(EventType.WindowResumed, (w, _) => _changed()); 68 | } 69 | 70 | Widget _empty = Container(); 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | // make sure window is ready? 75 | if (!widget.debug && _window == null) return _empty; 76 | // in production, make sure builder when window is ready 77 | return Builder(builder: widget.builder ?? (_) => widget.child!) 78 | ._provider(_window) 79 | ._autosize(enabled: _autosize, onChange: _onSizeChanged) 80 | ._material(color: Colors.transparent) 81 | ._pointerless(_ignorePointer) 82 | ._app(enabled: widget.app, debug: widget.debug); 83 | } 84 | 85 | @override 86 | void dispose() { 87 | super.dispose(); 88 | // TODO: remove event listener 89 | // w.un("resumed").un("") 90 | } 91 | 92 | _changed() async { 93 | // clickable == !ignorePointer 94 | _ignorePointer = !(_window?.config?.clickable ?? true); 95 | _autosize = _window?.config?.autosize ?? true; 96 | // update the flutter ui 97 | if (mounted) setState(() {}); 98 | } 99 | 100 | _onSizeChanged(Size size) { 101 | var radio = _window?.pixelRadio ?? 1; 102 | _window?.update(WindowConfig( 103 | width: (size.width * radio).toInt(), 104 | height: (size.height * radio).toInt(), 105 | )); 106 | } 107 | } 108 | 109 | class _MeasuredSized extends StatefulWidget { 110 | const _MeasuredSized({ 111 | Key? key, 112 | required this.onChange, 113 | required this.child, 114 | this.delay = 0, 115 | }) : super(key: key); 116 | 117 | final Widget child; 118 | 119 | final int delay; 120 | 121 | final void Function(Size size)? onChange; 122 | 123 | @override 124 | _MeasuredSizedState createState() => _MeasuredSizedState(); 125 | } 126 | 127 | class _MeasuredSizedState extends State<_MeasuredSized> { 128 | @override 129 | void initState() { 130 | SchedulerBinding.instance!.addPostFrameCallback(postFrameCallback); 131 | super.initState(); 132 | } 133 | 134 | @override 135 | Widget build(BuildContext context) { 136 | if (widget.onChange == null) return widget.child; 137 | SchedulerBinding.instance!.addPostFrameCallback(postFrameCallback); 138 | return UnconstrainedBox( 139 | child: Container( 140 | key: widgetKey, 141 | child: NotificationListener( 142 | onNotification: (_) { 143 | SchedulerBinding.instance?.addPostFrameCallback(postFrameCallback); 144 | return true; 145 | }, 146 | child: SizeChangedLayoutNotifier(child: widget.child), 147 | ), 148 | ), 149 | ); 150 | } 151 | 152 | final widgetKey = GlobalKey(); 153 | Size? oldSize; 154 | 155 | void postFrameCallback(Duration _) async { 156 | final context = widgetKey.currentContext!; 157 | 158 | if (widget.delay > 0) 159 | await Future.delayed(Duration(milliseconds: widget.delay)); 160 | if (mounted == false) return; 161 | 162 | final newSize = context.size!; 163 | if (newSize == Size.zero) return; 164 | // if (oldSize == newSize) return; 165 | oldSize = newSize; 166 | widget.onChange!(newSize); 167 | } 168 | } 169 | 170 | typedef DragCallback = void Function(Offset offset); 171 | 172 | class _DragAnchor extends StatefulWidget { 173 | final Widget child; 174 | // TODO: 175 | // final bool horizontal; 176 | // final bool vertical; 177 | 178 | // final DragCallback? onDragStart; 179 | // final DragCallback? onDragUpdate; 180 | // final DragCallback? onDragEnd; 181 | 182 | const _DragAnchor({ 183 | Key? key, 184 | required this.child, 185 | 186 | // this.horizontal = true, 187 | // this.vertical = true, 188 | 189 | // this.onDragStart, 190 | // this.onDragUpdate, 191 | // this.onDragEnd, 192 | }) : super(key: key); 193 | 194 | @override 195 | State<_DragAnchor> createState() => _DragAnchorState(); 196 | } 197 | 198 | class _DragAnchorState extends State<_DragAnchor> { 199 | @override 200 | Widget build(BuildContext context) { 201 | // return Draggable(); 202 | return GestureDetector( 203 | onTapDown: _enableDrag, 204 | onTapUp: _disableDrag2, 205 | onTapCancel: _disableDrag, 206 | child: widget.child, 207 | ); 208 | } 209 | 210 | _enableDrag(_) { 211 | // enabe drag 212 | Window.of(context)?.update(WindowConfig( 213 | draggable: true, 214 | )); 215 | } 216 | 217 | _disableDrag() { 218 | // disable drag 219 | Window.of(context)?.update(WindowConfig( 220 | draggable: false, 221 | )); 222 | } 223 | 224 | _disableDrag2(_) { 225 | _disableDrag(); 226 | } 227 | } 228 | 229 | class _ResizeAnchor extends StatefulWidget { 230 | final Widget child; 231 | 232 | final bool horizontal; 233 | final bool vertical; 234 | 235 | const _ResizeAnchor({ 236 | Key? key, 237 | required this.child, 238 | this.horizontal = true, 239 | this.vertical = true, 240 | }) : super(key: key); 241 | 242 | @override 243 | State<_ResizeAnchor> createState() => __ResizeAnchorState(); 244 | } 245 | 246 | class __ResizeAnchorState extends State<_ResizeAnchor> { 247 | @override 248 | Widget build(BuildContext context) { 249 | return GestureDetector( 250 | onScaleStart: (v) { 251 | print("=======> scale start $v"); 252 | }, 253 | onScaleUpdate: (v) { 254 | print("=======> scale update $v"); 255 | }, 256 | onScaleEnd: (v) { 257 | print("=======> scale end $v"); 258 | }, 259 | child: widget.child, 260 | ); 261 | } 262 | } 263 | 264 | extension WidgetProviderExtension on Widget { 265 | /// Export floatwing extension function to inject for root widget 266 | Widget floatwing({ 267 | bool debug = false, 268 | bool app = false, 269 | }) { 270 | return FloatwingContainer(child: this, debug: debug, app: app); 271 | } 272 | 273 | /// Export draggable extension function to inject for child widget 274 | // Widget draggable({ 275 | // bool enabled = true, 276 | // }) { 277 | // return enabled?_DragAnchor(child: this):this; 278 | // } 279 | 280 | /// Export resizable extension function to inject for child 281 | // Widget resizable({ 282 | // bool enabled = true, 283 | // }) { 284 | // return enabled?_ResizeAnchor(child: this):this; 285 | // } 286 | 287 | Widget _provider(Window? window) { 288 | return FloatwingProvider(child: this, window: window); 289 | } 290 | 291 | Widget _autosize({ 292 | bool enabled = false, 293 | void Function(Size)? onChange, 294 | int delay = 0, 295 | }) { 296 | return !enabled 297 | ? this 298 | : _MeasuredSized(child: this, delay: delay, onChange: onChange); 299 | } 300 | 301 | Widget _pointerless([bool ignoring = false]) { 302 | return IgnorePointer(child: this, ignoring: ignoring); 303 | } 304 | 305 | Widget _material({ 306 | bool enabled = false, 307 | Color? color, 308 | }) { 309 | return !enabled ? this : Material(color: color, child: this); 310 | } 311 | 312 | Widget _app({ 313 | bool enabled = false, 314 | bool debug = false, 315 | }) { 316 | return !enabled 317 | ? this 318 | : MaterialApp(debugShowCheckedModeBanner: debug, home: this); 319 | } 320 | } 321 | 322 | extension WidgetBuilderProviderExtension on WidgetBuilder { 323 | WidgetBuilder floatwing({ 324 | bool debug = false, 325 | bool app = false, 326 | }) { 327 | return (_) => FloatwingContainer( 328 | builder: this, 329 | debug: debug, 330 | app: app, 331 | ); 332 | } 333 | 334 | Widget make() { 335 | return Builder(builder: this); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | class SystemConfig { 4 | int? pixelRadio; 5 | int? screenWidth; 6 | int? screenHeight; 7 | 8 | Size? screenSize; 9 | 10 | SystemConfig._({ 11 | this.pixelRadio, 12 | this.screenWidth, 13 | this.screenHeight, 14 | }) { 15 | var w = screenWidth?.toDouble(); 16 | var h = screenHeight?.toDouble(); 17 | if (w != null && h != null) screenSize = Size(w, h); 18 | } 19 | 20 | Map toMap() { 21 | return { 22 | "pixelRadio": pixelRadio, 23 | "screen": { 24 | "height": screenHeight, 25 | "width": screenWidth, 26 | }, 27 | }; 28 | } 29 | 30 | @override 31 | String toString() { 32 | return "${toMap()} ${screenSize}"; 33 | } 34 | 35 | factory SystemConfig() { 36 | return SystemConfig._( 37 | pixelRadio: window.devicePixelRatio.toInt(), 38 | screenHeight: window.physicalSize.height.toInt(), 39 | screenWidth: window.physicalSize.width.toInt(), 40 | ); 41 | } 42 | 43 | factory SystemConfig.fromMap(Map map) { 44 | var screen = map["screen"] ?? {}; 45 | return SystemConfig._( 46 | pixelRadio: map["pixelRadio"], 47 | screenHeight: screen["height"], 48 | screenWidth: screen["width"], 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/window.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'dart:convert'; 6 | import 'package:flutter_floatwing/flutter_floatwing.dart'; 7 | 8 | typedef OnDataHanlder = Future Function( 9 | String? source, String? name, dynamic data); 10 | 11 | class Window { 12 | String id = "default"; 13 | WindowConfig? config; 14 | 15 | double? pixelRadio; 16 | SystemConfig? system; 17 | OnDataHanlder? _onDataHandler; 18 | 19 | late EventManager _eventManager; 20 | 21 | Window({this.id = "default", this.config}) { 22 | _eventManager = EventManager(_message, window: this); 23 | 24 | // share data use the call 25 | _channel.setMethodCallHandler((call) { 26 | switch (call.method) { 27 | case "data.share": 28 | { 29 | var map = call.arguments as Map; 30 | // source, name, data 31 | // if not provided, should not call this 32 | return _onDataHandler?.call( 33 | map["source"], map["name"], map["data"]) ?? 34 | Future.value(null); 35 | } 36 | } 37 | return Future.value(null); 38 | }); 39 | } 40 | 41 | static final MethodChannel _channel = 42 | MethodChannel('${FloatwingPlugin.channelID}/window'); 43 | static final BasicMessageChannel _message = BasicMessageChannel( 44 | '${FloatwingPlugin.channelID}/window_msg', JSONMessageCodec()); 45 | 46 | factory Window.fromMap(Map? map) { 47 | return Window().applyMap(map); 48 | } 49 | 50 | @override 51 | String toString() { 52 | return "Window[$id]@${super.hashCode}, ${_eventManager.toString()}, config: $config"; 53 | } 54 | 55 | Window applyMap(Map? map) { 56 | // apply the map to config and object 57 | if (map == null) return this; 58 | id = map["id"]; 59 | pixelRadio = map["pixelRadio"] ?? 1.0; 60 | system = SystemConfig.fromMap(map["system"] ?? {}); 61 | config = WindowConfig.fromMap(map["config"]); 62 | return this; 63 | } 64 | 65 | /// `of` extact window object window from context 66 | /// The data from the closest instance of this class that encloses the given 67 | /// context. 68 | static Window? of(BuildContext context) { 69 | return context 70 | .dependOnInheritedWidgetOfExactType() 71 | ?.window; 72 | } 73 | 74 | Future hide() { 75 | return show(visible: false); 76 | // return FloatwingPlugin().showWindow(id, false); 77 | } 78 | 79 | Future close({bool force = false}) async { 80 | // return await FloatwingPlugin().closeWindow(id, force: force); 81 | return await _channel.invokeMethod("window.close", { 82 | "id": id, 83 | "force": force, 84 | }).then((v) { 85 | // remove the window from plugin 86 | FloatwingPlugin().windows.remove(id); 87 | return v; 88 | }); 89 | } 90 | 91 | Future create({bool start = false}) async { 92 | // // create the engine first 93 | return await FloatwingPlugin() 94 | .createWindow(this.id, this.config!, start: start, window: this); 95 | } 96 | 97 | /// create child window 98 | /// just method shoudld only called in window engine 99 | Future createChildWindow( 100 | String? id, 101 | WindowConfig config, { 102 | bool start = false, // start immediately if true 103 | Window? window, 104 | }) async { 105 | return FloatwingPlugin().internalCreateWindow(id, config, 106 | start: start, 107 | window: window, 108 | channel: _channel, 109 | name: "window.create_child"); 110 | } 111 | 112 | Future start() async { 113 | assert(config != null, "config can't be null"); 114 | return await _channel.invokeMethod("window.start", { 115 | "id": id, 116 | }); 117 | // return await FloatwingPlugin().startWindow(id); 118 | } 119 | 120 | Future update(WindowConfig cfg) async { 121 | // update window with config, config con't update with id, entry, route 122 | var size = config?.size; 123 | if (size != null && size < Size.zero) { 124 | // special case, should updated 125 | cfg.width = null; 126 | cfg.height = null; 127 | } 128 | var updates = await _channel.invokeMapMethod("window.update", { 129 | "id": id, 130 | // don't set pixelRadio 131 | "config": cfg.toMap(), 132 | }); 133 | // var updates = await FloatwingPlugin().updateWindow(id, cfg); 134 | // update the plugin store 135 | applyMap(updates); 136 | return true; 137 | } 138 | 139 | Future show({bool visible = true}) async { 140 | config?.visible = visible; 141 | return await _channel.invokeMethod("window.show", { 142 | "id": id, 143 | "visible": visible, 144 | }).then((v) { 145 | // update the plugin store 146 | if (v) FloatwingPlugin().windows[id]?.config?.visible = visible; 147 | return v; 148 | }); 149 | } 150 | 151 | /// share data with current window 152 | /// send data use current window id as target id 153 | /// and get value return 154 | Future share( 155 | dynamic data, { 156 | String name = "default", 157 | }) async { 158 | var map = {}; 159 | map["target"] = id; 160 | map["data"] = data; 161 | map["name"] = name; 162 | // make sure data is serialized 163 | return await _channel.invokeMethod("data.share", map); 164 | } 165 | 166 | /// launch main activity 167 | Future launchMainActivity() async { 168 | return await _channel.invokeMethod("window.launch_main"); 169 | } 170 | 171 | /// on data to receive data from other shared 172 | /// maybe same like event handler 173 | /// but one window in engine can only have one data handler 174 | /// to make sure data not be comsumed multiple times. 175 | Window onData(OnDataHanlder handler) { 176 | assert(_onDataHandler == null, "onData can only called once"); 177 | _onDataHandler = handler; 178 | return this; 179 | } 180 | 181 | // sync window object from android service 182 | // only window engine call this 183 | // if we manage other windows in some window engine 184 | // this will not works, we must improve it 185 | static Future?> sync() async { 186 | return await _channel.invokeMapMethod("window.sync"); 187 | } 188 | 189 | /// on register callback to listener 190 | Window on(EventType type, WindowListener callback) { 191 | _eventManager.on(this, type, callback); 192 | return this; 193 | } 194 | 195 | Map toMap() { 196 | var map = Map(); 197 | map["id"] = id; 198 | map["pixelRadio"] = pixelRadio; 199 | map["config"] = config?.toMap(); 200 | return map; 201 | } 202 | } 203 | 204 | class WindowConfig { 205 | String? id; 206 | 207 | String? entry; 208 | String? route; 209 | Function? callback; // use callback to start engine 210 | 211 | bool? autosize; 212 | 213 | int? width; 214 | int? height; 215 | int? x; 216 | int? y; 217 | 218 | int? format; 219 | GravityType? gravity; 220 | int? type; 221 | 222 | bool? clickable; 223 | bool? draggable; 224 | bool? focusable; 225 | 226 | /// immersion status bar 227 | bool? immersion; 228 | 229 | bool? visible; 230 | 231 | /// we need this for update, so must wihtout default value 232 | WindowConfig({ 233 | this.id = "default", 234 | this.entry = "main", 235 | this.route, 236 | this.callback, 237 | this.autosize, 238 | this.width, 239 | this.height, 240 | this.x, 241 | this.y, 242 | this.format, 243 | this.gravity, 244 | this.type, 245 | this.clickable, 246 | this.draggable, 247 | this.focusable, 248 | this.immersion, 249 | this.visible, 250 | }) : assert( 251 | callback == null || 252 | PluginUtilities.getCallbackHandle(callback) != null, 253 | "callback is not a static function"); 254 | 255 | factory WindowConfig.fromMap(Map map) { 256 | var _cb; 257 | if (map["callback"] != null) 258 | _cb = PluginUtilities.getCallbackFromHandle( 259 | CallbackHandle.fromRawHandle(map["callback"])); 260 | return WindowConfig( 261 | // id: map["id"], 262 | entry: map["entry"], 263 | route: map["route"], 264 | callback: _cb, // get the callback from id 265 | 266 | autosize: map["autosize"], 267 | 268 | width: map["width"], 269 | height: map["height"], 270 | x: map["x"], 271 | y: map["y"], 272 | 273 | format: map["format"], 274 | gravity: GravityType.Unknown.fromInt(map["gravity"]), 275 | type: map["type"], 276 | 277 | clickable: map["clickable"], 278 | draggable: map["draggable"], 279 | focusable: map["focusable"], 280 | 281 | immersion: map["immersion"], 282 | 283 | visible: map["visible"], 284 | ); 285 | } 286 | 287 | Map toMap() { 288 | var map = Map(); 289 | // map["id"] = id; 290 | map["entry"] = entry; 291 | map["route"] = route; 292 | // find the callback id from callback function 293 | map["callback"] = callback != null 294 | ? PluginUtilities.getCallbackHandle(callback!)?.toRawHandle() 295 | : null; 296 | 297 | map["autosize"] = autosize; 298 | 299 | map["width"] = width; 300 | map["height"] = height; 301 | map["x"] = x; 302 | map["y"] = y; 303 | 304 | map["format"] = format; 305 | map["gravity"] = gravity?.toInt(); 306 | map["type"] = type; 307 | 308 | map["clickable"] = clickable; 309 | map["draggable"] = draggable; 310 | map["focusable"] = focusable; 311 | 312 | map["immersion"] = immersion; 313 | 314 | map["visible"] = visible; 315 | 316 | return map; 317 | } 318 | 319 | // return a window frm config 320 | Window to() { 321 | // will lose window instance 322 | return Window(id: this.id ?? "default", config: this); 323 | } 324 | 325 | Future create({ 326 | String? id = "default", 327 | bool start = false, 328 | }) async { 329 | assert(!(entry == "main" && route == null)); 330 | return await FloatwingPlugin().createWindow(id, this, start: start); 331 | } 332 | 333 | Size get size => Size((width ?? 0).toDouble(), (height ?? 0).toDouble()); 334 | 335 | @override 336 | String toString() { 337 | var map = this.toMap(); 338 | map.removeWhere((key, value) => value == null); 339 | return json.encode(map).toString(); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.18.0" 44 | fake_async: 45 | dependency: transitive 46 | description: 47 | name: fake_async 48 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.3.1" 52 | flutter: 53 | dependency: "direct main" 54 | description: flutter 55 | source: sdk 56 | version: "0.0.0" 57 | flutter_test: 58 | dependency: "direct dev" 59 | description: flutter 60 | source: sdk 61 | version: "0.0.0" 62 | leak_tracker: 63 | dependency: transitive 64 | description: 65 | name: leak_tracker 66 | sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" 67 | url: "https://pub.dev" 68 | source: hosted 69 | version: "10.0.0" 70 | leak_tracker_flutter_testing: 71 | dependency: transitive 72 | description: 73 | name: leak_tracker_flutter_testing 74 | sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 75 | url: "https://pub.dev" 76 | source: hosted 77 | version: "2.0.1" 78 | leak_tracker_testing: 79 | dependency: transitive 80 | description: 81 | name: leak_tracker_testing 82 | sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "2.0.1" 86 | matcher: 87 | dependency: transitive 88 | description: 89 | name: matcher 90 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "0.12.16+1" 94 | material_color_utilities: 95 | dependency: transitive 96 | description: 97 | name: material_color_utilities 98 | sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "0.8.0" 102 | meta: 103 | dependency: transitive 104 | description: 105 | name: meta 106 | sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "1.11.0" 110 | path: 111 | dependency: transitive 112 | description: 113 | name: path 114 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "1.9.0" 118 | sky_engine: 119 | dependency: transitive 120 | description: flutter 121 | source: sdk 122 | version: "0.0.99" 123 | source_span: 124 | dependency: transitive 125 | description: 126 | name: source_span 127 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 128 | url: "https://pub.dev" 129 | source: hosted 130 | version: "1.10.0" 131 | stack_trace: 132 | dependency: transitive 133 | description: 134 | name: stack_trace 135 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "1.11.1" 139 | stream_channel: 140 | dependency: transitive 141 | description: 142 | name: stream_channel 143 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "2.1.2" 147 | string_scanner: 148 | dependency: transitive 149 | description: 150 | name: string_scanner 151 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "1.2.0" 155 | term_glyph: 156 | dependency: transitive 157 | description: 158 | name: term_glyph 159 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "1.2.1" 163 | test_api: 164 | dependency: transitive 165 | description: 166 | name: test_api 167 | sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "0.6.1" 171 | vector_math: 172 | dependency: transitive 173 | description: 174 | name: vector_math 175 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "2.1.4" 179 | vm_service: 180 | dependency: transitive 181 | description: 182 | name: vm_service 183 | sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "13.0.0" 187 | sdks: 188 | dart: ">=3.2.0-0 <4.0.0" 189 | flutter: ">=1.20.0" 190 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_floatwing 2 | description: A Flutter plugin that makes it easier to make floating/overlay window for Android with pure Flutter. 3 | version: 0.2.1 4 | homepage: https://github.com/jiusanzhou/flutter_floatwing 5 | 6 | environment: 7 | sdk: '>=3.0.0 <4.0.0' 8 | flutter: ">=1.20.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | # For information on the generic Dart part of this file, see the 19 | # following page: https://dart.dev/tools/pub/pubspec 20 | 21 | # The following section is specific to Flutter. 22 | flutter: 23 | # This section identifies this Flutter project as a plugin project. 24 | # The 'pluginClass' and Android 'package' identifiers should not ordinarily 25 | # be modified. They are used by the tooling to maintain consistency when 26 | # adding or updating assets for this project. 27 | plugin: 28 | platforms: 29 | android: 30 | package: im.zoe.labs.flutter_floatwing 31 | pluginClass: FlutterFloatwingPlugin 32 | 33 | # To add assets to your plugin package, add an assets section, like this: 34 | # assets: 35 | # - images/a_dot_burr.jpeg 36 | # - images/a_dot_ham.jpeg 37 | # 38 | # For details regarding assets in packages, see 39 | # https://flutter.dev/assets-and-images/#from-packages 40 | # 41 | # An image asset can refer to one or more resolution-specific "variants", see 42 | # https://flutter.dev/assets-and-images/#resolution-aware. 43 | 44 | # To add custom fonts to your plugin package, add a fonts section here, 45 | # in this "flutter" section. Each entry in this list should have a 46 | # "family" key with the font family name, and a "fonts" key with a 47 | # list giving the asset and other descriptors for the font. For 48 | # example: 49 | # fonts: 50 | # - family: Schyler 51 | # fonts: 52 | # - asset: fonts/Schyler-Regular.ttf 53 | # - asset: fonts/Schyler-Italic.ttf 54 | # style: italic 55 | # - family: Trajan Pro 56 | # fonts: 57 | # - asset: fonts/TrajanPro.ttf 58 | # - asset: fonts/TrajanPro_Bold.ttf 59 | # weight: 700 60 | # 61 | # For details regarding fonts in packages, see 62 | # https://flutter.dev/custom-fonts/#from-packages 63 | -------------------------------------------------------------------------------- /test/flutter_floatwing_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:flutter_floatwing/flutter_floatwing.dart'; 4 | 5 | void main() { 6 | const MethodChannel channel = MethodChannel('flutter_floatwing'); 7 | 8 | TestWidgetsFlutterBinding.ensureInitialized(); 9 | 10 | setUp(() { 11 | channel.setMockMethodCallHandler((MethodCall methodCall) async { 12 | return '42'; 13 | }); 14 | }); 15 | 16 | tearDown(() { 17 | channel.setMockMethodCallHandler(null); 18 | }); 19 | 20 | test('getPlatformVersion', () async { 21 | // expect(await FlutterFloatwing.platformVersion, '42'); 22 | }); 23 | } 24 | --------------------------------------------------------------------------------