├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_CN.md ├── example ├── .gitignore ├── assets │ ├── airpos.png │ ├── appletv.png │ ├── icon_apple.png │ ├── icon_apple2.png │ ├── icon_color.png │ ├── iphone.png │ ├── switch.png │ └── watch.png ├── lib │ ├── color.dart │ └── main.dart └── pubspec.yaml ├── lib └── ffloat.dart ├── pubspec.yaml └── test └── ffloat_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | 9 | .idea/ 10 | 11 | .metadata 12 | 13 | *.iml 14 | 15 | pubspec.lock 16 | 17 | 18 | android/ 19 | ios/ 20 | macos/ 21 | 22 | readme.py 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.1 2 | - 适配 3.7 运算 3 | 4 | ## 2.0.0 5 | - 适配 3.7 6 | 7 | ## 1.0.2 8 | 9 | - FFloatController adds setState function to support external partial refresh of FFloat content 10 | 11 | ```dart 12 | controller.setState(() { 13 | // update your float ui 14 | }); 15 | ``` 16 | 17 | ## 1.0.1 18 | 19 | - Supports `alignment` configuration in absolute position mode 20 | 21 | - Fix FFloatController status monitoring problem 22 | 23 | ## 1.0.0 24 | 25 | - First version 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020-present Fliggy Android Team . 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 following link. 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 | 4 | 5 |

6 | 7 |

ffloat

8 | 9 | 10 |
11 | 12 |

FFloat, although simple and easy to use, can satisfy all your imagination of the floating layer.

13 | 14 |

Born and elegant, supporting precise position control. Triangles with rounded corners, borders, gradients, shadows? Everything you need 😃.️

15 | 16 |

Author:Newton(coorchice.cb@alibaba-inc.com)

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 |

47 |

48 | 49 | 50 | 51 |

