├── .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 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.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 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
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 | [](https://pub.dartlang.org/packages/flutter_floatwing)
6 | [](https://pub.dev/packages/flutter_floatwing/score)
7 | [](https://pub.dev/packages/flutter_floatwing/score)
8 | [](https://pub.dev/packages/flutter_floatwing/score)
9 | [](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 | ||||
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 | [](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 | 
120 |
121 | The whole view hierarchy like below:
122 |
123 | 
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 |
--------------------------------------------------------------------------------