52 | 53 | **English | [简体中文](https://github.com/Fliggy-Mobile/ffloat/blob/master/README_CN.md)** 54 | 55 | > Like it? Please cast your **Star** 🥰 ! 56 | 57 | # ✨ Features 58 | 59 | - Supports **floating layer position** control based on anchor elements 60 | 61 | - Convenient floating layer **show / hide control** 62 | 63 | - Comes with elegant **interactive effects** 64 | 65 | - Flexible and beautiful **decorative triangle** 66 | 67 | - Support precise control of **round corners** 68 | 69 | - Wonderful **gradient** effect support 70 | 71 | - Simple but powerful **border** support 72 | 73 | - Support absolute position **Dual mode** 74 | 75 | 76 | # 🛠 Guide 77 | 78 | ## ⚙️ Parameters 79 | 80 | 81 | ### 🔩 Basic parameters 82 | 83 | |Param|Type|Necessary|Default|desc| 84 | |---|---|:---:|---|---| 85 | |builder|FloatBuilder|true|null|[FloatBuilder] returns the content component of [FFloat]. If only the content area is updated, proceed with `setter (() {})`| 86 | |color|Color|false|`Color(0x7F000000)`|[FFloat] colors| 87 | |gradient|Gradient|false|null|Gradient. Will overwrite color| 88 | |anchor|Widget|false|null|Anchor element| 89 | |location|Offset|false|null|position. After specifying the location of [FFloat] through [location], all configurations that determine the location based on the anchor point will be invalid| 90 | |alignment|FFloatAlignment|false|FFloatAlignment.topCenter|[FFloat] Based on the relative position of the [anchor] anchor element.| 91 | |margin|EdgeInsets|false|EdgeInsets.zero|[FFloat] Determine the distance between anchor points based on relative| 92 | |padding|EdgeInsets|false|null|[FFloat] Internal spacing| 93 | |canTouchOutside|bool|false|false|Click [FFloat] to hide the area outside the range.| 94 | |backgroundColor|Color|false|Colors.transparent|[FFloat] The color of the background area when floating| 95 | |autoDismissDuration|Duration|false|null|Duration of automatic disappearance. If it is null, it will not disappear automatically| 96 | |controller|FFloatController|false|null|[FFloatController] can control the show/hide of [FFloat]. See [FFloatController] for details| 97 | |animDuration|Duration|false|`Duration(milliseconds: 100)`|Show/hide animation duration| 98 | 99 | ### 🔺 Triangle 100 | 101 | |Param|Type|Necessary|Default|desc| 102 | |---|---|:---:|---|---| 103 | |triangleWidth|double|false|12|The width of the triangle| 104 | |triangleHeight|double|false|6|The height of the triangle| 105 | |triangleAlignment|TriangleAlignment|false|TriangleAlignment.center|Relative position of triangle| 106 | |triangleOffset|Offset|false|Offset.zero|Triangle position offset| 107 | |hideTriangle|bool|false|false|Whether to hide the decorative triangle| 108 | 109 | ### 🔆 Corner & Border 110 | 111 | |Param|Type|Necessary|Default|desc| 112 | |---|---|:---:|---|---| 113 | |corner|FFloatCorner|false|null|corner| 114 | |cornerStyle|FFloatCornerStyle|false|FFloatCornerStyle.round|corner style| 115 | |strokeColor|Color|false|null|Stroke color| 116 | |strokeWidth|double|false|null|Stroke width| 117 | 118 | ### 🔳 Shadow parameters 119 | 120 | |Param|Type|Necessary|Default|desc| 121 | |---|---|:---:|---|---| 122 | |shadowColor|Color|false|null|Shadow color| 123 | |shadowOffset|Offset|false|null|Shadow offset| 124 | |shadowBlur|double|false|null|The larger the value, the greater the shadow| 125 | 126 | 127 | ## 📺 Demo 128 | 129 | ### 🔩 Basic Demo 130 | 131 | 132 | ![](https://gw.alicdn.com/tfs/TB1GwD9FhD1gK0jSZFyXXciOVXa-464-140.gif) 133 | 134 | ```dart 135 | FFloat( 136 | (_) => createContent(), 137 | controller: controller1, 138 | padding: EdgeInsets.only(left: 9, right: 9, top: 6, bottom: 6), 139 | corner: FFloatCorner.all(10), 140 | alignment: floatAlignment1, 141 | canTouchOutside: false, 142 | anchor: buildAnchor1(), 143 | ) 144 | ``` 145 | 146 | **FFloat** can wrap a normal component (that is, assign the normal component to the `anchor` parameter of **FFloat**), so that the component has the ability to click to pop up the floating layer. 147 | 148 | And **FFloat** will not have any adverse effect on the original components, which is amazing! 149 | 150 | 151 | Alternatively, you can control the display of the floating layer through **FFloatController**. Of course, the premise is that you need to create a **FFloatController**, and then assign it to the **FFloat** controller property. 152 | 153 | ```dart 154 | FFloatController controller = FFloatController(); 155 | 156 | FFloat( 157 | controller: controller, 158 | ) 159 | 160 | /// show float 161 | controller.show(); 162 | /// hide float 163 | controller.dismiss(); 164 | ``` 165 | 166 | **FFloat** is smart enough to automatically determine where it should appear based on the position of `anchor`. With `alignment` and` margin`, you can adjust the position of the floating layer in an incredibly simple way until you think it is ok. 167 | 168 | This is an unprecedented change 👍! 169 | 170 | In the past, if you wanted to display a floating layer based on the position of an element, then you had to go through a series of tedious operations to get the position of the element. Then coordinate conversion is performed, and the position is calculated according to the size of the floating layer. 171 | 172 | God, it's complicated enough just to think about it. Not to mention when you encounter a scene that needs to be centered and left aligned, it is a nightmare 👿. 173 | 174 | As for the content of the floating layer, just pass the `buidler` parameter and return a **Widget** in the **FloatBuilder** function. 175 | 176 | If your floating layer content needs to be refreshed, the **FloatBuilder** function provides a **StateSetter** parameter, through which you can refresh only the content in the floating layer without affecting the content outside the floating layer. It's really efficient. 177 | 178 | ```dart 179 | FFloat( 180 | (setter){ 181 | return GestureDetector( 182 | onTap:(){ 183 | setter((){ 184 | /// update something 185 | }); 186 | } 187 | anchor: buildWidgte()); 188 | }, 189 | anchor: buildAnchor() 190 | ) 191 | ``` 192 | 193 | 194 | ### 💫 Background & Animation 195 | 196 | ![](https://gw.alicdn.com/tfs/TB179P9GuH2gK0jSZFEXXcqMpXa-720-135.gif) 197 | 198 | ```dart 199 | FFloat( 200 | (_) => FSuper( 201 | text: "Surprise😃 !", 202 | textColor: Colors.white, 203 | ), 204 | controller: controller2_1, 205 | color: Color(0xff5D5D5E), 206 | corner: FFloatCorner.all(6), 207 | margin: EdgeInsets.only(bottom: 10), 208 | padding: EdgeInsets.only(left: 9, right: 9, top: 3, bottom: 3), 209 | anchor: buildAnchor(), 210 | canTouchOutside: false, 211 | autoDismissDuration: Duration(milliseconds: 2000), 212 | ), 213 | 214 | FFloat( 215 | (_) => buildSearch(), 216 | child2Alignment: Alignment.centerLeft, 217 | child2Margin: EdgeInsets.only(left: (9.0 + 18.0 + 9.0)), 218 | ), 219 | controller: controller2_2, 220 | color: Colors.black.withOpacity(0.95), 221 | backgroundColor: Colors.black26, 222 | corner: FFloatCorner.all(20), 223 | margin: EdgeInsets.only(bottom: 10, left: 10), 224 | anchor: buildAnchor(), 225 | alignment: FFloatAlignment.topRight, 226 | triangleAlignment: TriangleAlignment.end, 227 | triangleOffset: Offset(-39, 0), 228 | ) 229 | ``` 230 | 231 | When the floating layer of **FFloat** appears, you can choose whether you want a background color, by configuring `backgroundColor`. 232 | 233 | And through the `canTouchOutside` property, you set whether your floating layer can be closed by clicking the area outside the floating layer. 234 | 235 | When set to `canTouchOutside = false`, it often means that you need to control the hiding of the floating layer through a **FFloatController**. 236 | 237 | By default, **FFloat** comes with the **Scale** show/hide animation. 238 | 239 | According to the orientation of the floating layer you set, **FFloat** can intelligently determine the starting anchor point of the animation. This makes everything more natural. 240 | 241 | If you don't need the animation effect, just pass **null** through the `animDuration` parameter to cancel the animation effect and return to the more abrupt show/hide. 242 | 243 | When you configure the `autoDismissDuration` parameter, **FFloat** will enter auto-disappear mode. This means that after the floating layer pops up, it will automatically disappear at the time you expect. You don't need to intervene too much. 244 | 245 | 246 | ### 🔺 Decorative triangle 247 | 248 | ![](https://gw.alicdn.com/tfs/TB1L3JoFEH1gK0jSZSyXXXtlpXa-753-220.gif) 249 | 250 | ```dart 251 | FFloat( 252 | (setter) => buildContent(), 253 | shadowColor: Colors.black38, 254 | shadowBlur: 8.0, 255 | shadowOffset: Offset(2.0, 2.0), 256 | color: Colors.white, 257 | corner: FFloatCorner.all(3), 258 | controller: controller3_1, 259 | alignment: FFloatAlignment.bottomLeft, 260 | hideTriangle: true, 261 | anchor: buildAnchor(), 262 | ), 263 | 264 | FFloat( 265 | (setter) => buildContent(), 266 | controller: controller3_2, 267 | alignment: FFloatAlignment.bottomLeft, 268 | margin: EdgeInsets.only(top: 2), 269 | shadowColor: Colors.black38, 270 | shadowBlur: 8.0, 271 | shadowOffset: Offset(2.0, 2.0), 272 | corner: FFloatCorner.all(3), 273 | color: Colors.white, 274 | triangleAlignment: TriangleAlignment.start, 275 | triangleOffset: Offset(10, 10), 276 | triangleWidth: 20, 277 | triangleHeight: 15, 278 | anchor: buildAnchor(), 279 | ), 280 | ``` 281 | 282 | **FFloat** Intimately built-in decorative triangle. You can see it by default. If you don't want to get a floating layer with a decorative triangle, you can control its show/hide through the `hideTriangle` property. 283 | 284 | Again, **FFloat** is really smart. The decorative triangle can automatically determine where it should appear based on the relative position of the floating layer and the anchor element. It looks delicate enough. 285 | 286 | The theme styles of decorative triangles and floating layers are perfectly natural. In terms of color, **FFloat** completely saves you trouble. Whether it is solid color, border, gradient color, everything does not need additional processing. 287 | 288 | `triangleAlignment` provides an easy way to adjust the position of the decorative triangle. 289 | 290 | If you are still not satisfied, `triangleOffset` will allow you to further offset based on the relative position. For **FFloat**, nothing is impossible. 291 | 292 | Of course, **FFloat** will definitely prepare you to control the size of the decorative triangle, just like the problems to be solved by `triangleWidth` and` triangleHeight` 293 | 294 | ### 🔆 Corner & Stroke 295 | 296 | ![](https://gw.alicdn.com/tfs/TB1xbhyFqL7gK0jSZFBXXXZZpXa-670-241.gif) 297 | 298 | ```dart 299 | 300 | FFloat( 301 | (setter) { 302 | return buildContent(); 303 | }, 304 | anchor: buildAnchor(), 305 | controller: controller4, 306 | color: Colors.white, 307 | corner: FFloatCorner.all(6), 308 | strokeColor: mainShadowColor, 309 | strokeWidth: 1.0, 310 | alignment: FFloatAlignment.bottomLeft, 311 | hideTriangle: true, 312 | margin: EdgeInsets.only(top: 9), 313 | padding: EdgeInsets.only(top: 9, bottom: 9), 314 | ) 315 | ``` 316 | 317 | As you can see, a beautiful rounded floating layer with a border is so simple to build. 318 | 319 | **FFloat** provides the **FWidget** series of components in the same vein, a simple way to set the rounded corners, you can flexibly configure the rounded corners through a simple `corner` attribute. 320 | 321 | The `cornerStyle` property that comes with` corner` allows you to switch the style of rounded corners (rounded corners or beveled corners) at any time. 322 | 323 | If you are already a user of **FWidget**, I believe you already know, we configure the border effect for the component, only need to pass two simple properties `strokeWidth` and` strokeColor`. 324 | 325 | Our original intention was to help developer build beautiful applications more quickly. 326 | 327 | 328 | ### 🔳 Gradient & Shadow 329 | 330 | ![](https://gw.alicdn.com/tfs/TB11GHPFrj1gK0jSZFuXXcrHpXa-414-179.gif) 331 | 332 | ```dart 333 | 334 | FFloat( 335 | (setter) => buildContent(), 336 | anchor: buildAnchor(), 337 | controller: controller5, 338 | gradient: SweepGradient( 339 | colors: [ 340 | Color(0xffE271C0), 341 | Color(0xffC671EB), 342 | Color(0xff7673F3), 343 | Color(0xff8BEBEF), 344 | Color(0xff93FCA8), 345 | Color(0xff94FC9D), 346 | Color(0xffEDF980), 347 | Color(0xffF0C479), 348 | Color(0xffE07E77), 349 | ], 350 | ), 351 | corner: FFloatCorner.all(100), 352 | hideTriangle: true, 353 | margin: EdgeInsets.only(top: 9), 354 | alignment: FFloatAlignment.bottomCenter, 355 | shadowColor: Colors.black38, 356 | shadowBlur: 3, 357 | shadowOffset: Offset(3, 2), 358 | ) 359 | ``` 360 | 361 | Yes, **FFloat** still has support for gradients after combining many capabilities. 362 | 363 | Of course, just through a simple `gradient` attribute, you can get a beautiful gradient effect. 364 | 365 | In addition, as a modern component, **FFloat** will of course support shadows. 366 | 367 | You only need to get a basic shadow effect through the `shadowColor` property. If you want to further adjust the shadow, you can use the` shadowBlur` and `shadowOffset` properties. 368 | 369 | ### 📌 Absolute position Float 370 | 371 | ![](https://gw.alicdn.com/tfs/TB1gnz8GrH1gK0jSZFwXXc7aXXa-858-508.gif) 372 | 373 | ```dart 374 | GestureDetector( 375 | onPanDown: (details) { 376 | FFloat( 377 | (setter) => createContent(), 378 | autoDismissDuration: Duration(milliseconds: 2000), 379 | alignment: _alignment, 380 | canTouchOutside: false, 381 | 382 | /// Configure floating element position by absolute coordinates 383 | location: Offset(details.globalPosition.dx, details.globalPosition.dy), 384 | ).show(context); /// show 385 | }, 386 | child: FSuper(...), 387 | ) 388 | ``` 389 | 390 | In some cases, our floating element does not need to appear based on an anchor point, but hopes that it appears in a certain position. 391 | 392 | If the developer knows such a location, use the `location` parameter to set the location of **FFloat**. 393 | 394 | At this time, the developer does not need to put **FFloat** in the view tree to wrap any elements. This means that developer can create a floating element in any callback or function anytime, anywhere. 395 | 396 | Through `FFloat.show (context)` and `FFloat.dismiss ()`, developer can easily control **show / hide** of floating elements at any time. 397 | 398 | All other configurations of **FFloat** are still valid. 399 | 400 | ### 👏 More exciting 401 | 402 | ![](https://gw.alicdn.com/tfs/TB1NGfIFAL0gK0jSZFtXXXQCXXa-460-500.gif) 403 | 404 | In **FFloat**, the floating layer can automatically follow the movement of the anchor element, and you do n’t need to pay attention to the series of calculations caused by the position change. 405 | 406 | **FFloat** has handled it for you well enough! 407 | 408 | Let **FFloat** solve all your floating layer problems, you only need to beautify your application. 409 | 410 | # 😃 How to use? 411 | 412 | Add dependencies in the project `pubspec.yaml` file: 413 | 414 | ## 🌐 pub dependency 415 | 416 | ``` 417 | dependencies: 418 | ffloat: ^ 419 | ``` 420 | 421 | > ⚠️ Attention,please go to [**pub**] (https://pub.dev/packages/ffloat) to get the latest version number of **FFloat** 422 | 423 | ## 🖥 git dependencies 424 | 425 | ``` 426 | dependencies: 427 | ffloat: 428 | git: 429 | url: 'git@github.com:Fliggy-Mobile/ffloat.git' 430 | ref: '' 431 | ``` 432 | 433 | > ⚠️ Attention,please refer to [**FFloat**] (https://github.com/Fliggy-Mobile/ffloat) official project for branch number or tag. 434 | 435 | 436 | # 💡 License 437 | 438 | ``` 439 | Copyright 2020-present Fliggy Android Team . 440 | 441 | Licensed under the Apache License, Version 2.0 (the "License"); 442 | you may not use this file except in compliance with the License. 443 | You may obtain a copy of the License at following link. 444 | 445 | http://www.apache.org/licenses/LICENSE-2.0 446 | 447 | Unless required by applicable law or agreed to in writing, software 448 | distributed under the License is distributed on an "AS IS" BASIS, 449 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 450 | See the License for the specific language governing permissions and 451 | limitations under the License. 452 | 453 | ``` 454 | 455 | 456 | ### Like it? Please cast your [**Star**](https://github.com/Fliggy-Mobile/ffloat) 🥰 ! 457 | 458 | 459 | 460 | --- 461 | 462 | # How to run Demo project? 463 | 464 | 1. **clone** project to local 465 | 466 | 2. Enter the project `example` directory and run the following command 467 | 468 | ``` 469 | flutter create . 470 | ``` 471 | 472 | 3. Run the demo in `example` 473 | 474 | # [🛸Portal: Update Log](https://github.com/Fliggy-Mobile/ffloat/blob/master/CHANGELOG.md) 475 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

ffloat

8 | 9 | 10 |
11 | 12 |

FFloat ,虽简单易用,但能满足你对浮层的一切想象。

13 | 14 |

生而优雅,支持精准的位置控制。圆角、边框、渐变、阴影装饰三角?应有尽有 😃。️

15 | 16 |

主理人:纽特(coorchice.cb@alibaba-inc.com)

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 |

47 |

48 | 49 | 50 | 51 |

52 | 53 | **[English](https://github.com/Fliggy-Mobile/ffloat) | 简体中文** 54 | 55 | > 感觉还不错?请投出您的 **Star** 吧 🥰 ! 56 | 57 | # ✨ 特性 58 | 59 | - 支持基于锚点元素的**浮层位置**控制 60 | 61 | - 便捷的浮层**显示/隐藏控制** 62 | 63 | - 自带优雅的**交互效果** 64 | 65 | - 灵活美观的**装饰三角** 66 | 67 | - 支持对**圆角**精准控制 68 | 69 | - 美妙的**渐变**效果支持 70 | 71 | - 简单但强大的**边框**支持 72 | 73 | - 支持绝对位置**双模式** 74 | 75 | 76 | # 🛠 使用指南 77 | 78 | 79 | ## ⚙️ 参数 80 | 81 | ### 🔩 基础参数 82 | 83 | |参数|类型|必要|默认值|说明| 84 | |---|---|:---:|---|---| 85 | |builder|FloatBuilder|是|null|通过 [FloatBuilder] 返回 [FFloat] 的内容组件。如果只更新内容区域的话,通过 `setter((){})` 进行| 86 | |color|Color|否|`Color(0x7F000000)`|[FFloat] 的颜色| 87 | |gradient|Gradient|否|null|渐变色。会覆盖 color| 88 | |anchor|Widget|否|null|锚点组件| 89 | |location|Offset|否|null|位置。通过 [location] 指定 [FFloat] 的位置后,基于锚点确定位置的所有配置将失效| 90 | |alignment|FFloatAlignment|否|FFloatAlignment.topCenter|[FFloat] 基于 [anchor] 锚点元素的相对位置。| 91 | |margin|EdgeInsets|否|EdgeInsets.zero|[FFloat] 基于相对确定锚定点的间距| 92 | |padding|EdgeInsets|否|null|[FFloat] 内部间距| 93 | |canTouchOutside|bool|否|false|点击 [FFloat] 范围外区域是否隐藏。| 94 | |backgroundColor|Color|否|Colors.transparent|[FFloat] 浮出时,背景区域的颜色| 95 | |autoDismissDuration|Duration|否|null|自动消失时长。如果为 null,就不会自动消失| 96 | |controller|FFloatController|否|null|通过 [FFloatController] 可以控制 [FFloat] 的显示/隐藏。详见 [FFloatController]| 97 | |animDuration|Duration|否|`Duration(milliseconds: 100)`|显示/隐藏动效时长| 98 | 99 | ### 🔺 三角 100 | 101 | |参数|类型|必要|默认值|说明| 102 | |---|---|:---:|---|---| 103 | |triangleWidth|double|否|12|三角的宽| 104 | |triangleHeight|double|否|6|三角的高| 105 | |triangleAlignment|TriangleAlignment|否|TriangleAlignment.center|三角的相对位置| 106 | |triangleOffset|Offset|否|Offset.zero|三角的位置偏移| 107 | |hideTriangle|bool|否|false|是否隐藏装饰三角| 108 | 109 | ### 🔆 圆角 & 边框 110 | 111 | |参数|类型|必要|默认值|说明| 112 | |---|---|:---:|---|---| 113 | |corner|FFloatCorner|否|null|圆角| 114 | |cornerStyle|FFloatCornerStyle|否|FFloatCornerStyle.round|圆角样式| 115 | |strokeColor|Color|否|null|描边颜色| 116 | |strokeWidth|double|否|null|描边宽度| 117 | 118 | ### 🔳 阴影参数 119 | 120 | |参数|类型|必要|默认值|说明| 121 | |---|---|:---:|---|---| 122 | |shadowColor|Color|否|null|阴影颜色| 123 | |shadowOffset|Offset|否|null|阴影偏移量| 124 | |shadowBlur|double|否|null|值越大,阴影越大| 125 | 126 | 127 | ## 📺 使用示例 128 | 129 | ### 🔩 基本使用 130 | 131 | 132 | ![](https://gw.alicdn.com/tfs/TB1GwD9FhD1gK0jSZFyXXciOVXa-464-140.gif) 133 | 134 | ```dart 135 | FFloat( 136 | (_) => createContent(), 137 | controller: controller1, 138 | padding: EdgeInsets.only(left: 9, right: 9, top: 6, bottom: 6), 139 | corner: FFloatCorner.all(10), 140 | alignment: floatAlignment1, 141 | canTouchOutside: false, 142 | anchor: buildAnchor1(), 143 | ) 144 | ``` 145 | 146 | **FFloat** 能够去包裹一个正常的组件(即将正常组件赋值给 **FFloat** 的 `anchor` 参数),使得该组件具备点击弹出浮层的能力。 147 | 148 | 而且 **FFloat** 不会对原本的组件产生任何的不利影响,这很神奇吧! 149 | 150 | 或者,你也可以通过 **FFloatController** 来控制浮层的展示。当然,前提是你需要创建一个 **FFloatController** ,然后把它赋值给 **FFloat** 的 `controller` 属性。 151 | 152 | ```dart 153 | FFloatController controller = FFloatController(); 154 | 155 | FFloat( 156 | controller: controller, 157 | ) 158 | 159 | /// 显示浮层 160 | controller.show(); 161 | /// 隐藏浮层 162 | controller.dismiss(); 163 | ``` 164 | 165 | **FFloat** 足够的聪明,它能够根据 `anchor` 的位置自动的确定自己应该出现在什么地方。通过 `alignment` 和 `margin`,你能够以难以置信的简单的方式调整浮层的位置,直到你认为这可以了。 166 | 167 | 这是一种前所未有的改变 👍! 168 | 169 | 在过去,如果你想要基于一个元素的位置展示一个浮层,那你不得不经过一系列繁琐的操作,以获得元素的位置。然后再进行坐标转换,根据浮层的尺寸进行位置的计算。 170 | 171 | 天呐,光是想想就已经够复杂的了。更别提当遇到需要居中、居左对齐的场景了,那简直是噩梦 👿。 172 | 173 | 至于浮层的内容,只需要通过 `buidler` 参数,在 **FloatBuilder** 函数中返回一个 **Widget** 就可以了。 174 | 175 | 如果你的浮层内容需要刷新, **FloatBuilder** 函数提供了一个 **StateSetter** 参数,通过它可以只针对浮层中的内容刷新,而不影响浮层外的内容。真是高效呀。 176 | 177 | ```dart 178 | FFloat( 179 | (setter){ 180 | return GestureDetector( 181 | onTap:(){ 182 | setter((){ 183 | /// update something 184 | }); 185 | } 186 | anchor: buildWidgte()); 187 | }, 188 | anchor: buildAnchor() 189 | ) 190 | ``` 191 | 192 | 193 | ### 💫 背景 & 动画 194 | 195 | ![](https://gw.alicdn.com/tfs/TB179P9GuH2gK0jSZFEXXcqMpXa-720-135.gif) 196 | 197 | ```dart 198 | FFloat( 199 | (_) => FSuper( 200 | text: "Surprise😃 !", 201 | textColor: Colors.white, 202 | ), 203 | controller: controller2_1, 204 | color: Color(0xff5D5D5E), 205 | corner: FFloatCorner.all(6), 206 | margin: EdgeInsets.only(bottom: 10), 207 | padding: EdgeInsets.only(left: 9, right: 9, top: 3, bottom: 3), 208 | anchor: buildAnchor(), 209 | canTouchOutside: false, 210 | autoDismissDuration: Duration(milliseconds: 2000), 211 | ), 212 | 213 | FFloat( 214 | (_) => buildSearch(), 215 | child2Alignment: Alignment.centerLeft, 216 | child2Margin: EdgeInsets.only(left: (9.0 + 18.0 + 9.0)), 217 | ), 218 | controller: controller2_2, 219 | color: Colors.black.withOpacity(0.95), 220 | backgroundColor: Colors.black26, 221 | corner: FFloatCorner.all(20), 222 | margin: EdgeInsets.only(bottom: 10, left: 10), 223 | anchor: buildAnchor(), 224 | alignment: FFloatAlignment.topRight, 225 | triangleAlignment: TriangleAlignment.end, 226 | triangleOffset: Offset(-39, 0), 227 | ) 228 | ``` 229 | 230 | 当 **FFloat** 的浮层出现时,你可以选择是否需要背景色,通过配置 `backgroundColor`。 231 | 232 | 而通过 `canTouchOutside` 属性,你设置你的浮层是否可以通过点击浮层外区域关闭。 233 | 234 | 当设置为 `canTouchOutside = false` 时,往往就意味着你需要通过一个 **FFloatController** 来控制浮层的隐藏。 235 | 236 | 默认情况下,**FFloat** 附赠了一个 **缩放** 的显示/隐藏动画。 237 | 238 | 依据你设置的浮层方位,**FFloat** 能够智能的判断动画的起始锚点。这让一切都更加自然。 239 | 240 | 如果你不需要动画效果的话,只需要通过 `animDuration` 参数传入 **null**,就可以取消动画效果,回归比较突兀的显示/隐藏了。 241 | 242 | 在你配置了 `autoDismissDuration` 参数的情况下,**FFloat** 会进入自动消失模式。这意味着,在浮层弹出后,将在你期望的时间点自动消失。你无需过多干预。 243 | 244 | 245 | ### 🔺 装饰三角 246 | 247 | ![](https://gw.alicdn.com/tfs/TB1L3JoFEH1gK0jSZSyXXXtlpXa-753-220.gif) 248 | 249 | ```dart 250 | FFloat( 251 | (setter) => buildContent(), 252 | shadowColor: Colors.black38, 253 | shadowBlur: 8.0, 254 | shadowOffset: Offset(2.0, 2.0), 255 | color: Colors.white, 256 | corner: FFloatCorner.all(3), 257 | controller: controller3_1, 258 | alignment: FFloatAlignment.bottomLeft, 259 | hideTriangle: true, 260 | anchor: buildAnchor(), 261 | ), 262 | 263 | FFloat( 264 | (setter) => buildContent(), 265 | controller: controller3_2, 266 | alignment: FFloatAlignment.bottomLeft, 267 | margin: EdgeInsets.only(top: 2), 268 | shadowColor: Colors.black38, 269 | shadowBlur: 8.0, 270 | shadowOffset: Offset(2.0, 2.0), 271 | corner: FFloatCorner.all(3), 272 | color: Colors.white, 273 | triangleAlignment: TriangleAlignment.start, 274 | triangleOffset: Offset(10, 10), 275 | triangleWidth: 20, 276 | triangleHeight: 15, 277 | anchor: buildAnchor(), 278 | ), 279 | ``` 280 | 281 | **FFloat** 贴心的内置了装饰三角。默认情况下你就能看到它。如果你不希望得到一个有装饰三角的浮层,可以通过 `hideTriangle` 属性来控制它的显示/隐藏。 282 | 283 | 再次说明,**FFloat** 真的很聪明,装饰三角能够自动根据浮层与锚点元素的相对位置判断自己应该出现在什么位置,看起来才足够精致。 284 | 285 | 装饰三角和浮层的主题样式是浑然天成的,在色彩方面,**FFloat** 完全省去了你的烦恼。无论是纯色、边框、渐变色,一切都无需额外的处理。 286 | 287 | `triangleAlignment` 提供了简易的方式来调整装饰三角的位置。 288 | 289 | 如果你还是不满意,`triangleOffset` 能够让你基于相对位置进一步偏移。对于 **FFloat** 而言,没有什么不可能的。 290 | 291 | 当然,**FFloat** 一定会为你准备装饰三角的尺寸控制的,就像 `triangleWidth` 和 `triangleHeight` 要解决的问题那样。 292 | 293 | ### 🔆 圆角 & 边框 294 | 295 | ![](https://gw.alicdn.com/tfs/TB1xbhyFqL7gK0jSZFBXXXZZpXa-670-241.gif) 296 | 297 | ```dart 298 | 299 | FFloat( 300 | (setter) { 301 | return buildContent(); 302 | }, 303 | anchor: buildAnchor(), 304 | controller: controller4, 305 | color: Colors.white, 306 | corner: FFloatCorner.all(6), 307 | strokeColor: mainShadowColor, 308 | strokeWidth: 1.0, 309 | alignment: FFloatAlignment.bottomLeft, 310 | hideTriangle: true, 311 | margin: EdgeInsets.only(top: 9), 312 | padding: EdgeInsets.only(top: 9, bottom: 9), 313 | ) 314 | ``` 315 | 316 | 如你所见,一个漂亮的带边框的圆角浮层,构建起来是如此的简单。 317 | 318 | **FFloat** 提供了 **FWidget** 系列组件一脉相承的,简单的设置圆角的方式,仅仅通过一个简单的 `corner` 属性就能灵活的配置圆角。 319 | 320 | 与 `corner` 配套的 `cornerStyle` 属性,允许你随时切换圆角的风格(圆角 or 斜切角)。 321 | 322 | 如果你已经是 **FWidget** 的用户,相信你已经知道了,我们为组件配置边框效果,仅仅需要通过 `strokeWidth` 和 `strokeColor` 这样两个简单的属性即可。 323 | 324 | 我们的初心,始终是想帮助开发者能够更加快捷的构建出精美的应用。 325 | 326 | 327 | ### 🔳 渐变 & 阴影 328 | 329 | ![](https://gw.alicdn.com/tfs/TB11GHPFrj1gK0jSZFuXXcrHpXa-414-179.gif) 330 | 331 | ```dart 332 | 333 | FFloat( 334 | (setter) => buildContent(), 335 | anchor: buildAnchor(), 336 | controller: controller5, 337 | gradient: SweepGradient( 338 | colors: [ 339 | Color(0xffE271C0), 340 | Color(0xffC671EB), 341 | Color(0xff7673F3), 342 | Color(0xff8BEBEF), 343 | Color(0xff93FCA8), 344 | Color(0xff94FC9D), 345 | Color(0xffEDF980), 346 | Color(0xffF0C479), 347 | Color(0xffE07E77), 348 | ], 349 | ), 350 | corner: FFloatCorner.all(100), 351 | hideTriangle: true, 352 | margin: EdgeInsets.only(top: 9), 353 | alignment: FFloatAlignment.bottomCenter, 354 | shadowColor: Colors.black38, 355 | shadowBlur: 3, 356 | shadowOffset: Offset(3, 2), 357 | ) 358 | ``` 359 | 360 | 是的,**FFloat** 在兼具了诸多能力之后,仍然对渐变进行了支持。 361 | 362 | 当然,仅仅是通过一个简单的 `gradient` 属性,你就能获得漂亮的渐变效果。 363 | 364 | 此外,作为一个现代化的组件,**FFloat** 当然会对阴影作出支持。 365 | 366 | 你只需要通过 `shadowColor` 属性就能获得一个基础的阴影效果,如果你想要进一步对阴影作出调整的,可以使用 `shadowBlur` 和 `shadowOffset` 属性。 367 | 368 | ### 📌 基于绝对坐标的位置控制 369 | 370 | ![](https://gw.alicdn.com/tfs/TB1gnz8GrH1gK0jSZFwXXc7aXXa-858-508.gif) 371 | 372 | ```dart 373 | GestureDetector( 374 | onPanDown: (details) { 375 | FFloat( 376 | (setter) => createContent(), 377 | autoDismissDuration: Duration(milliseconds: 2000), 378 | alignment: _alignment, 379 | canTouchOutside: false, 380 | 381 | /// 通过绝对坐标配置浮动元素位置 382 | location: Offset(details.globalPosition.dx, details.globalPosition.dy), 383 | ).show(context); /// 显示 384 | }, 385 | child: FSuper(...), 386 | ) 387 | ``` 388 | 389 | 在一些情况下,我们的浮动元素不需要基于一个锚点出现,而是希望它出现在一个确定的位置。 390 | 391 | 如果开发者知道一个这样的位置的话,就用 `location` 参数来设置 **FFloat** 的位置吧。 392 | 393 | 此时,开发者完全不需要将 **FFloat** 放到视图树中包裹任何的元素。这意味着开发者可以随时随地,在任何回调或者函数中创建一个浮动元素。 394 | 395 | 通过 `FFloat.show(context)` 和 `FFloat.dismiss()`,开放着可以随时轻松的控制浮动元素的 **显示/隐藏** 。 396 | 397 | 而 **FFloat** 的其它一切配置都依旧有效。 398 | 399 | ### 👏 更多的精彩 400 | 401 | ![](https://gw.alicdn.com/tfs/TB1NGfIFAL0gK0jSZFtXXXQCXXa-460-500.gif) 402 | 403 | 在 **FFloat** 中,浮层能够自动的跟随锚点元素移动,你无需关注因位置变化而产生的一些列计算。 404 | 405 | **FFloat** 都帮你处理的足够好了! 406 | 407 | 让 **FFloat** 来解决你的一切浮层问题吧,你只需要用心美化你的应用就够了。 408 | 409 | 410 | # 😃 如何使用? 411 | 412 | 在项目 `pubspec.yaml` 文件中添加依赖: 413 | 414 | ## 🌐 pub 依赖方式 415 | 416 | ``` 417 | dependencies: 418 | ffloat: ^<版本号> 419 | ``` 420 | 421 | > ⚠️ 注意,请到 [**pub**](https://pub.dev/packages/ffloat) 获取 **ffloat** 最新版本号 422 | 423 | ## 🖥 git 依赖方式 424 | 425 | ``` 426 | dependencies: 427 | ffloat: 428 | git: 429 | url: 'git@github.com:Fliggy-Mobile/ffloat.git' 430 | ref: '<分支号 或 tag>' 431 | ``` 432 | 433 | 434 | > ⚠️ 注意,分支号 或 tag 请以 [**ffloat**](https://github.com/Fliggy-Mobile/ffloat) 官方项目为准。 435 | 436 | 437 | # 💡 License 438 | 439 | ``` 440 | Copyright 2020-present Fliggy Android Team . 441 | 442 | Licensed under the Apache License, Version 2.0 (the "License"); 443 | you may not use this file except in compliance with the License. 444 | You may obtain a copy of the License at following link. 445 | 446 | http://www.apache.org/licenses/LICENSE-2.0 447 | 448 | Unless required by applicable law or agreed to in writing, software 449 | distributed under the License is distributed on an "AS IS" BASIS, 450 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 451 | See the License for the specific language governing permissions and 452 | limitations under the License. 453 | 454 | ``` 455 | 456 | 457 | ### 感觉还不错?请投出您的 [**Star**](https://github.com/Fliggy-Mobile/ffloat) 吧 🥰 ! 458 | 459 | 460 | -- 461 | 462 | # 如何运行 Demo 工程? 463 | 464 | 1. **clone** 工程到本地 465 | 466 | 2. 进入工程 `example` 目录,运行以下命令 467 | 468 | ``` 469 | flutter create . 470 | ``` 471 | 472 | 3. 运行 `example` 中的 Demo 473 | 474 | # [🛸传送门:更新日志](https://github.com/Fliggy-Mobile/ffloat/blob/master/CHANGELOG.md) 475 | -------------------------------------------------------------------------------- /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 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/Flutter/flutter_export_environment.sh 65 | **/ios/ServiceDefinitions.json 66 | **/ios/Runner/GeneratedPluginRegistrant.* 67 | 68 | # Exceptions to above rules. 69 | !**/ios/**/default.mode1v3 70 | !**/ios/**/default.mode2v3 71 | !**/ios/**/default.pbxuser 72 | !**/ios/**/default.perspectivev3 73 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 74 | 75 | web/ 76 | .flutter-plugins-dependencies 77 | README.md 78 | test/ 79 | macos/ 80 | android/ 81 | ios/ 82 | test/ 83 | .metadata 84 | pubspec.lock 85 | README.md 86 | -------------------------------------------------------------------------------- /example/assets/airpos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fliggy-Mobile/ffloat/2b3b4549c23387a3d7187f85136a7c27123bfb19/example/assets/airpos.png -------------------------------------------------------------------------------- /example/assets/appletv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fliggy-Mobile/ffloat/2b3b4549c23387a3d7187f85136a7c27123bfb19/example/assets/appletv.png -------------------------------------------------------------------------------- /example/assets/icon_apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fliggy-Mobile/ffloat/2b3b4549c23387a3d7187f85136a7c27123bfb19/example/assets/icon_apple.png -------------------------------------------------------------------------------- /example/assets/icon_apple2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fliggy-Mobile/ffloat/2b3b4549c23387a3d7187f85136a7c27123bfb19/example/assets/icon_apple2.png -------------------------------------------------------------------------------- /example/assets/icon_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fliggy-Mobile/ffloat/2b3b4549c23387a3d7187f85136a7c27123bfb19/example/assets/icon_color.png -------------------------------------------------------------------------------- /example/assets/iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fliggy-Mobile/ffloat/2b3b4549c23387a3d7187f85136a7c27123bfb19/example/assets/iphone.png -------------------------------------------------------------------------------- /example/assets/switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fliggy-Mobile/ffloat/2b3b4549c23387a3d7187f85136a7c27123bfb19/example/assets/switch.png -------------------------------------------------------------------------------- /example/assets/watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fliggy-Mobile/ffloat/2b3b4549c23387a3d7187f85136a7c27123bfb19/example/assets/watch.png -------------------------------------------------------------------------------- /example/lib/color.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/widgets.dart'; 3 | 4 | const Color mainBackgroundColor = Color(0xfff1f3f6); 5 | const Color mainTextTitleColor = Color(0xff366471); 6 | const Color mainTextNormalColor = Color(0xff3e6a77); 7 | const Color mainTextSubColor = Color(0xff6c909b); 8 | const Color mainShadowColor = Color(0x4d3754AA); 9 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:fbutton/fbutton.dart'; 4 | import 'package:ffloat/ffloat.dart'; 5 | import 'package:ffloat_example/color.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:fradio/fradio.dart'; 8 | import 'package:fsuper/fsuper.dart'; 9 | 10 | void main() => runApp(MyApp()); 11 | 12 | class MyApp extends StatefulWidget { 13 | @override 14 | _MyAppState createState() => _MyAppState(); 15 | } 16 | 17 | class _MyAppState extends State { 18 | @override 19 | void initState() { 20 | super.initState(); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return MaterialApp( 26 | debugShowCheckedModeBanner: false, 27 | home: FFloatPage(), 28 | ); 29 | } 30 | } 31 | 32 | class FFloatPage extends StatefulWidget { 33 | @override 34 | State createState() { 35 | return _FFloatPage(); 36 | } 37 | } 38 | 39 | class _FFloatPage extends State { 40 | /// controllers 41 | FFloatController controller1 = FFloatController(); 42 | FFloatController controller2_1 = FFloatController(); 43 | FFloatController controller2_2 = FFloatController(); 44 | FFloatController controller2_3 = FFloatController(); 45 | FFloatController controller3_1 = FFloatController(); 46 | FFloatController controller3_2 = FFloatController(); 47 | FFloatController controller3_3 = FFloatController(); 48 | FFloatController controller4 = FFloatController(); 49 | FFloatController controller5 = FFloatController(); 50 | 51 | String text_1 = "Click, Now!"; 52 | FFloatAlignment floatAlignment1 = FFloatAlignment.topCenter; 53 | 54 | List fileMenuList = []; 55 | List clickList = []; 56 | int group_menu_value1 = -1; 57 | int group_menu_value2 = -1; 58 | int group_menu_value3 = -1; 59 | 60 | int group_toolbar_value = -1; 61 | 62 | int group_corner_value = -1; 63 | 64 | List alignmentList; 65 | 66 | @override 67 | void initState() { 68 | controller1.setStateChangedListener(() { 69 | print('controller1.isShow = ${controller1.isShow}'); 70 | if (!controller1.isShow) { 71 | setState(() { 72 | floatAlignment1 = randomFloatAlignment(floatAlignment1); 73 | }); 74 | } 75 | }); 76 | controller2_2.setStateChangedListener(() { 77 | print('controller2_2.isShow = ${controller2_2.isShow}'); 78 | }); 79 | fileMenuList.add("New Finder Window ⌘N"); 80 | fileMenuList.add("New Smart Folder "); 81 | fileMenuList.add("New Tab ⌘T"); 82 | fileMenuList.add("Open in New Tab ^⌘O"); 83 | 84 | /// clickList 85 | clickList.add("Android"); 86 | clickList.add("iOS"); 87 | clickList.add("Flutter"); 88 | clickList.add("Fuchsia"); 89 | 90 | alignmentList = [ 91 | // FFloatAlignment.topLeft, 92 | FFloatAlignment.topCenter, 93 | // FFloatAlignment.topRight, 94 | // FFloatAlignment.bottomLeft, 95 | FFloatAlignment.bottomCenter, 96 | // FFloatAlignment.bottomRight, 97 | // FFloatAlignment.leftTop, 98 | FFloatAlignment.leftCenter, 99 | // FFloatAlignment.leftBottom, 100 | // FFloatAlignment.rightTop, 101 | FFloatAlignment.rightCenter, 102 | // FFloatAlignment.rightBottom, 103 | ]; 104 | super.initState(); 105 | } 106 | 107 | @override 108 | Widget build(BuildContext context) { 109 | return Scaffold( 110 | backgroundColor: mainBackgroundColor, 111 | appBar: AppBar( 112 | backgroundColor: mainBackgroundColor, 113 | title: const Text( 114 | 'FFloat', 115 | style: TextStyle(color: mainTextTitleColor), 116 | ), 117 | centerTitle: true, 118 | ), 119 | body: SingleChildScrollView( 120 | physics: BouncingScrollPhysics(), 121 | child: Column( 122 | crossAxisAlignment: CrossAxisAlignment.center, 123 | children: [ 124 | buildTitle("Base Demo"), 125 | buildMiddleMargin(), 126 | 127 | /// baseDemo 128 | baseDemo(), 129 | buildMiddleMargin(), 130 | buildTitle("Background & Animation"), 131 | buildMiddleMargin(), 132 | buildMiddleMargin(), 133 | buildMiddleMargin(), 134 | 135 | /// backgroundDemo 136 | backgroundDemo(), 137 | buildMiddleMargin(), 138 | buildTitle("Triangle"), 139 | buildMiddleMargin(), 140 | buildMiddleMargin(), 141 | 142 | /// Triangle 143 | triangleDemo(), 144 | buildMiddleMargin(), 145 | buildMiddleMargin(), 146 | buildMiddleMargin(), 147 | buildMiddleMargin(), 148 | buildMiddleMargin(), 149 | buildMiddleMargin(), 150 | buildTitle("Corner & Stroke"), 151 | buildMiddleMargin(), 152 | 153 | /// cornerDemo 154 | cornerDemo(), 155 | buildMiddleMargin(), 156 | buildMiddleMargin(), 157 | buildMiddleMargin(), 158 | buildMiddleMargin(), 159 | buildMiddleMargin(), 160 | buildMiddleMargin(), 161 | buildMiddleMargin(), 162 | buildMiddleMargin(), 163 | buildTitle("Gradient & Shadow"), 164 | buildMiddleMargin(), 165 | 166 | /// gradientDemo 167 | gradientDemo(), 168 | buildMiddleMargin(), 169 | buildMiddleMargin(), 170 | buildMiddleMargin(), 171 | buildMiddleMargin(), 172 | buildMiddleMargin(), 173 | buildTitle("More Surprise"), 174 | buildMiddleMargin(), 175 | buildMiddleMargin(), 176 | 177 | /// moreDemo 178 | moreDemo(), 179 | buildMiddleMargin(), 180 | buildMiddleMargin(), 181 | buildMiddleMargin(), 182 | buildMiddleMargin(), 183 | buildMiddleMargin(), 184 | 185 | buildTitle("Absolute Position Demo"), 186 | buildMiddleMargin(), 187 | buildMiddleMargin(), 188 | 189 | /// Absolute Position Demo 190 | buildAbsolutePositionDemo(context), 191 | buildMiddleMargin(), 192 | buildMiddleMargin(), 193 | buildMiddleMargin(), 194 | ], 195 | ), 196 | ), 197 | ); 198 | } 199 | 200 | GestureDetector buildAbsolutePositionDemo(BuildContext context) { 201 | return GestureDetector( 202 | onPanDown: (details) { 203 | FFloat( 204 | (setter) => Text( 205 | "Hello", 206 | style: TextStyle(color: Colors.white), 207 | ), 208 | location: 209 | Offset(details.globalPosition.dx, details.globalPosition.dy), 210 | autoDismissDuration: Duration(milliseconds: 2000), 211 | padding: EdgeInsets.all(6.0), 212 | corner: FFloatCorner.all(6.0), 213 | canTouchOutside: false, 214 | alignment: randomAlignment(), 215 | ).show(context); 216 | }, 217 | child: FSuper( 218 | width: 300, 219 | height: 200, 220 | backgroundColor: Colors.white, 221 | shadowColor: mainShadowColor, 222 | shadowOffset: Offset(3.0, 3.0), 223 | shadowBlur: 5.0, 224 | corner: Corner.all(6.0), 225 | ), 226 | ); 227 | } 228 | 229 | Container moreDemo() { 230 | return Container( 231 | width: double.infinity, 232 | height: 250, 233 | margin: EdgeInsets.only(left: 20, right: 20), 234 | padding: EdgeInsets.only(top: 20, bottom: 20), 235 | decoration: BoxDecoration( 236 | color: Color(0xffe2f1fa), 237 | borderRadius: BorderRadius.all(Radius.circular(6)), 238 | boxShadow: [ 239 | BoxShadow( 240 | color: mainShadowColor, blurRadius: 5, offset: Offset(5, 5)), 241 | ], 242 | ), 243 | alignment: Alignment.center, 244 | child: Stack( 245 | children: [ 246 | Positioned( 247 | left: 300, 248 | child: FFloat( 249 | (_) { 250 | return Text( 251 | "Airpos", 252 | style: TextStyle(color: Colors.white), 253 | ); 254 | }, 255 | padding: 256 | EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 15), 257 | margin: EdgeInsets.only(bottom: 3), 258 | corner: FFloatCorner( 259 | leftTopCorner: 50, 260 | rightTopCorner: 50, 261 | leftBottomCorner: 35, 262 | rightBottomCorner: 35), 263 | triangleWidth: 30, 264 | triangleHeight: 10, 265 | anchor: Image.asset( 266 | "assets/airpos.png", 267 | width: 60, 268 | ), 269 | canTouchOutside: false, 270 | ), 271 | ), 272 | Positioned( 273 | left: 100, 274 | top: 10, 275 | child: FFloat( 276 | (_) { 277 | return Text( 278 | "Apple Tv", 279 | style: TextStyle(color: Colors.white), 280 | ); 281 | }, 282 | padding: 283 | EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 15), 284 | margin: EdgeInsets.only(bottom: 3), 285 | corner: FFloatCorner( 286 | leftTopCorner: 80, 287 | rightTopCorner: 80, 288 | leftBottomCorner: 35, 289 | rightBottomCorner: 35), 290 | triangleWidth: 55, 291 | triangleHeight: 10, 292 | anchor: Image.asset( 293 | "assets/appletv.png", 294 | width: 60, 295 | ), 296 | canTouchOutside: false, 297 | ), 298 | ), 299 | Positioned( 300 | left: 80, 301 | top: 90, 302 | child: FFloat( 303 | (_) { 304 | return Text( 305 | "iPhone", 306 | style: TextStyle(color: Colors.white), 307 | ); 308 | }, 309 | padding: 310 | EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), 311 | margin: EdgeInsets.only(bottom: 10, right: 3), 312 | corner: FFloatCorner( 313 | leftTopCorner: 50, 314 | rightTopCorner: 25, 315 | leftBottomCorner: 50, 316 | rightBottomCorner: 25), 317 | triangleWidth: 23, 318 | triangleHeight: 10, 319 | alignment: FFloatAlignment.leftTop, 320 | anchor: Image.asset( 321 | "assets/iphone.png", 322 | width: 80, 323 | ), 324 | canTouchOutside: false, 325 | ), 326 | ), 327 | Positioned( 328 | left: 200, 329 | top: 10, 330 | child: FFloat( 331 | (_) { 332 | return Text( 333 | "Switch", 334 | style: TextStyle(color: Colors.white), 335 | ); 336 | }, 337 | padding: 338 | EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 15), 339 | margin: EdgeInsets.only(bottom: 3), 340 | corner: FFloatCorner( 341 | leftTopCorner: 80, 342 | rightTopCorner: 80, 343 | leftBottomCorner: 40, 344 | rightBottomCorner: 40), 345 | triangleWidth: 41, 346 | triangleHeight: 10, 347 | anchor: Image.asset( 348 | "assets/switch.png", 349 | width: 60, 350 | ), 351 | canTouchOutside: false, 352 | ), 353 | ), 354 | Positioned( 355 | left: 250, 356 | top: 100, 357 | child: FFloat( 358 | (_) { 359 | return Text( 360 | "Watch", 361 | style: TextStyle(color: Colors.white), 362 | ); 363 | }, 364 | padding: 365 | EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), 366 | margin: EdgeInsets.only(bottom: 3), 367 | corner: FFloatCorner( 368 | leftTopCorner: 50, 369 | rightTopCorner: 80, 370 | leftBottomCorner: 50, 371 | rightBottomCorner: 80), 372 | alignment: FFloatAlignment.rightCenter, 373 | triangleWidth: 19, 374 | triangleHeight: 10, 375 | anchor: Image.asset( 376 | "assets/watch.png", 377 | width: 60, 378 | ), 379 | canTouchOutside: false, 380 | ), 381 | ), 382 | ], 383 | ), 384 | ); 385 | } 386 | 387 | FFloat gradientDemo() { 388 | return FFloat( 389 | (setter) { 390 | return Container( 391 | width: 100, 392 | height: 100, 393 | ); 394 | }, 395 | anchor: FButton( 396 | width: 72, 397 | height: 30, 398 | corner: FButtonCorner.all(3), 399 | padding: EdgeInsets.all(0), 400 | color: Color(0xff373737), 401 | effect: true, 402 | image: Image.asset("assets/icon_color.png", width: 18), 403 | onPressed: () { 404 | controller5.show(); 405 | }, 406 | hoverColor: Colors.white60.withOpacity(0.3), 407 | ), 408 | controller: controller5, 409 | gradient: SweepGradient( 410 | colors: [ 411 | Color(0xffE271C0), 412 | Color(0xffC671EB), 413 | Color(0xff7673F3), 414 | Color(0xff8BEBEF), 415 | Color(0xff93FCA8), 416 | Color(0xff94FC9D), 417 | Color(0xffEDF980), 418 | Color(0xffF0C479), 419 | Color(0xffE07E77), 420 | ], 421 | ), 422 | corner: FFloatCorner.all(100), 423 | hideTriangle: true, 424 | margin: EdgeInsets.only(top: 9), 425 | alignment: FFloatAlignment.bottomCenter, 426 | shadowColor: Colors.black38, 427 | shadowBlur: 3, 428 | shadowOffset: Offset(3, 2), 429 | ); 430 | } 431 | 432 | FFloat cornerDemo() { 433 | return FFloat( 434 | (setter) { 435 | return SizedBox( 436 | width: 150, 437 | height: 100, 438 | child: ListView.builder( 439 | shrinkWrap: true, 440 | itemCount: clickList.length, 441 | itemBuilder: (context, index) { 442 | return FRadio.custom( 443 | value: index, 444 | groupValue: group_corner_value, 445 | onChanged: (value) { 446 | setter(() { 447 | group_corner_value = value; 448 | }); 449 | }, 450 | normal: FSuper( 451 | width: double.infinity, 452 | textAlign: TextAlign.left, 453 | text: clickList[index], 454 | padding: 455 | EdgeInsets.only(left: 20, right: 10, top: 3, bottom: 3), 456 | ), 457 | selected: FSuper( 458 | width: double.infinity, 459 | text: clickList[index], 460 | textAlign: TextAlign.left, 461 | textColor: Colors.white, 462 | backgroundColor: Color(0xff008FFF), 463 | padding: 464 | EdgeInsets.only(left: 20, right: 10, top: 3, bottom: 3), 465 | ), 466 | hover: FSuper( 467 | width: double.infinity, 468 | text: clickList[index], 469 | textAlign: TextAlign.left, 470 | backgroundColor: Colors.black38.withOpacity(0.1), 471 | padding: 472 | EdgeInsets.only(left: 20, right: 10, top: 3, bottom: 3), 473 | ), 474 | ); 475 | }), 476 | ); 477 | }, 478 | anchor: FButton( 479 | width: 80, 480 | height: 40, 481 | onPressed: () { 482 | controller4.show(); 483 | }, 484 | text: "Click", 485 | textColor: mainTextNormalColor, 486 | color: Colors.white, 487 | effect: true, 488 | padding: EdgeInsets.zero, 489 | corner: FButtonCorner.all(6), 490 | shadowColor: mainShadowColor, 491 | shadowOffset: Offset(1, 1), 492 | shadowBlur: 5.0, 493 | image: Icon( 494 | Icons.arrow_drop_down, 495 | size: 20, 496 | color: mainTextNormalColor, 497 | ), 498 | imageAlignment: ImageAlignment.right, 499 | ), 500 | controller: controller4, 501 | color: Colors.white, 502 | corner: FFloatCorner.all(6), 503 | strokeColor: mainShadowColor, 504 | strokeWidth: 1.0, 505 | alignment: FFloatAlignment.bottomLeft, 506 | hideTriangle: true, 507 | margin: EdgeInsets.only(top: 9), 508 | padding: EdgeInsets.only(top: 9, bottom: 9), 509 | ); 510 | } 511 | 512 | Container triangleDemo() { 513 | return Container( 514 | width: double.infinity, 515 | height: 22, 516 | decoration: BoxDecoration(color: Color(0xff5D5D5E), boxShadow: [ 517 | BoxShadow( 518 | color: Colors.black38, 519 | offset: Offset(0, 8), 520 | blurRadius: 8.0, 521 | ) 522 | ]), 523 | child: Row( 524 | mainAxisSize: MainAxisSize.max, 525 | children: [ 526 | Image.asset("assets/icon_apple2.png", width: 13.5, height: 16), 527 | const SizedBox(width: 16), 528 | FFloat( 529 | (setter) => Container( 530 | width: 230, 531 | height: 121, 532 | padding: EdgeInsets.only(top: 6, bottom: 6), 533 | child: ListView.builder( 534 | shrinkWrap: true, 535 | itemCount: fileMenuList.length, 536 | itemBuilder: (context, index) { 537 | return FRadio.custom( 538 | value: index, 539 | groupValue: group_menu_value1, 540 | onChanged: (value) { 541 | setter(() { 542 | group_menu_value1 = value; 543 | }); 544 | }, 545 | normal: FSuper( 546 | width: double.infinity, 547 | textAlign: TextAlign.left, 548 | text: fileMenuList[index], 549 | padding: EdgeInsets.only( 550 | left: 20, right: 10, top: 3, bottom: 3), 551 | ), 552 | selected: FSuper( 553 | width: double.infinity, 554 | text: fileMenuList[index], 555 | textAlign: TextAlign.left, 556 | textColor: Colors.white, 557 | backgroundColor: Color(0xff008FFF), 558 | padding: EdgeInsets.only( 559 | left: 20, right: 10, top: 3, bottom: 3), 560 | ), 561 | hover: FSuper( 562 | width: double.infinity, 563 | text: fileMenuList[index], 564 | textAlign: TextAlign.left, 565 | backgroundColor: Colors.black38.withOpacity(0.1), 566 | padding: EdgeInsets.only( 567 | left: 20, right: 10, top: 3, bottom: 3), 568 | ), 569 | ); 570 | }), 571 | ), 572 | shadowColor: Colors.black38, 573 | shadowBlur: 8.0, 574 | shadowOffset: Offset(2.0, 2.0), 575 | color: Colors.white, 576 | corner: FFloatCorner.all(3), 577 | controller: controller3_1, 578 | alignment: FFloatAlignment.bottomLeft, 579 | hideTriangle: true, 580 | anchor: FRadio.custom( 581 | width: 56, 582 | height: 22, 583 | value: 0, 584 | groupValue: group_toolbar_value, 585 | onChanged: (value) { 586 | setState(() { 587 | group_toolbar_value = value; 588 | }); 589 | controller3_1.show(); 590 | }, 591 | normal: FSuper( 592 | text: "Finder", 593 | textColor: Colors.white, 594 | textSize: 14, 595 | textAlignment: Alignment.center, 596 | padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), 597 | ), 598 | hover: FSuper( 599 | text: "Finder", 600 | textColor: Colors.white, 601 | textSize: 14, 602 | textAlignment: Alignment.center, 603 | padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), 604 | backgroundColor: Color(0xff008FFF).withOpacity(0.2), 605 | ), 606 | selected: FSuper( 607 | text: "Finder", 608 | textColor: Colors.white, 609 | textSize: 14, 610 | padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), 611 | backgroundColor: Color(0xff008FFF), 612 | ), 613 | ), 614 | ), 615 | const SizedBox(width: 4), 616 | FFloat( 617 | (setter) => Container( 618 | width: 230, 619 | height: 121, 620 | padding: EdgeInsets.only(top: 6, bottom: 6), 621 | child: ListView.builder( 622 | shrinkWrap: true, 623 | itemCount: fileMenuList.length, 624 | itemBuilder: (context, index) { 625 | return FRadio.custom( 626 | value: index, 627 | groupValue: group_menu_value2, 628 | onChanged: (value) { 629 | setter(() { 630 | group_menu_value2 = value; 631 | }); 632 | }, 633 | normal: FSuper( 634 | width: double.infinity, 635 | textAlign: TextAlign.left, 636 | text: fileMenuList[index], 637 | padding: EdgeInsets.only( 638 | left: 20, right: 10, top: 3, bottom: 3), 639 | ), 640 | selected: FSuper( 641 | width: double.infinity, 642 | text: fileMenuList[index], 643 | textAlign: TextAlign.left, 644 | textColor: Colors.white, 645 | backgroundColor: Color(0xff008FFF), 646 | padding: EdgeInsets.only( 647 | left: 20, right: 10, top: 3, bottom: 3), 648 | ), 649 | hover: FSuper( 650 | width: double.infinity, 651 | text: fileMenuList[index], 652 | textAlign: TextAlign.left, 653 | backgroundColor: Colors.black38.withOpacity(0.1), 654 | padding: EdgeInsets.only( 655 | left: 20, right: 10, top: 3, bottom: 3), 656 | ), 657 | ); 658 | }), 659 | ), 660 | controller: controller3_2, 661 | alignment: FFloatAlignment.bottomLeft, 662 | margin: EdgeInsets.only(top: 2), 663 | shadowColor: Colors.black38, 664 | shadowBlur: 8.0, 665 | shadowOffset: Offset(2.0, 2.0), 666 | corner: FFloatCorner.all(3), 667 | color: Colors.white, 668 | triangleAlignment: TriangleAlignment.start, 669 | triangleOffset: Offset(10, 10), 670 | triangleWidth: 20, 671 | triangleHeight: 15, 672 | anchor: FRadio.custom( 673 | width: 38, 674 | height: 22, 675 | value: 1, 676 | groupValue: group_toolbar_value, 677 | onChanged: (value) { 678 | setState(() { 679 | group_toolbar_value = value; 680 | }); 681 | controller3_2.show(); 682 | }, 683 | normal: FSuper( 684 | text: "File", 685 | textColor: Colors.white, 686 | textSize: 14, 687 | textAlignment: Alignment.center, 688 | padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), 689 | ), 690 | hover: FSuper( 691 | text: "File", 692 | textColor: Colors.white, 693 | textSize: 14, 694 | textAlignment: Alignment.center, 695 | padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), 696 | backgroundColor: Color(0xff008FFF).withOpacity(0.2), 697 | ), 698 | selected: FSuper( 699 | text: "File", 700 | textColor: Colors.white, 701 | textSize: 14, 702 | padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), 703 | backgroundColor: Color(0xff008FFF), 704 | ), 705 | ), 706 | ), 707 | const SizedBox(width: 4), 708 | FFloat( 709 | (setter) => Container( 710 | width: 230, 711 | height: 121, 712 | padding: EdgeInsets.only(top: 6, bottom: 6), 713 | child: ListView.builder( 714 | shrinkWrap: true, 715 | itemCount: fileMenuList.length, 716 | itemBuilder: (context, index) { 717 | return FRadio.custom( 718 | value: index, 719 | groupValue: group_menu_value3, 720 | onChanged: (value) { 721 | setter(() { 722 | group_menu_value3 = value; 723 | }); 724 | }, 725 | normal: FSuper( 726 | width: double.infinity, 727 | textAlign: TextAlign.left, 728 | text: fileMenuList[index], 729 | padding: EdgeInsets.only( 730 | left: 20, right: 10, top: 3, bottom: 3), 731 | ), 732 | selected: FSuper( 733 | width: double.infinity, 734 | text: fileMenuList[index], 735 | textAlign: TextAlign.left, 736 | textColor: Colors.white, 737 | backgroundColor: Color(0xff008FFF), 738 | padding: EdgeInsets.only( 739 | left: 20, right: 10, top: 3, bottom: 3), 740 | ), 741 | hover: FSuper( 742 | width: double.infinity, 743 | text: fileMenuList[index], 744 | textAlign: TextAlign.left, 745 | // backgroundColor: Color(0xff008FFF), 746 | backgroundColor: Colors.black38.withOpacity(0.1), 747 | padding: EdgeInsets.only( 748 | left: 20, right: 10, top: 3, bottom: 3), 749 | ), 750 | ); 751 | }), 752 | ), 753 | controller: controller3_3, 754 | alignment: FFloatAlignment.rightTop, 755 | margin: EdgeInsets.only(bottom: 8), 756 | corner: FFloatCorner.all(3), 757 | shadowColor: Colors.black38, 758 | shadowBlur: 8.0, 759 | shadowOffset: Offset(2.0, 2.0), 760 | color: Colors.white, 761 | triangleAlignment: TriangleAlignment.start, 762 | triangleOffset: Offset(10, 10), 763 | triangleWidth: 20, 764 | triangleHeight: 15, 765 | anchor: FRadio.custom( 766 | width: 38, 767 | height: 22, 768 | value: 2, 769 | groupValue: group_toolbar_value, 770 | onChanged: (value) { 771 | setState(() { 772 | group_toolbar_value = value; 773 | }); 774 | controller3_3.show(); 775 | }, 776 | normal: FSuper( 777 | text: "Edit", 778 | textColor: Colors.white, 779 | textSize: 14, 780 | textAlignment: Alignment.center, 781 | padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), 782 | ), 783 | hover: FSuper( 784 | text: "Edit", 785 | textColor: Colors.white, 786 | textSize: 14, 787 | textAlignment: Alignment.center, 788 | padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), 789 | backgroundColor: Color(0xff008FFF).withOpacity(0.2), 790 | ), 791 | selected: FSuper( 792 | text: "Edit", 793 | textColor: Colors.white, 794 | textSize: 14, 795 | padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), 796 | backgroundColor: Color(0xff008FFF), 797 | ), 798 | ), 799 | ), 800 | ], 801 | ), 802 | padding: EdgeInsets.only(left: 12), 803 | ); 804 | } 805 | 806 | String test = "Surprise😃 !"; 807 | Widget backgroundDemo() { 808 | return FSuper( 809 | width: double.infinity, 810 | height: 40, 811 | backgroundColor: Color(0xff000000), 812 | child1: Row( 813 | children: [ 814 | FFloat( 815 | (_) => FSuper( 816 | // text: "Surprise😃 !", 817 | text: test, 818 | textColor: Colors.white, 819 | ), 820 | controller: controller2_1, 821 | color: Color(0xff5D5D5E), 822 | corner: FFloatCorner.all(6), 823 | margin: EdgeInsets.only(bottom: 10), 824 | padding: EdgeInsets.only(left: 9, right: 9, top: 3, bottom: 3), 825 | anchor: FButton( 826 | width: 72, 827 | height: 30, 828 | text: "esc", 829 | textColor: Colors.white, 830 | fontSize: 15, 831 | alignment: Alignment.center, 832 | corner: FButtonCorner.all(3), 833 | padding: EdgeInsets.all(0), 834 | color: Color(0xff373737), 835 | effect: true, 836 | onPressed: () { 837 | if (!controller2_1.isShow) { 838 | test = "Surprise😃 !"; 839 | controller2_1.show(); 840 | } else { 841 | controller2_1.setState(() { 842 | test = "Changed!"; 843 | }); 844 | } 845 | }, 846 | hoverColor: Colors.white60.withOpacity(0.3), 847 | ), 848 | canTouchOutside: false, 849 | autoDismissDuration: Duration(milliseconds: 2000), 850 | ), 851 | const SizedBox(width: 16), 852 | FFloat( 853 | (_) => FSuper( 854 | text: "HA🌝 !", 855 | textColor: Colors.white, 856 | ), 857 | controller: controller2_3, 858 | color: Color(0xff5D5D5E), 859 | corner: FFloatCorner.all(6), 860 | margin: EdgeInsets.only(bottom: 10), 861 | padding: EdgeInsets.only(left: 9, right: 9, top: 3, bottom: 3), 862 | anchor: FButton( 863 | width: 72, 864 | height: 30, 865 | corner: FButtonCorner.all(3), 866 | imageAlignment: ImageAlignment.left, 867 | image: Icon(Icons.add, color: Colors.white, size: 18), 868 | padding: EdgeInsets.all(0), 869 | color: Color(0xff373737), 870 | effect: true, 871 | onPressed: () { 872 | controller2_3.show(); 873 | }, 874 | hoverColor: Colors.white60.withOpacity(0.3), 875 | ), 876 | animDuration: Duration(milliseconds: 800), 877 | ), 878 | ], 879 | ), 880 | child1Alignment: Alignment.centerLeft, 881 | child1Margin: EdgeInsets.only(left: 20), 882 | child2: FFloat( 883 | (_) => Container( 884 | height: 50, 885 | child: Row( 886 | mainAxisSize: MainAxisSize.min, 887 | children: [ 888 | const SizedBox(width: 12.0), 889 | Icon( 890 | Icons.search, 891 | color: Colors.white, 892 | ), 893 | const SizedBox(width: 9.0), 894 | Container( 895 | width: 200, 896 | height: 50, 897 | child: TextField( 898 | maxLines: 1, 899 | decoration: InputDecoration( 900 | border: InputBorder.none, 901 | hintText: "SEARCH", 902 | hintStyle: TextStyle( 903 | color: Colors.white70, 904 | fontSize: 15, 905 | ), 906 | ), 907 | style: TextStyle( 908 | color: Colors.white, 909 | fontSize: 15, 910 | ), 911 | cursorColor: Colors.white30, 912 | ), 913 | ), 914 | ], 915 | ), 916 | ), 917 | controller: controller2_2, 918 | color: Colors.black.withOpacity(0.95), 919 | backgroundColor: Colors.black26, 920 | corner: FFloatCorner.all(25), 921 | margin: EdgeInsets.only(bottom: 10, left: 10), 922 | anchor: FButton( 923 | width: 72, 924 | height: 30, 925 | corner: FButtonCorner.all(3), 926 | imageAlignment: ImageAlignment.left, 927 | image: Icon(Icons.search, color: Colors.white, size: 18), 928 | padding: EdgeInsets.all(0), 929 | color: Color(0xff373737), 930 | effect: true, 931 | onPressed: () { 932 | controller2_2.show(); 933 | }, 934 | hoverColor: Colors.white60.withOpacity(0.3), 935 | ), 936 | alignment: FFloatAlignment.topRight, 937 | triangleAlignment: TriangleAlignment.end, 938 | triangleOffset: Offset(-39, 0), 939 | ), 940 | child2Alignment: Alignment.centerRight, 941 | child2Margin: EdgeInsets.only(right: 20), 942 | ); 943 | } 944 | 945 | Widget baseDemo() { 946 | return Container( 947 | width: double.infinity, 948 | height: 100, 949 | alignment: Alignment.center, 950 | child: FFloat( 951 | (_) => createFloat1(), 952 | controller: controller1, 953 | padding: EdgeInsets.only(left: 9, right: 9, top: 6, bottom: 6), 954 | corner: FFloatCorner.all(10), 955 | alignment: floatAlignment1, 956 | canTouchOutside: false, 957 | anchor: buildChild1(), 958 | ), 959 | ); 960 | } 961 | 962 | FSuper buildChild1() { 963 | return FSuper( 964 | width: 200, 965 | height: 50, 966 | textAlignment: Alignment.center, 967 | text: text_1, 968 | textColor: mainTextNormalColor, 969 | textSize: 18, 970 | corner: Corner.all(10), 971 | backgroundColor: Colors.white, 972 | padding: EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 12), 973 | shadowColor: mainShadowColor, 974 | shadowBlur: 5.0, 975 | shadowOffset: Offset(2.0, 2.0), 976 | ); 977 | } 978 | 979 | Widget createFloat1() { 980 | return Row( 981 | crossAxisAlignment: CrossAxisAlignment.center, 982 | children: [ 983 | Text( 984 | "Found me!", 985 | style: TextStyle( 986 | color: Colors.white, 987 | fontSize: 12, 988 | ), 989 | ), 990 | const SizedBox(width: 6), 991 | InkWell( 992 | onTap: () => controller1.dismiss(), 993 | child: Padding( 994 | padding: const EdgeInsets.all(0), 995 | child: Icon( 996 | Icons.close, 997 | size: 12, 998 | color: Colors.white, 999 | ), 1000 | ), 1001 | ), 1002 | ], 1003 | ); 1004 | } 1005 | 1006 | SizedBox buildMiddleMargin() { 1007 | return const SizedBox( 1008 | height: 26, 1009 | ); 1010 | } 1011 | 1012 | SizedBox buildSmallMargin() { 1013 | return const SizedBox( 1014 | height: 18, 1015 | ); 1016 | } 1017 | 1018 | Padding buildDesc(String desc) { 1019 | return Padding( 1020 | padding: const EdgeInsets.all(8), 1021 | child: Text( 1022 | desc, 1023 | textAlign: TextAlign.center, 1024 | style: TextStyle( 1025 | color: Colors.grey, 1026 | fontSize: 12, 1027 | ), 1028 | )); 1029 | } 1030 | 1031 | Container buildTitle(String title) { 1032 | return Container( 1033 | alignment: Alignment.centerLeft, 1034 | padding: EdgeInsets.all(9), 1035 | color: Color(0xffe0e0e0).withOpacity(0.38), 1036 | child: Text( 1037 | title, 1038 | style: TextStyle(color: mainTextSubColor.withOpacity(0.7)), 1039 | ), 1040 | ); 1041 | } 1042 | 1043 | FFloatAlignment randomFloatAlignment(FFloatAlignment alignment) { 1044 | switch (alignment) { 1045 | case FFloatAlignment.topLeft: 1046 | return FFloatAlignment.topCenter; 1047 | case FFloatAlignment.topCenter: 1048 | return FFloatAlignment.topRight; 1049 | case FFloatAlignment.topRight: 1050 | return FFloatAlignment.bottomLeft; 1051 | case FFloatAlignment.bottomLeft: 1052 | return FFloatAlignment.bottomCenter; 1053 | case FFloatAlignment.bottomCenter: 1054 | return FFloatAlignment.bottomRight; 1055 | case FFloatAlignment.bottomRight: 1056 | return FFloatAlignment.leftTop; 1057 | case FFloatAlignment.leftTop: 1058 | return FFloatAlignment.leftCenter; 1059 | case FFloatAlignment.leftCenter: 1060 | return FFloatAlignment.leftBottom; 1061 | case FFloatAlignment.leftBottom: 1062 | return FFloatAlignment.rightTop; 1063 | case FFloatAlignment.rightTop: 1064 | return FFloatAlignment.rightCenter; 1065 | case FFloatAlignment.rightCenter: 1066 | return FFloatAlignment.rightBottom; 1067 | case FFloatAlignment.rightBottom: 1068 | return FFloatAlignment.topLeft; 1069 | } 1070 | return FFloatAlignment.topCenter; 1071 | } 1072 | 1073 | FFloatAlignment randomAlignment() { 1074 | return alignmentList[Random().nextInt(alignmentList.length)]; 1075 | } 1076 | } 1077 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ffloat_example 2 | description: Demonstrates how to use the ffloat plugin. 3 | publish_to: none 4 | 5 | environment: 6 | sdk: '>=2.7.0 <3.0.0' 7 | 8 | dependencies: 9 | cupertino_icons: ^0.1.3 10 | fbutton: ^1.0.4 11 | ffloat: 12 | path: ../ 13 | flutter: 14 | sdk: flutter 15 | fradio: ^1.0.1 16 | fsuper: ^0.1.5 17 | fswitch: ^1.1.2 18 | 19 | dependency_overrides: {} 20 | 21 | dev_dependencies: 22 | flutter_test: 23 | sdk: flutter 24 | 25 | flutter: 26 | assets: 27 | - assets/ 28 | uses-material-design: true 29 | -------------------------------------------------------------------------------- /lib/ffloat.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | /// 描述三角的相对位置 7 | /// [start] - 三角在 [FFloat] 上下侧,表示三角和 [FFloat] 左边缘对齐;三角在 [FFloat] 左右侧,表示三角和 [FFloat] 上边缘对齐 8 | /// [center] - 三角在 [FFloat] 上下侧,表示三角水平居中;三角在 [FFloat] 左右侧,表示三角垂直居中 9 | /// [end] - 三角在 [FFloat] 上下侧,表示三角和 [FFloat] 右边缘对齐;三角在 [FFloat] 左右侧,表示三角和 [FFloat] 下边缘对齐 10 | /// 11 | /// Describe the relative position of the triangle 12 | /// [start]-The triangle is above and below [FFloat], indicating that the triangle is aligned with the left edge of [FFloat]; 13 | /// the triangle is on the left and right of [FFloat], indicating that the triangle is aligned with the top edge of [FFloat] 14 | /// [center] - The triangle is above and below [FFloat], indicating that the triangle is horizontally centered; 15 | /// the triangle is on the left and right sides of [FFloat], indicating that the triangle is vertically centered 16 | /// [end] - The triangle is above and below [FFloat], indicating that the triangle is aligned with the right edge of [FFloat]; 17 | /// the triangle is on the left and right of [FFloat], indicating that the triangle is aligned with the bottom edge of [FFloat] 18 | enum TriangleAlignment { 19 | start, 20 | center, 21 | end, 22 | } 23 | 24 | /// 描述 [FFloat] 相对于锚点元素的位置。 25 | /// topLeft - 在锚点元素【上方】,且【左边缘】与锚点元素对齐 26 | /// topCenter - 在锚点元素【上方】,且水平居中 27 | /// topRight - 在锚点元素【上方】,且【右边缘】与锚点元素对齐 28 | /// bottomLeft - 在锚点元素【下方】,且【左边缘】与锚点元素对齐 29 | /// bottomCenter - 在锚点元素【下方】,且水平居中 30 | /// bottomRight - 在锚点元素【下方】,且【右边缘】与锚点元素对齐 31 | /// leftTop - 在锚点元素【左侧】,且【上边缘】与锚点元素对齐 32 | /// leftCenter - 在锚点元素【左侧】,且垂直居中 33 | /// leftBottom - 在锚点元素【左侧】,且【下边缘】与锚点元素对齐 34 | /// rightTop - 在锚点元素【右侧】,且【上边缘】与锚点元素对齐 35 | /// rightCenter - 在锚点元素【右侧】,且垂直居中 36 | /// rightBottom - 在锚点元素【右侧】,且【下边缘】与锚点元素对齐 37 | /// 38 | /// Description [FFloat] The position relative to the anchor element. 39 | /// topLeft - In the anchor element [above], and the [leftEdge] is aligned with the anchor element 40 | /// topCenter - In the anchor element [above], and horizontally centered 41 | /// topRight - In the anchor element [above], and the [rightEdge] is aligned with the anchor element 42 | /// bottomLeft - In the anchor element [below], and the [leftEdge] is aligned with the anchor element 43 | /// bottomCenter - In the anchor element [below], and horizontally centered 44 | /// bottomRight - In the anchor element [below], and the [rightEdge] is aligned with the anchor element 45 | /// leftTop - In the anchor element [left], and the [upperEdge] is aligned with the anchor element 46 | /// leftCenter - In the anchor element [left], and vertically centered 47 | /// leftBottom - In the anchor element [left], and the [bottomEdge] is aligned with the anchor element 48 | /// rightTop - In the anchor element [right], and the [upperEdge] is aligned with the anchor element 49 | /// rightCenter - In the anchor element [right], and vertically centered 50 | /// rightBottom - In the anchor element [right side], and the [bottomEdge] is aligned with the anchor element 51 | enum FFloatAlignment { 52 | topLeft, 53 | topCenter, 54 | topRight, 55 | bottomLeft, 56 | bottomCenter, 57 | bottomRight, 58 | leftTop, 59 | leftCenter, 60 | leftBottom, 61 | rightTop, 62 | rightCenter, 63 | rightBottom, 64 | } 65 | 66 | /// 用于返回一个 [Widget],如果只更新内容区域的话,通过 setter((){}) 进行 67 | /// 68 | /// Used to return a [Widget], if only the content area is updated, through setter (() {}) 69 | typedef FloatBuilder = Widget Function(StateSetter setter); 70 | 71 | /// [FFloat] 能够在屏幕的任意位置浮出一个组件。甚至可以基于 [anchor] 锚点组件来动态的确定漂浮组件的位置。 72 | /// [FFloat] 同时提供了绝妙的配置选项。圆角、描边、背景、偏移、装饰三角。 73 | /// [FFloat] 设置了 [FFloatController] 控制器,可以方便的随时对漂浮组件进行控制。 74 | /// 75 | /// [FFloat] can float a component anywhere on the screen. You can even dynamically determine the position of the floating component based on the [anchor] anchor component. 76 | /// [FFloat] also provides wonderful configuration options. Fillet, stroke, background, offset, decorative triangle. 77 | /// [FFloat] The [FFloatController] controller is set, which can easily control the floating component at any time. 78 | // ignore: must_be_immutable 79 | class FFloat extends StatefulWidget { 80 | /// 通过 [FloatBuilder] 返回 [FFloat] 的内容组件。 81 | /// 如果只更新内容区域的话,通过 setter((){}) 进行 82 | /// 83 | /// [FloatBuilder] returns the content component of [FFloat]. 84 | /// If only the content area is updated, proceed via setter (() {}) 85 | final FloatBuilder builder; 86 | 87 | /// [FFloat] 的颜色 88 | /// 89 | /// [FFloat] colors 90 | final Color color; 91 | 92 | /// 锚点组件 93 | /// 94 | /// Anchor component 95 | final Widget anchor; 96 | 97 | /// 位置。通过 [location] 指定 [FFloat] 的位置后,基于锚点确定位置的所有配置将失效。 98 | /// 99 | /// position. After specifying the location of [FFloat] through [location], all configurations that determine the location based on the anchor point will be invalid. 100 | final Offset? location; 101 | 102 | /// [FFloat] 基于 [anchor] 锚点元素的相对位置。 103 | /// 104 | /// [FFloat] Based on the relative position of the [anchor] anchor element. 105 | final FFloatAlignment alignment; 106 | 107 | /// [FFloat] 基于相对确定锚定点的间距 108 | /// 109 | /// [FFloat] Determine the distance between anchor points based on relative 110 | final EdgeInsets margin; 111 | 112 | /// [FFloat] 内部间距 113 | /// 114 | /// [FFloat] Internal spacing 115 | final EdgeInsets? padding; 116 | 117 | /// 点击 [FFloat] 范围外区域是否隐藏。 118 | /// 119 | /// Click [FFloat] to hide the area outside the range. 120 | final bool canTouchOutside; 121 | 122 | /// [FFloat] 浮出时,背景区域的颜色 123 | /// 124 | /// [FFloat] The color of the background area when floating 125 | final Color backgroundColor; 126 | 127 | /// 自动消失时长。如果为 null,就不会自动消失。 128 | /// 129 | /// Duration of automatic disappearance. If it is null, it will not disappear automatically. 130 | final Duration? autoDismissDuration; 131 | 132 | /// 通过 [FFloatController] 可以控制 [FFloat] 的显示/隐藏。详见 [FFloatController]。 133 | /// 134 | /// [FFloatController] can control the display / hide of [FFloat]. See [FFloatController] for details. 135 | final FFloatController? controller; 136 | 137 | /// 显示/隐藏动效时长。默认 `Duration(milliseconds: 100)` 138 | /// 139 | /// Show / hide animation duration. Default `Duration (milliseconds: 100)` 140 | final Duration animDuration; 141 | 142 | /// 三角的宽 143 | /// 144 | /// The width of the triangle 145 | final double triangleWidth; 146 | 147 | /// 三角的高 148 | /// 149 | /// The height of the triangle 150 | final double triangleHeight; 151 | 152 | /// 三角的相对位置。详见 [TriangleAlignment]。 153 | /// 154 | /// The relative position of the triangle. See [TriangleAlignment] for details. 155 | final TriangleAlignment triangleAlignment; 156 | 157 | /// 三角的位置偏移 158 | /// 159 | /// Triangle position offset 160 | final Offset triangleOffset; 161 | 162 | /// 是否隐藏装饰三角 163 | /// 164 | /// Whether to hide the decorative triangle 165 | final bool hideTriangle; 166 | 167 | /// 描边颜色 168 | /// 169 | /// Stroke color 170 | final Color? strokeColor; 171 | 172 | /// 描边宽度 173 | /// 174 | /// Stroke width 175 | final double? strokeWidth; 176 | 177 | /// 圆角。详见 [FFloatCorner]。 178 | /// 179 | /// Corner. See [FFloatCorner] for details. 180 | final FFloatCorner? corner; 181 | 182 | /// 圆角样式。详见 [FFloatCornerStyle]。 183 | /// 184 | /// Corner style. See [FFloatCornerStyle] for details. 185 | final FFloatCornerStyle cornerStyle; 186 | 187 | /// 设置组件阴影颜色 188 | /// 189 | /// Set component shadow color 190 | Color? shadowColor; 191 | 192 | /// 设置组件阴影偏移 193 | /// 194 | /// Set component shadow offset 195 | Offset? shadowOffset; 196 | 197 | /// 设置组件高斯与阴影形状卷积的标准偏差。 198 | /// 199 | /// Sets the standard deviation of the component's Gaussian convolution with the shadow shape. 200 | double shadowBlur; 201 | 202 | /// 设置组件渐变色背景。会覆盖 [backgroundColor] 203 | /// 你可选择 [LinearGradient],[RadialGradient],[SweepGradient] 等.. 204 | /// 205 | /// Sets the gradient background of the component. [backgroundColor] 206 | /// You can choose [LinearGradient], [RadialGradient], [SweepGradient], etc .. 207 | Gradient? gradient; 208 | 209 | _FFloat? _float; 210 | 211 | FFloat( 212 | this.builder, { 213 | required this.anchor, 214 | this.location, 215 | this.margin = EdgeInsets.zero, 216 | this.triangleWidth = 12, 217 | this.triangleHeight = 6, 218 | this.triangleAlignment = TriangleAlignment.center, 219 | this.triangleOffset = Offset.zero, 220 | this.alignment = FFloatAlignment.topCenter, 221 | this.padding, 222 | this.color = _FFloatContent.DefaultColor, 223 | this.strokeColor, 224 | this.strokeWidth, 225 | this.corner, 226 | this.cornerStyle = FFloatCornerStyle.round, 227 | this.backgroundColor = Colors.transparent, 228 | this.gradient, 229 | this.canTouchOutside = true, 230 | this.hideTriangle = false, 231 | this.autoDismissDuration, 232 | this.controller, 233 | this.animDuration = const Duration(milliseconds: 100), 234 | this.shadowBlur = 1.0, 235 | this.shadowColor, 236 | this.shadowOffset, 237 | }); 238 | 239 | @override 240 | _FFloatState createState() => _FFloatState(); 241 | 242 | void show(BuildContext context) { 243 | _float = _FFloat( 244 | context, 245 | builder, 246 | location: location, 247 | margin: margin, 248 | triangleWidth: triangleWidth, 249 | triangleHeight: triangleHeight, 250 | triangleAlignment: triangleAlignment, 251 | triangleOffset: triangleOffset, 252 | alignment: alignment, 253 | padding: padding, 254 | color: color, 255 | strokeColor: strokeColor, 256 | strokeWidth: strokeWidth, 257 | corner: corner, 258 | cornerStyle: cornerStyle, 259 | backgroundColor: backgroundColor, 260 | gradient: gradient, 261 | canTouchOutside: canTouchOutside, 262 | hideTriangle: hideTriangle, 263 | autoDismissDuration: autoDismissDuration, 264 | controller: controller, 265 | animDuration: animDuration, 266 | shadowBlur: shadowBlur, 267 | shadowColor: shadowColor, 268 | shadowOffset: shadowOffset, 269 | ); 270 | _float!.show(); 271 | } 272 | 273 | void dismiss() { 274 | if (_float != null) { 275 | _float!.dismiss(); 276 | } 277 | } 278 | } 279 | 280 | class _FFloatState extends State { 281 | GlobalKey key = GlobalKey(); 282 | Offset? anchorLocation; 283 | Size? anchorSize; 284 | 285 | _FFloat? _float; 286 | 287 | @override 288 | void initState() { 289 | init(); 290 | if (widget.location == null) { 291 | postUpdateCallback(); 292 | } 293 | super.initState(); 294 | } 295 | 296 | void init() { 297 | createFloat(context); 298 | } 299 | 300 | void createFloat(BuildContext context) { 301 | _float = _FFloat( 302 | context, 303 | widget.builder, 304 | location: widget.location, 305 | margin: widget.margin, 306 | triangleWidth: widget.triangleWidth, 307 | triangleHeight: widget.triangleHeight, 308 | triangleAlignment: widget.triangleAlignment, 309 | triangleOffset: widget.triangleOffset, 310 | alignment: widget.alignment, 311 | padding: widget.padding, 312 | color: widget.color, 313 | strokeColor: widget.strokeColor, 314 | strokeWidth: widget.strokeWidth, 315 | corner: widget.corner, 316 | cornerStyle: widget.cornerStyle, 317 | backgroundColor: widget.backgroundColor, 318 | gradient: widget.gradient, 319 | canTouchOutside: widget.canTouchOutside, 320 | hideTriangle: widget.hideTriangle, 321 | autoDismissDuration: widget.autoDismissDuration, 322 | controller: widget.controller, 323 | animDuration: widget.animDuration, 324 | shadowBlur: widget.shadowBlur, 325 | shadowColor: widget.shadowColor, 326 | shadowOffset: widget.shadowOffset, 327 | ); 328 | } 329 | 330 | void postUpdateCallback() { 331 | WidgetsBinding.instance.addPostFrameCallback((time) { 332 | if (!mounted) return; 333 | RenderBox? box = key.currentContext?.findRenderObject() as RenderBox?; 334 | Offset? location = box?.localToGlobal(Offset.zero); 335 | Size? size = box?.size; 336 | bool needUpdate = false; 337 | if (location != null && location != anchorLocation) { 338 | needUpdate = true; 339 | anchorLocation = location; 340 | } 341 | if (size != null && size != anchorSize) { 342 | needUpdate = true; 343 | anchorSize = size; 344 | } 345 | if (_float != null && needUpdate) { 346 | _float!.update(anchorSize, anchorLocation); 347 | } 348 | postUpdateCallback(); 349 | }); 350 | } 351 | 352 | @override 353 | void dispose() { 354 | if (_float != null) { 355 | _float!.dispose(); 356 | _float = null; 357 | } 358 | super.dispose(); 359 | } 360 | 361 | @override 362 | void didUpdateWidget(FFloat oldWidget) { 363 | if (_float != null) { 364 | asyncParams(); 365 | if (_float!.isShow) { 366 | WidgetsBinding.instance.addPostFrameCallback((_) { 367 | if (!mounted) return; 368 | if (_float != null) { 369 | _float!._showFloat(); 370 | } 371 | }); 372 | } 373 | } 374 | super.didUpdateWidget(oldWidget); 375 | } 376 | 377 | void asyncParams() { 378 | if (_float == null) return; 379 | _float! 380 | ..context = context 381 | ..builder = widget.builder 382 | ..location = widget.location 383 | ..margin = widget.margin 384 | ..triangleWidth = widget.triangleWidth 385 | ..triangleHeight = widget.triangleHeight 386 | ..triangleAlignment = widget.triangleAlignment 387 | ..triangleOffset = widget.triangleOffset 388 | ..alignment = widget.alignment 389 | ..padding = widget.padding 390 | ..color = widget.color 391 | ..strokeColor = widget.strokeColor 392 | ..strokeWidth = widget.strokeWidth 393 | ..corner = widget.corner 394 | ..cornerStyle = widget.cornerStyle 395 | ..backgroundColor = widget.backgroundColor 396 | ..gradient = widget.gradient 397 | ..canTouchOutside = widget.canTouchOutside 398 | ..hideTriangle = widget.hideTriangle 399 | ..autoDismissDuration = widget.autoDismissDuration 400 | ..controller = widget.controller 401 | ..animDuration = widget.animDuration 402 | ..shadowBlur = widget.shadowBlur 403 | ..shadowColor = widget.shadowColor 404 | ..shadowOffset = widget.shadowOffset; 405 | } 406 | 407 | @override 408 | void didChangeDependencies() { 409 | super.didChangeDependencies(); 410 | } 411 | 412 | @override 413 | Widget build(BuildContext context) { 414 | if (widget.location != null) { 415 | return Container(); 416 | } else { 417 | return LayoutBuilder( 418 | key: key, 419 | builder: (context, _) { 420 | return GestureDetector( 421 | onTap: handleOnTap, 422 | child: widget.anchor, 423 | ); 424 | }, 425 | ); 426 | } 427 | } 428 | 429 | void handleOnTap() { 430 | if (_float != null) { 431 | // asyncParams(); 432 | _float!.show(); 433 | } 434 | } 435 | } 436 | 437 | class _FFloat { 438 | double triangleWidth; 439 | double triangleHeight; 440 | TriangleAlignment triangleAlignment; 441 | Offset triangleOffset; 442 | Color? color; 443 | Widget? child; 444 | FFloatAlignment alignment; 445 | EdgeInsets margin; 446 | EdgeInsets? padding; 447 | Color? strokeColor; 448 | double? strokeWidth; 449 | FFloatCorner? corner; 450 | FFloatCornerStyle cornerStyle; 451 | FloatBuilder? builder; 452 | bool canTouchOutside = false; 453 | Color backgroundColor; 454 | Gradient? gradient; 455 | bool hideTriangle = false; 456 | Duration? autoDismissDuration; 457 | FFloatController? controller; 458 | Offset? location; 459 | Duration? animDuration; 460 | double shadowBlur; 461 | Color? shadowColor; 462 | Offset? shadowOffset; 463 | 464 | Offset? childLocation; 465 | Size? childSize; 466 | 467 | OverlayEntry? _overlayEntry; 468 | bool _isShow = false; 469 | 470 | bool get isShow => _isShow; 471 | 472 | set isShow(bool value) { 473 | if (_isShow == value) return; 474 | _isShow = value; 475 | controller?.isShow = value; 476 | } 477 | 478 | Timer? dismissTimer; 479 | 480 | _FFloatContentController? ffloatContentController; 481 | 482 | ValueNotifier? notifier; 483 | 484 | BuildContext context; 485 | 486 | _FFloat( 487 | this.context, 488 | this.builder, { 489 | this.child, 490 | this.location, 491 | this.margin = EdgeInsets.zero, 492 | this.triangleWidth = 12, 493 | this.triangleHeight = 6, 494 | this.triangleAlignment = TriangleAlignment.center, 495 | this.triangleOffset = Offset.zero, 496 | this.alignment = FFloatAlignment.topCenter, 497 | this.padding, 498 | this.color = _FFloatContent.DefaultColor, 499 | this.strokeColor, 500 | this.strokeWidth, 501 | this.corner, 502 | this.cornerStyle = FFloatCornerStyle.round, 503 | this.backgroundColor = Colors.transparent, 504 | this.gradient, 505 | this.canTouchOutside = true, 506 | this.hideTriangle = false, 507 | this.autoDismissDuration, 508 | this.controller, 509 | this.animDuration, 510 | this.shadowBlur = 1.0, 511 | this.shadowColor, 512 | this.shadowOffset, 513 | }) { 514 | init(); 515 | } 516 | 517 | void init() { 518 | notifier = new ValueNotifier(0); 519 | notifier!.addListener(() { 520 | if (notifier?.value == 0) { 521 | realDismiss(); 522 | } 523 | }); 524 | ffloatContentController = new _FFloatContentController(); 525 | controller?._show = () { 526 | show(); 527 | }; 528 | controller?._dismiss = () { 529 | dismiss(); 530 | }; 531 | controller?._rebuildShow = () { 532 | rebuildShow(); 533 | }; 534 | controller?._setState = (VoidCallback fn) { 535 | ffloatContentController?.setState(fn); 536 | }; 537 | } 538 | 539 | void update(Size? anchorSize, Offset? location) { 540 | childSize = anchorSize; 541 | childLocation = location; 542 | if (ffloatContentController != null && isShow) { 543 | ffloatContentController!.update(anchorSize, location); 544 | } 545 | } 546 | 547 | void dispose() { 548 | controller?.dispose(); 549 | ffloatContentController?.dispose(); 550 | if (dismissTimer != null && dismissTimer!.isActive) { 551 | dismissTimer!.cancel(); 552 | dismissTimer = null; 553 | } 554 | } 555 | 556 | void show() { 557 | /// Prevent duplicate display 558 | if (isShow) return; 559 | _showFloat(); 560 | } 561 | 562 | void rebuildShow() { 563 | _showFloat(); 564 | } 565 | 566 | void _showFloat() { 567 | final bool hasShow = isShow; 568 | isShow = true; 569 | if (_overlayEntry != null) { 570 | _overlayEntry!.remove(); 571 | } 572 | OverlayState overlayState = Overlay.of(context); 573 | _overlayEntry = OverlayEntry(builder: (BuildContext context) { 574 | Widget floatContent = _FFloatContent( 575 | (location != null ? location : childLocation) ?? Offset.zero, 576 | builder, 577 | anchorSize: childSize ?? Size.zero, 578 | alignment: alignment, 579 | triangleWidth: triangleWidth, 580 | triangleHeight: triangleHeight, 581 | triangleOffset: triangleOffset, 582 | triangleAlignment: triangleAlignment, 583 | margin: margin, 584 | padding: padding, 585 | color: color, 586 | strokeColor: strokeColor, 587 | strokeWidth: strokeWidth, 588 | corner: corner, 589 | cornerStyle: cornerStyle, 590 | backgroundColor: backgroundColor, 591 | gradient: gradient, 592 | onTouchBackground: canTouchOutside ? dismiss : null, 593 | hideTriangle: hideTriangle, 594 | controller: ffloatContentController, 595 | notifier: notifier, 596 | animDuration: animDuration, 597 | shadowOffset: shadowOffset, 598 | shadowColor: shadowColor, 599 | shadowBlur: shadowBlur, 600 | initShow: hasShow, 601 | ); 602 | return floatContent; 603 | }); 604 | overlayState.insert(_overlayEntry!); 605 | if (autoDismissDuration != null && dismissTimer == null) { 606 | dismissTimer = Timer(autoDismissDuration!, () { 607 | if (_overlayEntry != null) { 608 | dismissTimer = null; 609 | dismiss(); 610 | } 611 | }); 612 | } 613 | } 614 | 615 | void dismiss() { 616 | isShow = false; 617 | if (dismissTimer != null && dismissTimer!.isActive) { 618 | dismissTimer!.cancel(); 619 | dismissTimer = null; 620 | } 621 | if (notifier != null) { 622 | notifier!.value = 1; 623 | } 624 | } 625 | 626 | void realDismiss() { 627 | isShow = false; 628 | if (_overlayEntry != null) { 629 | _overlayEntry!.remove(); 630 | _overlayEntry = null; 631 | } 632 | } 633 | } 634 | 635 | // ignore: must_be_immutable 636 | class _FFloatContent extends StatefulWidget { 637 | static const Color DefaultColor = Color(0x7F000000); 638 | 639 | FloatBuilder? builder; 640 | Size? anchorSize; 641 | Offset? location; 642 | Widget? child; 643 | double triangleWidth; 644 | double triangleHeight; 645 | TriangleAlignment triangleAlignment; 646 | Offset triangleOffset; 647 | Color? color; 648 | FFloatAlignment alignment; 649 | EdgeInsets margin; 650 | EdgeInsets? padding; 651 | Color? strokeColor; 652 | double? strokeWidth; 653 | FFloatCorner? corner; 654 | FFloatCornerStyle cornerStyle; 655 | Color backgroundColor; 656 | Gradient? gradient; 657 | VoidCallback? onTouchBackground; 658 | bool hideTriangle = false; 659 | _FFloatContentController? controller; 660 | ValueNotifier? notifier; 661 | Duration? animDuration; 662 | double shadowBlur; 663 | Color? shadowColor; 664 | Offset? shadowOffset; 665 | bool? initShow; 666 | 667 | _FFloatContent( 668 | this.location, 669 | this.builder, { 670 | this.anchorSize = Size.zero, 671 | this.triangleWidth = 12, 672 | this.triangleHeight = 6, 673 | this.triangleAlignment = TriangleAlignment.center, 674 | this.triangleOffset = Offset.zero, 675 | this.alignment = FFloatAlignment.topCenter, 676 | this.margin = EdgeInsets.zero, 677 | this.padding, 678 | this.color = DefaultColor, 679 | this.strokeColor, 680 | this.strokeWidth = 1.0, 681 | this.corner, 682 | this.cornerStyle = FFloatCornerStyle.round, 683 | this.backgroundColor = Colors.transparent, 684 | this.gradient, 685 | this.onTouchBackground, 686 | this.hideTriangle = false, 687 | this.controller, 688 | this.notifier, 689 | this.animDuration, 690 | this.shadowColor, 691 | this.shadowOffset, 692 | this.shadowBlur = 1.0, 693 | this.initShow, 694 | }); 695 | 696 | @override 697 | _FFloatContentState createState() => _FFloatContentState(); 698 | } 699 | 700 | class _FFloatContentState extends State<_FFloatContent> 701 | with TickerProviderStateMixin { 702 | GlobalKey key = GlobalKey(); 703 | Size? areaSize; 704 | Offset? location; 705 | Size? anchorSize; 706 | AnimationController? animationController; 707 | Animation? scaleAnimation; 708 | bool? init; 709 | 710 | @override 711 | void initState() { 712 | init = true; 713 | super.initState(); 714 | animationController = new AnimationController( 715 | vsync: this, 716 | duration: widget.animDuration ?? Duration(milliseconds: 0)); 717 | scaleAnimation = 718 | Tween(begin: 0.0, end: 1.0).animate(animationController!); 719 | scaleAnimation!.addListener(() { 720 | setState(() {}); 721 | }); 722 | scaleAnimation!.addStatusListener((status) { 723 | // print('scaleAnimation.status = ${status.toString()}'); 724 | if ((status == AnimationStatus.dismissed || 725 | status == AnimationStatus.completed) && 726 | widget.notifier != null && 727 | widget.notifier!.value == 1) { 728 | widget.notifier!.value = 0; 729 | } 730 | }); 731 | animationController!.forward(); 732 | if (widget.notifier != null) { 733 | widget.notifier!.addListener(onNotifier); 734 | } 735 | location = widget.location; 736 | anchorSize = widget.anchorSize; 737 | if (widget.controller != null) { 738 | widget.controller!.state = this; 739 | } 740 | postUpdateCallback(); 741 | } 742 | 743 | void onNotifier() { 744 | if (mounted && 745 | widget.notifier != null && 746 | widget.notifier!.value == 1 && 747 | animationController != null) { 748 | animationController!.reverse(from: 1.0); 749 | } 750 | } 751 | 752 | void _setState(Function? func) { 753 | if (mounted && func != null) { 754 | setState(() { 755 | func(); 756 | }); 757 | } 758 | } 759 | 760 | void postUpdateCallback() { 761 | WidgetsBinding.instance.addPostFrameCallback((time) { 762 | if (!mounted) return; 763 | RenderBox? box = key.currentContext?.findRenderObject() as RenderBox?; 764 | Size? size = box?.size; 765 | if (size != null && areaSize != size) { 766 | areaSize = size; 767 | setState(() {}); 768 | } 769 | }); 770 | } 771 | 772 | @override 773 | void didUpdateWidget(_FFloatContent oldWidget) { 774 | super.didUpdateWidget(oldWidget); 775 | location = widget.location; 776 | anchorSize = widget.anchorSize; 777 | // if (animationController != null) { 778 | // animationController.forward(); 779 | // } 780 | } 781 | 782 | @override 783 | void dispose() { 784 | if (widget.notifier != null) { 785 | widget.notifier!.removeListener(onNotifier); 786 | } 787 | if (animationController != null) { 788 | animationController!.dispose(); 789 | } 790 | super.dispose(); 791 | } 792 | 793 | @override 794 | Widget build(BuildContext context) { 795 | if (widget.hideTriangle) { 796 | widget.triangleWidth = 0; 797 | widget.triangleHeight = 0; 798 | } 799 | List children = []; 800 | Widget background = Container( 801 | width: MediaQuery.of(context).size.width, 802 | height: MediaQuery.of(context).size.height, 803 | color: widget.backgroundColor, 804 | ); 805 | if (widget.onTouchBackground != null) { 806 | background = GestureDetector( 807 | onTap: widget.onTouchBackground, 808 | child: background, 809 | ); 810 | } else { 811 | background = IgnorePointer( 812 | child: background, 813 | ); 814 | } 815 | children.add(background); 816 | Widget content = buildFloatContent(); 817 | children.add(content); 818 | return Stack( 819 | clipBehavior: Clip.none, 820 | children: children, 821 | ); 822 | } 823 | 824 | Widget buildFloatContent() { 825 | List children = []; 826 | Color color = widget.color ?? _FFloatContent.DefaultColor; 827 | BorderRadius borderRadius = (widget.corner == null) 828 | ? BorderRadius.all(Radius.circular(0)) 829 | : BorderRadius.only( 830 | topLeft: Radius.circular(widget.corner!.leftTopCorner), 831 | topRight: Radius.circular(widget.corner!.rightTopCorner), 832 | bottomRight: Radius.circular(widget.corner!.rightBottomCorner), 833 | bottomLeft: Radius.circular(widget.corner!.leftBottomCorner), 834 | ); 835 | var sideColor = widget.strokeColor ?? Colors.transparent; 836 | var borderSide = BorderSide( 837 | width: widget.strokeWidth ?? 0, 838 | color: sideColor, 839 | style: BorderStyle.solid, 840 | ); 841 | var shape = widget.cornerStyle == FFloatCornerStyle.round 842 | ? RoundedRectangleBorder( 843 | borderRadius: borderRadius, 844 | side: borderSide, 845 | ) 846 | : BeveledRectangleBorder( 847 | borderRadius: borderRadius, 848 | side: borderSide, 849 | ); 850 | var decoration = ShapeDecoration( 851 | color: widget.gradient == null ? color : null, 852 | gradient: widget.gradient, 853 | shape: shape, 854 | shadows: widget.shadowColor != null && widget.shadowBlur != 0 855 | ? [ 856 | BoxShadow( 857 | color: widget.shadowColor ?? const Color(0xFF000000), 858 | offset: widget.shadowOffset ?? Offset(0, 0), 859 | blurRadius: widget.shadowBlur ?? 0.0, 860 | ) 861 | ] 862 | : null, 863 | ); 864 | Widget content = Container( 865 | decoration: decoration, 866 | padding: widget.padding, 867 | key: key, 868 | child: widget.builder != null ? widget.builder!(setState) : Container(), 869 | ); 870 | children.add(content); 871 | if (areaSize != null) { 872 | double rotate = calculateTriangleRotate(); 873 | EdgeInsets triangleOffset = calculateTriangleOffset(); 874 | Widget triangle = Positioned( 875 | left: triangleOffset.left == 0 ? null : triangleOffset.left, 876 | top: triangleOffset.top == 0 ? null : triangleOffset.top, 877 | right: triangleOffset.right == 0 ? null : triangleOffset.right, 878 | bottom: triangleOffset.bottom == 0 ? null : triangleOffset.bottom, 879 | child: Transform.rotate( 880 | angle: rotate, 881 | child: CustomPaint( 882 | size: Size(widget.triangleWidth ?? 0, widget.triangleHeight ?? 0), 883 | painter: (widget.strokeWidth != null && widget.strokeWidth! > 0) 884 | ? _TrianglePainter( 885 | gradient: widget.gradient, 886 | color: color, 887 | strokeColor: widget.strokeColor, 888 | strokeWidth: widget.strokeWidth, 889 | ) 890 | : null, 891 | ), 892 | ), 893 | ); 894 | children.add(triangle); 895 | } 896 | Offset areaOffset = calculateAreaOffset(); 897 | Widget floatContent = Positioned( 898 | left: (location?.dx ?? 0) + areaOffset.dx, 899 | top: (location?.dy ?? 0) + areaOffset.dy, 900 | child: Offstage( 901 | offstage: areaSize == null, 902 | child: Material( 903 | color: Colors.transparent, 904 | child: Transform.scale( 905 | scale: scaleAnimation?.value, 906 | alignment: matchScaleAnim(widget.anchorSize == Size.zero), 907 | child: Stack( 908 | clipBehavior: Clip.none, 909 | children: children, 910 | ), 911 | ), 912 | ), 913 | ), 914 | ); 915 | return floatContent; 916 | } 917 | 918 | Alignment matchScaleAnim(bool center) { 919 | if (center) return Alignment.center; 920 | switch (widget.alignment) { 921 | case FFloatAlignment.topLeft: 922 | return Alignment.bottomLeft; 923 | case FFloatAlignment.topCenter: 924 | return Alignment.bottomCenter; 925 | case FFloatAlignment.topRight: 926 | return Alignment.bottomRight; 927 | case FFloatAlignment.bottomLeft: 928 | return Alignment.topLeft; 929 | case FFloatAlignment.bottomCenter: 930 | return Alignment.topCenter; 931 | case FFloatAlignment.bottomRight: 932 | return Alignment.topRight; 933 | case FFloatAlignment.leftTop: 934 | return Alignment.topRight; 935 | case FFloatAlignment.leftCenter: 936 | return Alignment.centerRight; 937 | case FFloatAlignment.leftBottom: 938 | return Alignment.bottomRight; 939 | case FFloatAlignment.rightTop: 940 | return Alignment.topLeft; 941 | case FFloatAlignment.rightCenter: 942 | return Alignment.centerLeft; 943 | case FFloatAlignment.rightBottom: 944 | return Alignment.bottomLeft; 945 | } 946 | return Alignment.center; 947 | } 948 | 949 | Offset calculateAreaOffset() { 950 | if (areaSize == null) return Offset.zero; 951 | Offset offset = Offset( 952 | widget.margin.left - widget.margin.right, 953 | widget.margin.top - widget.margin.bottom, 954 | ); 955 | print("areaSize: $areaSize"); 956 | print("offset: $offset"); 957 | switch (widget.alignment) { 958 | case FFloatAlignment.topLeft: 959 | return Offset( 960 | 0, 961 | -areaSize!.height - widget.triangleHeight, 962 | ) + 963 | offset; 964 | case FFloatAlignment.topCenter: 965 | return Offset( 966 | (anchorSize!.width / 2.0) - (areaSize!.width / 2.0), 967 | -areaSize!.height - widget.triangleHeight, 968 | ) + 969 | offset; 970 | case FFloatAlignment.topRight: 971 | return Offset( 972 | anchorSize!.width - areaSize!.width, 973 | -areaSize!.height - widget.triangleHeight, 974 | ) + 975 | offset; 976 | case FFloatAlignment.bottomLeft: 977 | return Offset( 978 | 0, 979 | anchorSize!.height + widget.triangleHeight, 980 | ) + 981 | offset; 982 | case FFloatAlignment.bottomCenter: 983 | return Offset( 984 | (anchorSize!.width / 2.0) - (areaSize!.width / 2.0), 985 | anchorSize!.height + widget.triangleHeight, 986 | ) + 987 | offset; 988 | 989 | case FFloatAlignment.bottomRight: 990 | return Offset( 991 | anchorSize!.width - areaSize!.width, 992 | anchorSize!.height + widget.triangleHeight, 993 | ) + 994 | offset; 995 | 996 | case FFloatAlignment.leftTop: 997 | return Offset( 998 | -areaSize!.width - widget.triangleHeight, 999 | 0, 1000 | ) + 1001 | offset; 1002 | 1003 | case FFloatAlignment.leftCenter: 1004 | return Offset( 1005 | -areaSize!.width - widget.triangleHeight, 1006 | (anchorSize!.height / 2.0) - (areaSize!.height / 2.0), 1007 | ) + 1008 | offset; 1009 | 1010 | case FFloatAlignment.leftBottom: 1011 | return Offset( 1012 | -areaSize!.width - widget.triangleHeight, 1013 | anchorSize!.height - areaSize!.height, 1014 | ) + 1015 | offset; 1016 | 1017 | case FFloatAlignment.rightTop: 1018 | return Offset( 1019 | anchorSize!.width + widget.triangleHeight, 1020 | 0, 1021 | ) + 1022 | offset; 1023 | 1024 | case FFloatAlignment.rightCenter: 1025 | return Offset( 1026 | anchorSize!.width + widget.triangleHeight, 1027 | (anchorSize!.height / 2.0) - (areaSize!.height / 2.0), 1028 | ) + 1029 | offset; 1030 | 1031 | case FFloatAlignment.rightBottom: 1032 | return Offset( 1033 | anchorSize!.width + widget.triangleHeight, 1034 | anchorSize!.height - areaSize!.height, 1035 | ) + 1036 | offset; 1037 | } 1038 | return Offset.zero; 1039 | } 1040 | 1041 | double calculateTriangleRotate() { 1042 | switch (widget.alignment) { 1043 | case FFloatAlignment.topLeft: 1044 | case FFloatAlignment.topCenter: 1045 | case FFloatAlignment.topRight: 1046 | return pi; 1047 | case FFloatAlignment.bottomLeft: 1048 | case FFloatAlignment.bottomCenter: 1049 | case FFloatAlignment.bottomRight: 1050 | return 0.0; 1051 | case FFloatAlignment.leftTop: 1052 | case FFloatAlignment.leftCenter: 1053 | case FFloatAlignment.leftBottom: 1054 | return pi / 2.0; 1055 | case FFloatAlignment.rightTop: 1056 | case FFloatAlignment.rightCenter: 1057 | case FFloatAlignment.rightBottom: 1058 | return -pi / 2.0; 1059 | } 1060 | return pi; 1061 | } 1062 | 1063 | EdgeInsets calculateTriangleOffset() { 1064 | Offset srcOffset = widget.triangleOffset; 1065 | EdgeInsets offset = EdgeInsets.zero; 1066 | 1067 | /// 三角和内容区域会有一点间距,可能是由于计算精度和绘制像素精度有差异造成的 1068 | double fixOffset = 0.13 + (widget.strokeWidth ?? 0.0) / 2.0; 1069 | switch (widget.alignment) { 1070 | case FFloatAlignment.topLeft: 1071 | case FFloatAlignment.topCenter: 1072 | case FFloatAlignment.topRight: 1073 | double bottom = -widget.triangleHeight + fixOffset; 1074 | switch (widget.triangleAlignment) { 1075 | case TriangleAlignment.start: 1076 | offset = EdgeInsets.only( 1077 | left: srcOffset.dx, 1078 | bottom: bottom, 1079 | ); 1080 | break; 1081 | case TriangleAlignment.center: 1082 | offset = EdgeInsets.only( 1083 | left: srcOffset.dx + 1084 | (areaSize!.width / 2.0) - 1085 | (widget.triangleWidth / 2), 1086 | bottom: bottom, 1087 | ); 1088 | break; 1089 | case TriangleAlignment.end: 1090 | offset = EdgeInsets.only( 1091 | left: srcOffset.dx + areaSize!.width - widget.triangleWidth, 1092 | bottom: bottom, 1093 | ); 1094 | break; 1095 | } 1096 | break; 1097 | case FFloatAlignment.bottomLeft: 1098 | case FFloatAlignment.bottomCenter: 1099 | case FFloatAlignment.bottomRight: 1100 | double top = -widget.triangleHeight + fixOffset; 1101 | switch (widget.triangleAlignment) { 1102 | case TriangleAlignment.start: 1103 | offset = EdgeInsets.only( 1104 | left: srcOffset.dx, 1105 | top: top, 1106 | ); 1107 | break; 1108 | case TriangleAlignment.center: 1109 | offset = EdgeInsets.only( 1110 | left: srcOffset.dx + 1111 | (areaSize!.width / 2.0) - 1112 | (widget.triangleWidth / 2), 1113 | top: top, 1114 | ); 1115 | break; 1116 | case TriangleAlignment.end: 1117 | offset = EdgeInsets.only( 1118 | left: srcOffset.dx + areaSize!.width - widget.triangleWidth, 1119 | top: top, 1120 | ); 1121 | break; 1122 | } 1123 | break; 1124 | case FFloatAlignment.leftTop: 1125 | case FFloatAlignment.leftCenter: 1126 | case FFloatAlignment.leftBottom: 1127 | double startTop = srcOffset.dy + 1128 | (-widget.triangleHeight / 2.0) + 1129 | (widget.triangleWidth / 2.0); 1130 | double right = (-widget.triangleWidth / 2.0) - 1131 | (widget.triangleHeight / 2.0) + 1132 | fixOffset; 1133 | switch (widget.triangleAlignment) { 1134 | case TriangleAlignment.start: 1135 | offset = EdgeInsets.only( 1136 | right: right, 1137 | top: startTop, 1138 | ); 1139 | break; 1140 | case TriangleAlignment.center: 1141 | offset = EdgeInsets.only( 1142 | right: right, 1143 | top: 1144 | startTop + (areaSize!.height / 2.0) - (widget.triangleWidth / 2.0), 1145 | ); 1146 | break; 1147 | case TriangleAlignment.end: 1148 | offset = EdgeInsets.only( 1149 | right: right, 1150 | top: startTop + areaSize!.height - widget.triangleWidth, 1151 | ); 1152 | break; 1153 | } 1154 | break; 1155 | case FFloatAlignment.rightTop: 1156 | case FFloatAlignment.rightCenter: 1157 | case FFloatAlignment.rightBottom: 1158 | double startTop = srcOffset.dy + 1159 | (-widget.triangleHeight / 2.0) + 1160 | (widget.triangleWidth / 2.0); 1161 | double left = (-widget.triangleWidth / 2.0) - 1162 | (widget.triangleHeight / 2.0) + 1163 | fixOffset; 1164 | switch (widget.triangleAlignment) { 1165 | case TriangleAlignment.start: 1166 | offset = EdgeInsets.only( 1167 | left: left, 1168 | top: startTop, 1169 | ); 1170 | break; 1171 | case TriangleAlignment.center: 1172 | offset = EdgeInsets.only( 1173 | left: left, 1174 | top: 1175 | startTop + (areaSize!.height / 2.0) - (widget.triangleWidth / 2.0), 1176 | ); 1177 | break; 1178 | case TriangleAlignment.end: 1179 | offset = EdgeInsets.only( 1180 | left: left, 1181 | top: startTop + areaSize!.height - widget.triangleWidth, 1182 | ); 1183 | break; 1184 | } 1185 | break; 1186 | } 1187 | return offset; 1188 | } 1189 | } 1190 | 1191 | class _FFloatContentController { 1192 | _FFloatContentState? state; 1193 | 1194 | setState(VoidCallback fn) { 1195 | state?._setState(fn); 1196 | } 1197 | 1198 | update(Size? anchorSize, Offset? location) { 1199 | setState(() { 1200 | state?.anchorSize = anchorSize; 1201 | state?.location = location; 1202 | }); 1203 | } 1204 | 1205 | dispose() { 1206 | state = null; 1207 | } 1208 | } 1209 | 1210 | /// 圆角。 1211 | /// 1212 | /// corner 1213 | class FFloatCorner { 1214 | final double leftTopCorner; 1215 | final double rightTopCorner; 1216 | final double rightBottomCorner; 1217 | final double leftBottomCorner; 1218 | 1219 | /// 指定每一个圆角的大小 1220 | /// 1221 | /// Specify the size of each rounded corner 1222 | const FFloatCorner({ 1223 | this.leftTopCorner = 0, 1224 | this.rightTopCorner = 0, 1225 | this.rightBottomCorner = 0, 1226 | this.leftBottomCorner = 0, 1227 | }); 1228 | 1229 | /// 设置所有圆角为一个大小 1230 | /// 1231 | /// Set all rounded corners to one size 1232 | FFloatCorner.all(double radius) 1233 | : leftTopCorner = radius, 1234 | rightTopCorner = radius, 1235 | rightBottomCorner = radius, 1236 | leftBottomCorner = radius; 1237 | } 1238 | 1239 | /// 圆角风格。 1240 | /// [round] - 圆角 1241 | /// [bevel] - 斜角 1242 | /// 1243 | /// Rounded corner style. 1244 | /// [round]-rounded corners 1245 | /// [bevel]-beveled corners 1246 | enum FFloatCornerStyle { 1247 | round, 1248 | bevel, 1249 | } 1250 | 1251 | /// 通过 [FFloatController] 可以控制 [FFloat] 的显示、隐藏,以及感知状态变化。 1252 | /// 1253 | /// [FFloatController] can control [FFloat] display, hide, and sense state changes. 1254 | class FFloatController { 1255 | VoidCallback? _callback; 1256 | 1257 | bool _isShow = false; 1258 | 1259 | /// [FFloat] 是否显示 1260 | /// 1261 | /// [FFloat] Whether to display 1262 | bool get isShow => _isShow; 1263 | 1264 | set isShow(bool value) { 1265 | if (_isShow == value) return; 1266 | _isShow = value; 1267 | if (_callback != null) { 1268 | _callback!(); 1269 | } 1270 | } 1271 | 1272 | // _FFloatState _state; 1273 | 1274 | VoidCallback? _show; 1275 | VoidCallback? _dismiss; 1276 | VoidCallback? _rebuildShow; 1277 | StateSetter? _setState; 1278 | 1279 | /// 隐藏 [FFloat] 1280 | /// 1281 | /// Hide [FFloat] 1282 | void dismiss() { 1283 | if (_dismiss != null) { 1284 | _dismiss!(); 1285 | } 1286 | } 1287 | 1288 | /// 显示 [FFloat]。如果已经显示,将不会再次重建。 1289 | /// 1290 | /// Show [FFloat]。If it is already displayed, it will not be rebuilt again. 1291 | void show() { 1292 | if (_show != null) { 1293 | _show!(); 1294 | } 1295 | } 1296 | 1297 | /// 显示 [FFloat]。会重建。 1298 | /// 1299 | /// [FFloat] is displayed. Will rebuild. 1300 | void rebuildShow() { 1301 | if (_rebuildShow != null) { 1302 | _rebuildShow!(); 1303 | } 1304 | } 1305 | 1306 | /// 销毁 1307 | /// 1308 | /// destroy 1309 | dispose() { 1310 | // _callback = null; 1311 | } 1312 | 1313 | /// 设置监听。当 [FFloat] 显示状态发生变化的时候会回调。 1314 | /// 1315 | /// Set up monitoring. It will be called back when [FFloat] display status changes. 1316 | setStateChangedListener(VoidCallback listener) { 1317 | _callback = listener; 1318 | } 1319 | 1320 | setState(VoidCallback fn) { 1321 | _setState?.call(fn); 1322 | } 1323 | } 1324 | 1325 | class _TrianglePainter extends CustomPainter { 1326 | Color color; 1327 | double? strokeWidth; 1328 | Color? strokeColor; 1329 | Gradient? gradient; 1330 | 1331 | _TrianglePainter({ 1332 | this.color = _FFloatContent.DefaultColor, 1333 | this.strokeWidth = 0, 1334 | this.strokeColor, 1335 | this.gradient, 1336 | }); 1337 | 1338 | @override 1339 | void paint(Canvas canvas, Size size) { 1340 | Paint paint = Paint(); 1341 | 1342 | if (gradient != null) { 1343 | paint 1344 | ..isAntiAlias = true 1345 | ..strokeWidth = strokeWidth ?? 0 1346 | ..style = PaintingStyle.fill 1347 | ..shader = gradient 1348 | ?.createShader(Rect.fromLTRB(0, 0, size.width, size.height)); 1349 | } else { 1350 | paint 1351 | ..isAntiAlias = true 1352 | ..color = color 1353 | ..strokeWidth = strokeWidth ?? 0 1354 | ..style = PaintingStyle.fill; 1355 | } 1356 | Path path = Path(); 1357 | path.moveTo(size.width / 2, 0); 1358 | path.lineTo(0, size.height); 1359 | path.lineTo(size.width, size.height); 1360 | path.close(); 1361 | canvas.drawPath(path, paint); 1362 | 1363 | if (strokeColor != null && strokeWidth != null && strokeWidth! > 0) { 1364 | paint 1365 | ..shader = null 1366 | ..color = strokeColor! 1367 | ..style = PaintingStyle.stroke; 1368 | canvas.drawPath(path, paint); 1369 | } 1370 | } 1371 | 1372 | @override 1373 | bool shouldRepaint(_TrianglePainter oldDelegate) { 1374 | return true; 1375 | } 1376 | } 1377 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ffloat 2 | description: FFloat, although simple and easy to use, can satisfy all your imagination of the floating layer. 3 | version: 2.0.1 4 | author: CoorChice 5 | homepage: https://github.com/Fliggy-Mobile/ffloat 6 | 7 | environment: 8 | sdk: ">=2.12.0 <3.0.0" 9 | flutter: ">=1.10.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | # For information on the generic Dart part of this file, see the 20 | # following page: https://dart.dev/tools/pub/pubspec 21 | 22 | # The following section is specific to Flutter. 23 | flutter: 24 | # This section identifies this Flutter project as a plugin project. 25 | # The 'pluginClass' and Android 'package' identifiers should not ordinarily 26 | # be modified. They are used by the tooling to maintain consistency when 27 | # adding or updating assets for this project. 28 | # plugin: 29 | # platforms: 30 | # android: 31 | # package: fapi.taobao.ffloat 32 | # pluginClass: FfloatPlugin 33 | # ios: 34 | # pluginClass: FfloatPlugin 35 | # macos: 36 | # pluginClass: FfloatPlugin 37 | 38 | # To add assets to your plugin package, add an assets section, like this: 39 | # assets: 40 | # - images/a_dot_burr.jpeg 41 | # - images/a_dot_ham.jpeg 42 | # 43 | # For details regarding assets in packages, see 44 | # https://flutter.dev/assets-and-images/#from-packages 45 | # 46 | # An image asset can refer to one or more resolution-specific "variants", see 47 | # https://flutter.dev/assets-and-images/#resolution-aware. 48 | 49 | # To add custom fonts to your plugin package, add a fonts section here, 50 | # in this "flutter" section. Each entry in this list should have a 51 | # "family" key with the font family name, and a "fonts" key with a 52 | # list giving the asset and other descriptors for the font. For 53 | # example: 54 | # fonts: 55 | # - family: Schyler 56 | # fonts: 57 | # - asset: fonts/Schyler-Regular.ttf 58 | # - asset: fonts/Schyler-Italic.ttf 59 | # style: italic 60 | # - family: Trajan Pro 61 | # fonts: 62 | # - asset: fonts/TrajanPro.ttf 63 | # - asset: fonts/TrajanPro_Bold.ttf 64 | # weight: 700 65 | # 66 | # For details regarding fonts in packages, see 67 | # https://flutter.dev/custom-fonts/#from-packages 68 | -------------------------------------------------------------------------------- /test/ffloat_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:ffloat/ffloat.dart'; 4 | 5 | void main() { 6 | 7 | } 8 | --------------------------------------------------------------------------------