diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cc7d8..ad34ad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ -## 0.0.1 +## 1.0.0 -* TODO: Describe initial release. +- First version diff --git a/LICENSE b/LICENSE index ba75c69..19443ba 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,13 @@ -TODO: Add your license here. +Copyright 2020-present Fliggy Android Team . + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at following link. + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index cc90656..f0c1d7d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,476 @@ -# ffloat +

+ + + +

-A new Flutter plugin. +

ffloat

-## Getting Started -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. +
-For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +

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

+ +

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

+ +

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

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +

+

+ + + +

+ +**English | [简体中文](https://github.com/Fliggy-Mobile/ffloat/blob/master/README_CN.md)** + +> Like it? Please cast your **Star** 🥰 ! + +# ✨ Features + +- Supports **floating layer position** control based on anchor elements + +- Convenient floating layer **show / hide control** + +- Comes with elegant **interactive effects** + +- Flexible and beautiful **decorative triangle** + +- Support precise control of **round corners** + +- Wonderful **gradient** effect support + +- Simple but powerful **border** support + +- Support absolute position **Dual mode** + + +# 🛠 Guide + +## ⚙️ Parameters + + +### 🔩 Basic parameters + +|Param|Type|Necessary|Default|desc| +|---|---|:---:|---|---| +|builder|FloatBuilder|true|null|[FloatBuilder] returns the content component of [FFloat]. If only the content area is updated, proceed with `setter (() {})`| +|color|Color|false|`Color(0x7F000000)`|[FFloat] colors| +|gradient|Gradient|false|null|Gradient. Will overwrite color| +|child|Widget|false|null|Anchor element| +|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| +|alignment|FFloatAlignment|false|FFloatAlignment.topCenter|[FFloat] Based on the relative position of the [child] anchor element.| +|margin|EdgeInsets|false|EdgeInsets.zero|[FFloat] Determine the distance between anchor points based on relative| +|padding|EdgeInsets|false|null|[FFloat] Internal spacing| +|canTouchOutside|bool|false|false|Click [FFloat] to hide the area outside the range.| +|backgroundColor|Color|false|Colors.transparent|[FFloat] The color of the background area when floating| +|autoDismissDuration|Duration|false|null|Duration of automatic disappearance. If it is null, it will not disappear automatically| +|controller|FFloatController|false|null|[FFloatController] can control the show/hide of [FFloat]. See [FFloatController] for details| +|animDuration|Duration|false|`Duration(milliseconds: 100)`|Show/hide animation duration| + +### 🔺 Triangle + +|Param|Type|Necessary|Default|desc| +|---|---|:---:|---|---| +|triangleWidth|double|false|12|The width of the triangle| +|triangleHeight|double|false|6|The height of the triangle| +|triangleAlignment|TriangleAlignment|false|TriangleAlignment.center|Relative position of triangle| +|triangleOffset|Offset|false|Offset.zero|Triangle position offset| +|hideTriangle|bool|false|false|Whether to hide the decorative triangle| + +### 🔆 Corner & Border + +|Param|Type|Necessary|Default|desc| +|---|---|:---:|---|---| +|corner|FFloatCorner|false|null|corner| +|cornerStyle|FFloatCornerStyle|false|FFloatCornerStyle.round|corner style| +|strokeColor|Color|false|null|Stroke color| +|strokeWidth|double|false|null|Stroke width| + +### 🔳 Shadow parameters + +|Param|Type|Necessary|Default|desc| +|---|---|:---:|---|---| +|shadowColor|Color|false|null|Shadow color| +|shadowOffset|Offset|false|null|Shadow offset| +|shadowBlur|double|false|null|The larger the value, the greater the shadow| + + +## 📺 Demo + +### 🔩 Basic Demo + + +![](https://gw.alicdn.com/tfs/TB1GwD9FhD1gK0jSZFyXXciOVXa-464-140.gif) + +```dart +FFloat( + (_) => createContent(), + controller: controller1, + padding: EdgeInsets.only(left: 9, right: 9, top: 6, bottom: 6), + corner: FFloatCorner.all(10), + alignment: floatAlignment1, + canTouchOutside: false, + child: buildChild1(), +) +``` + +**FFloat** can wrap a normal component (that is, assign the normal component to the `child` parameter of **FFloat**), so that the component has the ability to click to pop up the floating layer. + +And **FFloat** will not have any adverse effect on the original components, which is amazing! + + +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. + +```dart +FFloatController controller = FFloatController(); + +FFloat( + controller: controller, +) + +/// show float +controller.show(); +/// hide float +controller.dismiss(); +``` + +**FFloat** is smart enough to automatically determine where it should appear based on the position of `child`. With `alignment` and` margin`, you can adjust the position of the floating layer in an incredibly simple way until you think it is ok. + +This is an unprecedented change 👍! + +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. + +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 👿. + +As for the content of the floating layer, just pass the `buidler` parameter and return a **Widget** in the **FloatBuilder** function. + +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. + +```dart +FFloat( + (setter){ + return GestureDetector( + onTap:(){ + setter((){ + /// update something + }); + } + child: buildWidgte()); + }, + child: buildChild() +) +``` + + +### 💫 Background & Animation + +![](https://gw.alicdn.com/tfs/TB1suD8FkL0gK0jSZFAXXcA9pXa-753-143.gif) + +```dart +FFloat( + (_) => FSuper( + text: "Surprise😃 !", + textColor: Colors.white, + ), + controller: controller2_1, + color: Color(0xff5D5D5E), + corner: FFloatCorner.all(6), + margin: EdgeInsets.only(bottom: 10), + padding: EdgeInsets.only(left: 9, right: 9, top: 3, bottom: 3), + child: buildChild(), + canTouchOutside: false, + autoDismissDuration: Duration(milliseconds: 2000), +), + +FFloat( + (_) => buildSearch(), + child2Alignment: Alignment.centerLeft, + child2Margin: EdgeInsets.only(left: (9.0 + 18.0 + 9.0)), + ), + controller: controller2_2, + color: Colors.black.withOpacity(0.95), + backgroundColor: Colors.black26, + corner: FFloatCorner.all(20), + margin: EdgeInsets.only(bottom: 10, left: 10), + child: buildChild(), + alignment: FFloatAlignment.topRight, + triangleAlignment: TriangleAlignment.end, + triangleOffset: Offset(-39, 0), +) +``` + +When the floating layer of **FFloat** appears, you can choose whether you want a background color, by configuring `backgroundColor`. + +And through the `canTouchOutside` property, you set whether your floating layer can be closed by clicking the area outside the floating layer. + +When set to `canTouchOutside = false`, it often means that you need to control the hiding of the floating layer through a **FFloatController**. + +By default, **FFloat** comes with the **Scale** show/hide animation. + +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. + +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. + +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. + + +### 🔺 Decorative triangle + +![](https://gw.alicdn.com/tfs/TB1L3JoFEH1gK0jSZSyXXXtlpXa-753-220.gif) + +```dart +FFloat( + (setter) => buildContent(), + shadowColor: Colors.black38, + shadowBlur: 8.0, + shadowOffset: Offset(2.0, 2.0), + color: Colors.white, + corner: FFloatCorner.all(3), + controller: controller3_1, + alignment: FFloatAlignment.bottomLeft, + hideTriangle: true, + child: buildChild(), +), + +FFloat( + (setter) => buildContent(), + controller: controller3_2, + alignment: FFloatAlignment.bottomLeft, + margin: EdgeInsets.only(top: 2), + shadowColor: Colors.black38, + shadowBlur: 8.0, + shadowOffset: Offset(2.0, 2.0), + corner: FFloatCorner.all(3), + color: Colors.white, + triangleAlignment: TriangleAlignment.start, + triangleOffset: Offset(10, 10), + triangleWidth: 20, + triangleHeight: 15, + child: buildChild(), +), +``` + +**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. + +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. + +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. + +`triangleAlignment` provides an easy way to adjust the position of the decorative triangle. + +If you are still not satisfied, `triangleOffset` will allow you to further offset based on the relative position. For **FFloat**, nothing is impossible. + +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` + +### 🔆 Corner & Stroke + +![](https://gw.alicdn.com/tfs/TB1xbhyFqL7gK0jSZFBXXXZZpXa-670-241.gif) + +```dart + +FFloat( + (setter) { + return buildContent(); + }, + child: buildChild(), + controller: controller4, + color: Colors.white, + corner: FFloatCorner.all(6), + strokeColor: mainShadowColor, + strokeWidth: 1.0, + alignment: FFloatAlignment.bottomLeft, + hideTriangle: true, + margin: EdgeInsets.only(top: 9), + padding: EdgeInsets.only(top: 9, bottom: 9), +) +``` + +As you can see, a beautiful rounded floating layer with a border is so simple to build. + +**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. + +The `cornerStyle` property that comes with` corner` allows you to switch the style of rounded corners (rounded corners or beveled corners) at any time. + +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`. + +Our original intention was to help developers build beautiful applications more quickly. + + +### 🔳 Gradient & Shadow + +![](https://gw.alicdn.com/tfs/TB11GHPFrj1gK0jSZFuXXcrHpXa-414-179.gif) + +```dart + +FFloat( + (setter) => buildContent(), + child: buildChild(), + controller: controller5, + gradient: SweepGradient( + colors: [ + Color(0xffE271C0), + Color(0xffC671EB), + Color(0xff7673F3), + Color(0xff8BEBEF), + Color(0xff93FCA8), + Color(0xff94FC9D), + Color(0xffEDF980), + Color(0xffF0C479), + Color(0xffE07E77), + ], + ), + corner: FFloatCorner.all(100), + hideTriangle: true, + margin: EdgeInsets.only(top: 9), + alignment: FFloatAlignment.bottomCenter, + shadowColor: Colors.black38, + shadowBlur: 3, + shadowOffset: Offset(3, 2), +) +``` + +Yes, **FFloat** still has support for gradients after combining many capabilities. + +Of course, just through a simple `gradient` attribute, you can get a beautiful gradient effect. + +In addition, as a modern component, **FFloat** will of course support shadows. + +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. + + +### 👏 More exciting + +![](https://gw.alicdn.com/tfs/TB1NGfIFAL0gK0jSZFtXXXQCXXa-460-500.gif) + +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. + +**FFloat** has handled it for you well enough! + +Let **FFloat** solve all your floating layer problems, you only need to beautify your application. + + +### 📌 Absolute position Float + +In some cases, our floating layer does not need to appear based on an anchor element, but hopes that it appears in a certain position. + +If you know such a location, use `location` to configure the location of your **FFloat** floating layer. + +At this point, you don't need to put **FFloat** in the view tree at all, which means you can create a floating layer in any callback or function anytime, anywhere. + +```dart +FSuper( + width: 20, + height: 20, + backgroundColor: Colors.redAccent, + onClick: () { + FFloat( + (_) => Container( + width: 50, + height: 50, + color: Colors.blue, + ), + location: Offset(50, 50), + ).show(context); + }, +) +``` + +The above code will create a floating layer at the coordinate position of the screen (50, 50) when the click event `onClick` is triggered. + +You see that through `FFloat.show (context)` and `FFloat.dismiss ()`, you can easily control the show/hide of the floating layer at any time. + +All configurations of **FFloat** are exactly the same as those described above. + + +# 😃 How to use? + +Add dependencies in the project `pubspec.yaml` file: + +## 🌐 pub dependency + +``` +dependencies: + ffloat: ^ +``` + +> ⚠️ Attention,please go to [**pub**] (https://pub.dev/packages/ffloat) to get the latest version number of **FFloat** + +## 🖥 git dependencies + +``` +dependencies: + ffloat: + git: + url: 'git@github.com:Fliggy-Mobile/ffloat.git' + ref: ''' +``` + +> ⚠️ Attention,please refer to [**FFloat**] (https://github.com/Fliggy-Mobile/ffloat) official project for branch number or tag. + + +# 💡 License + +``` +Copyright 2020-present Fliggy Android Team . + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at following link. + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + + +### Like it? Please cast your [**Star**](https://github.com/Fliggy-Mobile/ffloat) 🥰 ! + + + +-- + +# How to run Demo project? + +1. **clone** project to local + +2. Enter the project `example` directory and run the following command + +``` +flutter create . +``` + +3. Run the demo in `example` diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..656c5f2 --- /dev/null +++ b/README_CN.md @@ -0,0 +1,475 @@ +

+ + + +

+ +

ffloat

+ + +
+ +

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

+ +

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

+ +

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

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +

+

+ + + +

+ +**[English](https://github.com/Fliggy-Mobile/ffloat) | 简体中文** + +> 感觉还不错?请投出您的 **Star** 吧 🥰 ! + +# ✨ 特性 + +- 支持基于锚点元素的**浮层位置**控制 + +- 便捷的浮层**显示/隐藏控制** + +- 自带优雅的**交互效果** + +- 灵活美观的**装饰三角** + +- 支持对**圆角**精准控制 + +- 美妙的**渐变**效果支持 + +- 简单但强大的**边框**支持 + +- 支持绝对位置**双模式** + + +# 🛠 使用指南 + + +## ⚙️ 参数 + +### 🔩 基础参数 + +|参数|类型|必要|默认值|说明| +|---|---|:---:|---|---| +|builder|FloatBuilder|是|null|通过 [FloatBuilder] 返回 [FFloat] 的内容组件。如果只更新内容区域的话,通过 `setter((){})` 进行| +|color|Color|否|`Color(0x7F000000)`|[FFloat] 的颜色| +|gradient|Gradient|否|null|渐变色。会覆盖 color| +|child|Widget|否|null|锚点组件| +|location|Offset|否|null|位置。通过 [location] 指定 [FFloat] 的位置后,基于锚点确定位置的所有配置将失效| +|alignment|FFloatAlignment|否|FFloatAlignment.topCenter|[FFloat] 基于 [child] 锚点元素的相对位置。| +|margin|EdgeInsets|否|EdgeInsets.zero|[FFloat] 基于相对确定锚定点的间距| +|padding|EdgeInsets|否|null|[FFloat] 内部间距| +|canTouchOutside|bool|否|false|点击 [FFloat] 范围外区域是否隐藏。| +|backgroundColor|Color|否|Colors.transparent|[FFloat] 浮出时,背景区域的颜色| +|autoDismissDuration|Duration|否|null|自动消失时长。如果为 null,就不会自动消失| +|controller|FFloatController|否|null|通过 [FFloatController] 可以控制 [FFloat] 的显示/隐藏。详见 [FFloatController]| +|animDuration|Duration|否|`Duration(milliseconds: 100)`|显示/隐藏动效时长| + +### 🔺 三角 + +|参数|类型|必要|默认值|说明| +|---|---|:---:|---|---| +|triangleWidth|double|否|12|三角的宽| +|triangleHeight|double|否|6|三角的高| +|triangleAlignment|TriangleAlignment|否|TriangleAlignment.center|三角的相对位置| +|triangleOffset|Offset|否|Offset.zero|三角的位置偏移| +|hideTriangle|bool|否|false|是否隐藏装饰三角| + +### 🔆 圆角 & 边框 + +|参数|类型|必要|默认值|说明| +|---|---|:---:|---|---| +|corner|FFloatCorner|否|null|圆角| +|cornerStyle|FFloatCornerStyle|否|FFloatCornerStyle.round|圆角样式| +|strokeColor|Color|否|null|描边颜色| +|strokeWidth|double|否|null|描边宽度| + +### 🔳 阴影参数 + +|参数|类型|必要|默认值|说明| +|---|---|:---:|---|---| +|shadowColor|Color|否|null|阴影颜色| +|shadowOffset|Offset|否|null|阴影偏移量| +|shadowBlur|double|否|null|值越大,阴影越大| + + +## 📺 使用示例 + +### 🔩 基本使用 + + +![](https://gw.alicdn.com/tfs/TB1GwD9FhD1gK0jSZFyXXciOVXa-464-140.gif) + +```dart +FFloat( + (_) => createContent(), + controller: controller1, + padding: EdgeInsets.only(left: 9, right: 9, top: 6, bottom: 6), + corner: FFloatCorner.all(10), + alignment: floatAlignment1, + canTouchOutside: false, + child: buildChild1(), +) +``` + +**FFloat** 能够去包裹一个正常的组件(即将正常组件赋值给 **FFloat** 的 `child` 参数),使得该组件具备点击弹出浮层的能力。 + +而且 **FFloat** 不会对原本的组件产生任何的不利影响,这很神奇吧! + +或者,你也可以通过 **FFloatController** 来控制浮层的展示。当然,前提是你需要创建一个 **FFloatController** ,然后把它赋值给 **FFloat** 的 `controller` 属性。 + +```dart +FFloatController controller = FFloatController(); + +FFloat( + controller: controller, +) + +/// 显示浮层 +controller.show(); +/// 隐藏浮层 +controller.dismiss(); +``` + +**FFloat** 足够的聪明,它能够根据 `child` 的位置自动的确定自己应该出现在什么地方。通过 `alignment` 和 `margin`,你能够以难以置信的简单的方式调整浮层的位置,直到你认为这可以了。 + +这是一种前所未有的改变 👍! + +在过去,如果你想要基于一个元素的位置展示一个浮层,那你不得不经过一系列繁琐的操作,以获得元素的位置。然后再进行坐标转换,根据浮层的尺寸进行位置的计算。 + +天呐,光是想想就已经够复杂的了。更别提当遇到需要居中、居左对齐的场景了,那简直是噩梦 👿。 + +至于浮层的内容,只需要通过 `buidler` 参数,在 **FloatBuilder** 函数中返回一个 **Widget** 就可以了。 + +如果你的浮层内容需要刷新, **FloatBuilder** 函数提供了一个 **StateSetter** 参数,通过它可以只针对浮层中的内容刷新,而不影响浮层外的内容。真是高效呀。 + +```dart +FFloat( + (setter){ + return GestureDetector( + onTap:(){ + setter((){ + /// update something + }); + } + child: buildWidgte()); + }, + child: buildChild() +) +``` + + +### 💫 背景 & 动画 + +![](https://gw.alicdn.com/tfs/TB1suD8FkL0gK0jSZFAXXcA9pXa-753-143.gif) + +```dart +FFloat( + (_) => FSuper( + text: "Surprise😃 !", + textColor: Colors.white, + ), + controller: controller2_1, + color: Color(0xff5D5D5E), + corner: FFloatCorner.all(6), + margin: EdgeInsets.only(bottom: 10), + padding: EdgeInsets.only(left: 9, right: 9, top: 3, bottom: 3), + child: buildChild(), + canTouchOutside: false, + autoDismissDuration: Duration(milliseconds: 2000), +), + +FFloat( + (_) => buildSearch(), + child2Alignment: Alignment.centerLeft, + child2Margin: EdgeInsets.only(left: (9.0 + 18.0 + 9.0)), + ), + controller: controller2_2, + color: Colors.black.withOpacity(0.95), + backgroundColor: Colors.black26, + corner: FFloatCorner.all(20), + margin: EdgeInsets.only(bottom: 10, left: 10), + child: buildChild(), + alignment: FFloatAlignment.topRight, + triangleAlignment: TriangleAlignment.end, + triangleOffset: Offset(-39, 0), +) +``` + +当 **FFloat** 的浮层出现时,你可以选择是否需要背景色,通过配置 `backgroundColor`。 + +而通过 `canTouchOutside` 属性,你设置你的浮层是否可以通过点击浮层外区域关闭。 + +当设置为 `canTouchOutside = false` 时,往往就意味着你需要通过一个 **FFloatController** 来控制浮层的隐藏。 + +默认情况下,**FFloat** 附赠了一个 **缩放** 的显示/隐藏动画。 + +依据你设置的浮层方位,**FFloat** 能够智能的判断动画的起始锚点。这让一切都更加自然。 + +如果你不需要动画效果的话,只需要通过 `animDuration` 参数传入 **null**,就可以取消动画效果,回归比较突兀的显示/隐藏了。 + +在你配置了 `autoDismissDuration` 参数的情况下,**FFloat** 会进入自动消失模式。这意味着,在浮层弹出后,将在你期望的时间点自动消失。你无需过多干预。 + + +### 🔺 装饰三角 + +![](https://gw.alicdn.com/tfs/TB1L3JoFEH1gK0jSZSyXXXtlpXa-753-220.gif) + +```dart +FFloat( + (setter) => buildContent(), + shadowColor: Colors.black38, + shadowBlur: 8.0, + shadowOffset: Offset(2.0, 2.0), + color: Colors.white, + corner: FFloatCorner.all(3), + controller: controller3_1, + alignment: FFloatAlignment.bottomLeft, + hideTriangle: true, + child: buildChild(), +), + +FFloat( + (setter) => buildContent(), + controller: controller3_2, + alignment: FFloatAlignment.bottomLeft, + margin: EdgeInsets.only(top: 2), + shadowColor: Colors.black38, + shadowBlur: 8.0, + shadowOffset: Offset(2.0, 2.0), + corner: FFloatCorner.all(3), + color: Colors.white, + triangleAlignment: TriangleAlignment.start, + triangleOffset: Offset(10, 10), + triangleWidth: 20, + triangleHeight: 15, + child: buildChild(), +), +``` + +**FFloat** 贴心的内置了装饰三角。默认情况下你就能看到它。如果你不希望得到一个有装饰三角的浮层,可以通过 `hideTriangle` 属性来控制它的显示/隐藏。 + +再次说明,**FFloat** 真的很聪明,装饰三角能够自动根据浮层与锚点元素的相对位置判断自己应该出现在什么位置,看起来才足够精致。 + +装饰三角和浮层的主题样式是浑然天成的,在色彩方面,**FFloat** 完全省去了你的烦恼。无论是纯色、边框、渐变色,一切都无需额外的处理。 + +`triangleAlignment` 提供了简易的方式来调整装饰三角的位置。 + +如果你还是不满意,`triangleOffset` 能够让你基于相对位置进一步偏移。对于 **FFloat** 而言,没有什么不可能的。 + +当然,**FFloat** 一定会为你准备装饰三角的尺寸控制的,就像 `triangleWidth` 和 `triangleHeight` 要解决的问题那样。 + +### 🔆 圆角 & 边框 + +![](https://gw.alicdn.com/tfs/TB1xbhyFqL7gK0jSZFBXXXZZpXa-670-241.gif) + +```dart + +FFloat( + (setter) { + return buildContent(); + }, + child: buildChild(), + controller: controller4, + color: Colors.white, + corner: FFloatCorner.all(6), + strokeColor: mainShadowColor, + strokeWidth: 1.0, + alignment: FFloatAlignment.bottomLeft, + hideTriangle: true, + margin: EdgeInsets.only(top: 9), + padding: EdgeInsets.only(top: 9, bottom: 9), +) +``` + +如你所见,一个漂亮的带边框的圆角浮层,构建起来是如此的简单。 + +**FFloat** 提供了 **FWidget** 系列组件一脉相承的,简单的设置圆角的方式,仅仅通过一个简单的 `corner` 属性就能灵活的配置圆角。 + +与 `corner` 配套的 `cornerStyle` 属性,允许你随时切换圆角的风格(圆角 or 斜切角)。 + +如果你已经是 **FWidget** 的用户,相信你已经知道了,我们为组件配置边框效果,仅仅需要通过 `strokeWidth` 和 `strokeColor` 这样两个简单的属性即可。 + +我们的初心,始终是想帮助开发者能够更加快捷的构建出精美的应用。 + + +### 🔳 渐变 & 阴影 + +![](https://gw.alicdn.com/tfs/TB11GHPFrj1gK0jSZFuXXcrHpXa-414-179.gif) + +```dart + +FFloat( + (setter) => buildContent(), + child: buildChild(), + controller: controller5, + gradient: SweepGradient( + colors: [ + Color(0xffE271C0), + Color(0xffC671EB), + Color(0xff7673F3), + Color(0xff8BEBEF), + Color(0xff93FCA8), + Color(0xff94FC9D), + Color(0xffEDF980), + Color(0xffF0C479), + Color(0xffE07E77), + ], + ), + corner: FFloatCorner.all(100), + hideTriangle: true, + margin: EdgeInsets.only(top: 9), + alignment: FFloatAlignment.bottomCenter, + shadowColor: Colors.black38, + shadowBlur: 3, + shadowOffset: Offset(3, 2), +) +``` + +是的,**FFloat** 在兼具了诸多能力之后,仍然对渐变进行了支持。 + +当然,仅仅是通过一个简单的 `gradient` 属性,你就能获得漂亮的渐变效果。 + +此外,作为一个现代化的组件,**FFloat** 当然会对阴影作出支持。 + +你只需要通过 `shadowColor` 属性就能获得一个基础的阴影效果,如果你想要进一步对阴影作出调整的,可以使用 `shadowBlur` 和 `shadowOffset` 属性。 + + +### 👏 更多的精彩 + +![](https://gw.alicdn.com/tfs/TB1NGfIFAL0gK0jSZFtXXXQCXXa-460-500.gif) + +在 **FFloat** 中,浮层能够自动的跟随锚点元素移动,你无需关注因位置变化而产生的一些列计算。 + +**FFloat** 都帮你处理的足够好了! + +让 **FFloat** 来解决你的一切浮层问题吧,你只需要用心美化你的应用就够了。 + + +### 📌 绝对位置浮层 + +在一些情况下,我们的浮层不需要基于一个锚点元素出现,而是希望它出现在一个确定的位置。 + +如果你知道一个这样的位置的话,就用 `location` 来配置你的 **FFloat** 浮层的位置吧。 + +此时,你完全不需要将 **FFloat** 放到视图树中,这意味着你可以随时随地,在任何回调或者函数中创建一个浮层。 + +```dart +FSuper( + width: 20, + height: 20, + backgroundColor: Colors.redAccent, + onClick: () { + FFloat( + (_) => Container( + width: 50, + height: 50, + color: Colors.blue, + ), + location: Offset(50, 50), + ).show(context); + }, +) +``` + +以上代码,将会在点击事件 `onClick` 触发时,在屏幕的 `(50, 50)` 坐标位置创建一个浮层。 + +你看通过 `FFloat.show(context)` 和 `FFloat.dismiss()`,你可以随时轻松的控制浮层的显示/隐藏。 + +而 **FFloat** 的一切配置,和前面所介绍的都完全相同。 + + +# 😃 如何使用? + +在项目 `pubspec.yaml` 文件中添加依赖: + +## 🌐 pub 依赖方式 + +``` +dependencies: + ffloat: ^<版本号> +``` + +> ⚠️ 注意,请到 [**pub**](https://pub.dev/packages/ffloat) 获取 **ffloat** 最新版本号 + +## 🖥 git 依赖方式 + +``` +dependencies: + ffloat: + git: + url: 'git@github.com:Fliggy-Mobile/ffloat.git' + ref: '<分支号 或 tag>' +``` + + +> ⚠️ 注意,分支号 或 tag 请以 [**ffloat**](https://github.com/Fliggy-Mobile/ffloat) 官方项目为准。 + + +# 💡 License + +``` +Copyright 2020-present Fliggy Android Team . + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at following link. + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +``` + + +### 感觉还不错?请投出您的 [**Star**](https://github.com/Fliggy-Mobile/ffloat) 吧 🥰 ! + + +-- + +# 如何运行 Demo 工程? + +1. **clone** 工程到本地 + +2. 进入工程 `example` 目录,运行以下命令 + +``` +flutter create . +``` + +3. 运行 `example` 中的 Demo diff --git a/example/assets/airpos.png b/example/assets/airpos.png new file mode 100644 index 0000000..b9d4d5b Binary files /dev/null and b/example/assets/airpos.png differ diff --git a/example/assets/appletv.png b/example/assets/appletv.png new file mode 100644 index 0000000..27b18d5 Binary files /dev/null and b/example/assets/appletv.png differ diff --git a/example/assets/icon_apple.png b/example/assets/icon_apple.png new file mode 100644 index 0000000..81b8874 Binary files /dev/null and b/example/assets/icon_apple.png differ diff --git a/example/assets/icon_apple2.png b/example/assets/icon_apple2.png new file mode 100644 index 0000000..259a6be Binary files /dev/null and b/example/assets/icon_apple2.png differ diff --git a/example/assets/icon_color.png b/example/assets/icon_color.png new file mode 100644 index 0000000..82501af Binary files /dev/null and b/example/assets/icon_color.png differ diff --git a/example/assets/iphone.png b/example/assets/iphone.png new file mode 100644 index 0000000..ed607c7 Binary files /dev/null and b/example/assets/iphone.png differ diff --git a/example/assets/switch.png b/example/assets/switch.png new file mode 100644 index 0000000..12edbf5 Binary files /dev/null and b/example/assets/switch.png differ diff --git a/example/assets/watch.png b/example/assets/watch.png new file mode 100644 index 0000000..2380790 Binary files /dev/null and b/example/assets/watch.png differ diff --git a/example/lib/color.dart b/example/lib/color.dart new file mode 100644 index 0000000..94a839e --- /dev/null +++ b/example/lib/color.dart @@ -0,0 +1,8 @@ + +import 'package:flutter/widgets.dart'; + +const Color mainBackgroundColor = Color(0xfff1f3f6); +const Color mainTextTitleColor = Color(0xff366471); +const Color mainTextNormalColor = Color(0xff3e6a77); +const Color mainTextSubColor = Color(0xff6c909b); +const Color mainShadowColor = Color(0x4d3754AA); diff --git a/example/lib/main.dart b/example/lib/main.dart index 753f0a4..0b90d88 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,12 @@ import 'dart:async'; +import 'dart:ffi'; +import 'package:fbutton/fbutton.dart'; +import 'package:ffloat_example/color.dart'; import 'package:flutter/material.dart'; import 'package:ffloat/ffloat.dart'; +import 'package:fradio/fradio.dart'; +import 'package:fsuper/fsuper.dart'; void main() => runApp(MyApp()); @@ -14,162 +19,310 @@ class _MyAppState extends State { @override void initState() { super.initState(); - print('PageA - initState'); } @override - void didChangeDependencies() { - super.didChangeDependencies(); - print('PageA - didChangeDependencies'); + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: FFloatPage(), + ); } +} +class FFloatPage extends StatefulWidget { @override - void didUpdateWidget(MyApp oldWidget) { - super.didUpdateWidget(oldWidget); - print('PageA - didUpdateWidget'); + State createState() { + return _FFloatPage(); } +} - @override - void deactivate() { - super.deactivate(); - print('PageA - deactivate'); - } +class _FFloatPage extends State { + /// controllers + FFloatController controller1 = FFloatController(); + FFloatController controller2_1 = FFloatController(); + FFloatController controller2_2 = FFloatController(); + FFloatController controller2_3 = FFloatController(); + FFloatController controller3_1 = FFloatController(); + FFloatController controller3_2 = FFloatController(); + FFloatController controller3_3 = FFloatController(); + FFloatController controller4 = FFloatController(); + FFloatController controller5 = FFloatController(); - @override - void dispose() { - super.dispose(); - print('PageA - dispose'); - } + String text_1 = "Click, Now!"; + FFloatAlignment floatAlignment1 = FFloatAlignment.topCenter; - @override - Widget build(BuildContext context) { - return MaterialApp( - home: PageC(), - ); - } -} + List fileMenuList = []; + List clickList = []; + int group_menu_value1 = -1; + int group_menu_value2 = -1; + int group_menu_value3 = -1; + + int group_toolbar_value = -1; + + int group_corner_value = -1; -class PageB extends StatefulWidget { @override - _PageBState createState() => _PageBState(); -} + void initState() { + controller1.setStateChangedListener(() { + if (!controller1.isShow) { + setState(() { + floatAlignment1 = randomFloatAlignment(floatAlignment1); + }); + } else {} + }); + fileMenuList.add("New Finder Window ⌘N"); + fileMenuList.add("New Smart Folder "); + fileMenuList.add("New Tab ⌘T"); + fileMenuList.add("Open in New Tab ^⌘O"); + + /// clickList + clickList.add("Android"); + clickList.add("iOS"); + clickList.add("Flutter"); + clickList.add("Fuchsia"); + super.initState(); + } -class _PageBState extends State { @override Widget build(BuildContext context) { - print('PageB - build'); return Scaffold( + backgroundColor: mainBackgroundColor, appBar: AppBar( - title: Text('Page B'), - ), - body: Container( - child: Center( - child: RaisedButton( - onPressed: () { - Navigator.push(context, MaterialPageRoute( - builder: (context) { - return PageC(); - }, - )); - }, - child: Text("Open"), - ), + backgroundColor: mainBackgroundColor, + title: const Text( + 'FFloat', + style: TextStyle(color: mainTextTitleColor), ), + centerTitle: true, ), - ); - } - - @override - void initState() { - super.initState(); - print('PageB - initState'); - } + body: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + buildTitle("Base Demo"), + buildMiddleMargin(), - @override - void didChangeDependencies() { - super.didChangeDependencies(); - print('PageB - didChangeDependencies'); - } + /// baseDemo + baseDemo(), + buildMiddleMargin(), + buildTitle("Background & Animation"), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), - @override - void didUpdateWidget(PageB oldWidget) { - super.didUpdateWidget(oldWidget); - print('PageB - didUpdateWidget'); - } + /// backgroundDemo + backgroundDemo(), + buildMiddleMargin(), + buildTitle("Triangle"), + buildMiddleMargin(), + buildMiddleMargin(), - @override - void deactivate() { - super.deactivate(); - print( - 'PageB - deactivate,isCurrent = ${ModalRoute.of(context).isCurrent},isActive = ${ModalRoute.of(context).isActive}'); - } + /// Triangle + triangleDemo(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildTitle("Corner & Stroke"), + buildMiddleMargin(), - @override - void dispose() { - super.dispose(); - print('PageB - dispose'); - } -} + /// cornerDemo + cornerDemo(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildTitle("Gradient & Shadow"), + buildMiddleMargin(), -class PageC extends StatefulWidget { - @override - _PageCState createState() => _PageCState(); -} + /// gradientDemo + gradientDemo(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildTitle("More Surprise"), + buildMiddleMargin(), + buildMiddleMargin(), -class _PageCState extends State { - FFloatController controller = FFloatController(); + /// moreDemo + moreDemo(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + buildMiddleMargin(), + ], + ), + ), + ); + } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Page C'), + Container moreDemo() { + return Container( + width: double.infinity, + height: 250, + margin: EdgeInsets.only(left: 20, right: 20), + padding: EdgeInsets.only(top: 20, bottom: 20), + decoration: BoxDecoration( + color: Color(0xffe2f1fa), + borderRadius: BorderRadius.all(Radius.circular(6)), + boxShadow: [ + BoxShadow( + color: mainShadowColor, blurRadius: 5, offset: Offset(5, 5)), + ], ), - body: Stack( + alignment: Alignment.center, + child: Stack( children: [ - Container( - width: 100, - height: 100, - color: Colors.redAccent, + Positioned( + left: 300, + child: FFloat( + (_) { + return Text( + "Airpos", + style: TextStyle(color: Colors.white), + ); + }, + padding: + EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 15), + margin: EdgeInsets.only(bottom: 3), + corner: FFloatCorner( + leftTopCorner: 50, + rightTopCorner: 50, + leftBottomCorner: 35, + rightBottomCorner: 35), + triangleWidth: 30, + triangleHeight: 10, + child: Image.asset( + "assets/airpos.png", + width: 60, + ), + canTouchOutside: false, + ), + ), + Positioned( + left: 100, + top: 10, + child: FFloat( + (_) { + return Text( + "Apple Tv", + style: TextStyle(color: Colors.white), + ); + }, + padding: + EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 15), + margin: EdgeInsets.only(bottom: 3), + corner: FFloatCorner( + leftTopCorner: 80, + rightTopCorner: 80, + leftBottomCorner: 35, + rightBottomCorner: 35), + triangleWidth: 55, + triangleHeight: 10, + child: Image.asset( + "assets/appletv.png", + width: 60, + ), + canTouchOutside: false, + ), ), - Container( -// margin: EdgeInsets.only(right: 100, top: 100), + Positioned( + left: 80, + top: 90, child: FFloat( -// Text( -// "这是一条提示", -// style: TextStyle( -// decoration: TextDecoration.none, -// fontSize: 12, -// color: Colors.white), -// ), - Column( - children: [ - Text("data1"), - const SizedBox(height: 16), - Text("data2"), - ], + (_) { + return Text( + "iPhone", + style: TextStyle(color: Colors.white), + ); + }, + padding: + EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), + margin: EdgeInsets.only(bottom: 10, right: 3), + corner: FFloatCorner( + leftTopCorner: 50, + rightTopCorner: 25, + leftBottomCorner: 50, + rightBottomCorner: 25), + triangleWidth: 23, + triangleHeight: 10, + alignment: FFloatAlignment.leftTop, + child: Image.asset( + "assets/iphone.png", + width: 80, ), + canTouchOutside: false, + ), + ), + Positioned( + left: 200, + top: 10, + child: FFloat( + (_) { + return Text( + "Switch", + style: TextStyle(color: Colors.white), + ); + }, padding: - EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 12), - alignment: FFloatAlignment.topRight, - triangleAlignment: TriangleAlignment.center, - triangleWidth: 30, - triangleHeight: 20, - hideTriangle: true, -// canTouchOutside: true, -// autoDismissDuration: Duration(milliseconds: 2000), -// triangleOffset: Offset(10, 0), + EdgeInsets.only(left: 10, right: 10, top: 20, bottom: 15), + margin: EdgeInsets.only(bottom: 3), corner: FFloatCorner( - leftBottomCorner: 12, - rightBottomCorner: 12, - rightTopCorner: 12, + leftTopCorner: 80, + rightTopCorner: 80, + leftBottomCorner: 40, + rightBottomCorner: 40), + triangleWidth: 41, + triangleHeight: 10, + child: Image.asset( + "assets/switch.png", + width: 60, ), -// margin: EdgeInsets.only(right: 10, top: 10), - strokeColor: Colors.black87, -// strokeWidth: 1, - location: Offset(100, 100), - controller: controller, - child: Text("OOO"), + canTouchOutside: false, + ), + ), + Positioned( + left: 250, + top: 100, + child: FFloat( + (_) { + return Text( + "Watch", + style: TextStyle(color: Colors.white), + ); + }, + padding: + EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), + margin: EdgeInsets.only(bottom: 3), + corner: FFloatCorner( + leftTopCorner: 50, + rightTopCorner: 80, + leftBottomCorner: 50, + rightBottomCorner: 80), + alignment: FFloatAlignment.rightCenter, + triangleWidth: 19, + triangleHeight: 10, + child: Image.asset( + "assets/watch.png", + width: 60, + ), + canTouchOutside: false, ), ), ], @@ -177,40 +330,675 @@ class _PageCState extends State { ); } - @override - void initState() { - super.initState(); - print('PageC - initState'); - Timer(Duration(milliseconds: 1000), () { - controller.show(); - }); + FFloat gradientDemo() { + return FFloat( + (setter) { + return Container( + width: 100, + height: 100, + ); + }, + child: FButton( + width: 72, + height: 30, + corner: FButtonCorner.all(3), + padding: EdgeInsets.all(0), + color: Color(0xff373737), + effect: true, + image: Image.asset("assets/icon_color.png", width: 18), + onPressed: () { + controller5.show(); + }, + hoverColor: Colors.white60.withOpacity(0.3), + ), + controller: controller5, + gradient: SweepGradient( + colors: [ + Color(0xffE271C0), + Color(0xffC671EB), + Color(0xff7673F3), + Color(0xff8BEBEF), + Color(0xff93FCA8), + Color(0xff94FC9D), + Color(0xffEDF980), + Color(0xffF0C479), + Color(0xffE07E77), + ], + ), + corner: FFloatCorner.all(100), + hideTriangle: true, + margin: EdgeInsets.only(top: 9), + alignment: FFloatAlignment.bottomCenter, + shadowColor: Colors.black38, + shadowBlur: 3, + shadowOffset: Offset(3, 2), + ); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - print('PageC - didChangeDependencies'); + FFloat cornerDemo() { + return FFloat( + (setter) { + return SizedBox( + width: 150, + height: 100, + child: ListView.builder( + shrinkWrap: true, + itemCount: clickList.length, + itemBuilder: (context, index) { + return FRadio.custom( + value: index, + groupValue: group_corner_value, + onChanged: (value) { + setter(() { + group_corner_value = value; + }); + }, + normal: FSuper( + width: double.infinity, + textAlign: TextAlign.left, + text: clickList[index], + padding: + EdgeInsets.only(left: 20, right: 10, top: 3, bottom: 3), + ), + selected: FSuper( + width: double.infinity, + text: clickList[index], + textAlign: TextAlign.left, + textColor: Colors.white, + backgroundColor: Color(0xff008FFF), + padding: + EdgeInsets.only(left: 20, right: 10, top: 3, bottom: 3), + ), + hover: FSuper( + width: double.infinity, + text: clickList[index], + textAlign: TextAlign.left, + backgroundColor: Colors.black38.withOpacity(0.1), + padding: + EdgeInsets.only(left: 20, right: 10, top: 3, bottom: 3), + ), + ); + }), + ); + }, + child: FButton( + width: 80, + height: 40, + onPressed: () { + controller4.show(); + }, + text: "Click", + textColor: mainTextNormalColor, + color: Colors.white, + effect: true, + padding: EdgeInsets.zero, + corner: FButtonCorner.all(6), + shadowColor: mainShadowColor, + shadowOffset: Offset(1, 1), + shadowBlur: 5.0, + image: Icon( + Icons.arrow_drop_down, + size: 20, + color: mainTextNormalColor, + ), + imageAlignment: ImageAlignment.right, + ), + controller: controller4, + color: Colors.white, + corner: FFloatCorner.all(6), + strokeColor: mainShadowColor, + strokeWidth: 1.0, + alignment: FFloatAlignment.bottomLeft, + hideTriangle: true, + margin: EdgeInsets.only(top: 9), + padding: EdgeInsets.only(top: 9, bottom: 9), + ); } - @override - void didUpdateWidget(PageC oldWidget) { - super.didUpdateWidget(oldWidget); - print('PageC - didUpdateWidget'); - Timer(Duration(milliseconds: 1000), () { - controller.show(); - }); + Container triangleDemo() { + return Container( + width: double.infinity, + height: 22, + decoration: BoxDecoration(color: Color(0xff5D5D5E), boxShadow: [ + BoxShadow( + color: Colors.black38, + offset: Offset(0, 8), + blurRadius: 8.0, + ) + ]), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Image.asset("assets/icon_apple2.png", width: 13.5, height: 16), + const SizedBox(width: 16), + FFloat( + (setter) => Container( + width: 230, + height: 121, + padding: EdgeInsets.only(top: 6, bottom: 6), + child: ListView.builder( + shrinkWrap: true, + itemCount: fileMenuList.length, + itemBuilder: (context, index) { + return FRadio.custom( + value: index, + groupValue: group_menu_value1, + onChanged: (value) { + setter(() { + group_menu_value1 = value; + }); + }, + normal: FSuper( + width: double.infinity, + textAlign: TextAlign.left, + text: fileMenuList[index], + padding: EdgeInsets.only( + left: 20, right: 10, top: 3, bottom: 3), + ), + selected: FSuper( + width: double.infinity, + text: fileMenuList[index], + textAlign: TextAlign.left, + textColor: Colors.white, + backgroundColor: Color(0xff008FFF), + padding: EdgeInsets.only( + left: 20, right: 10, top: 3, bottom: 3), + ), + hover: FSuper( + width: double.infinity, + text: fileMenuList[index], + textAlign: TextAlign.left, + backgroundColor: Colors.black38.withOpacity(0.1), + padding: EdgeInsets.only( + left: 20, right: 10, top: 3, bottom: 3), + ), + ); + }), + ), + shadowColor: Colors.black38, + shadowBlur: 8.0, + shadowOffset: Offset(2.0, 2.0), + color: Colors.white, + corner: FFloatCorner.all(3), + controller: controller3_1, + alignment: FFloatAlignment.bottomLeft, + hideTriangle: true, + child: FRadio.custom( + width: 56, + height: 22, + value: 0, + groupValue: group_toolbar_value, + onChanged: (value) { + setState(() { + group_toolbar_value = value; + }); + controller3_1.show(); + }, + normal: FSuper( + text: "Finder", + textColor: Colors.white, + textSize: 14, + textAlignment: Alignment.center, + padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), + ), + hover: FSuper( + text: "Finder", + textColor: Colors.white, + textSize: 14, + textAlignment: Alignment.center, + padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), + backgroundColor: Color(0xff008FFF).withOpacity(0.2), + ), + selected: FSuper( + text: "Finder", + textColor: Colors.white, + textSize: 14, + padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), + backgroundColor: Color(0xff008FFF), + ), + ), + ), + const SizedBox(width: 4), + FFloat( + (setter) => Container( + width: 230, + height: 121, + padding: EdgeInsets.only(top: 6, bottom: 6), + child: ListView.builder( + shrinkWrap: true, + itemCount: fileMenuList.length, + itemBuilder: (context, index) { + return FRadio.custom( + value: index, + groupValue: group_menu_value2, + onChanged: (value) { + setter(() { + group_menu_value2 = value; + }); + }, + normal: FSuper( + width: double.infinity, + textAlign: TextAlign.left, + text: fileMenuList[index], + padding: EdgeInsets.only( + left: 20, right: 10, top: 3, bottom: 3), + ), + selected: FSuper( + width: double.infinity, + text: fileMenuList[index], + textAlign: TextAlign.left, + textColor: Colors.white, + backgroundColor: Color(0xff008FFF), + padding: EdgeInsets.only( + left: 20, right: 10, top: 3, bottom: 3), + ), + hover: FSuper( + width: double.infinity, + text: fileMenuList[index], + textAlign: TextAlign.left, + backgroundColor: Colors.black38.withOpacity(0.1), + padding: EdgeInsets.only( + left: 20, right: 10, top: 3, bottom: 3), + ), + ); + }), + ), + controller: controller3_2, + alignment: FFloatAlignment.bottomLeft, + margin: EdgeInsets.only(top: 2), + shadowColor: Colors.black38, + shadowBlur: 8.0, + shadowOffset: Offset(2.0, 2.0), + corner: FFloatCorner.all(3), + color: Colors.white, + triangleAlignment: TriangleAlignment.start, + triangleOffset: Offset(10, 10), + triangleWidth: 20, + triangleHeight: 15, + child: FRadio.custom( + width: 38, + height: 22, + value: 1, + groupValue: group_toolbar_value, + onChanged: (value) { + setState(() { + group_toolbar_value = value; + }); + controller3_2.show(); + }, + normal: FSuper( + text: "File", + textColor: Colors.white, + textSize: 14, + textAlignment: Alignment.center, + padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), + ), + hover: FSuper( + text: "File", + textColor: Colors.white, + textSize: 14, + textAlignment: Alignment.center, + padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), + backgroundColor: Color(0xff008FFF).withOpacity(0.2), + ), + selected: FSuper( + text: "File", + textColor: Colors.white, + textSize: 14, + padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), + backgroundColor: Color(0xff008FFF), + ), + ), + ), + const SizedBox(width: 4), + FFloat( + (setter) => Container( + width: 230, + height: 121, + padding: EdgeInsets.only(top: 6, bottom: 6), + child: ListView.builder( + shrinkWrap: true, + itemCount: fileMenuList.length, + itemBuilder: (context, index) { + return FRadio.custom( + value: index, + groupValue: group_menu_value3, + onChanged: (value) { + setter(() { + group_menu_value3 = value; + }); + }, + normal: FSuper( + width: double.infinity, + textAlign: TextAlign.left, + text: fileMenuList[index], + padding: EdgeInsets.only( + left: 20, right: 10, top: 3, bottom: 3), + ), + selected: FSuper( + width: double.infinity, + text: fileMenuList[index], + textAlign: TextAlign.left, + textColor: Colors.white, + backgroundColor: Color(0xff008FFF), + padding: EdgeInsets.only( + left: 20, right: 10, top: 3, bottom: 3), + ), + hover: FSuper( + width: double.infinity, + text: fileMenuList[index], + textAlign: TextAlign.left, +// backgroundColor: Color(0xff008FFF), + backgroundColor: Colors.black38.withOpacity(0.1), + padding: EdgeInsets.only( + left: 20, right: 10, top: 3, bottom: 3), + ), + ); + }), + ), + controller: controller3_3, + alignment: FFloatAlignment.rightTop, + margin: EdgeInsets.only(bottom: 8), + corner: FFloatCorner.all(3), + shadowColor: Colors.black38, + shadowBlur: 8.0, + shadowOffset: Offset(2.0, 2.0), + color: Colors.white, + triangleAlignment: TriangleAlignment.start, + triangleOffset: Offset(10, 10), + triangleWidth: 20, + triangleHeight: 15, + child: FRadio.custom( + width: 38, + height: 22, + value: 2, + groupValue: group_toolbar_value, + onChanged: (value) { + setState(() { + group_toolbar_value = value; + }); + controller3_3.show(); + }, + normal: FSuper( + text: "Edit", + textColor: Colors.white, + textSize: 14, + textAlignment: Alignment.center, + padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), + ), + hover: FSuper( + text: "Edit", + textColor: Colors.white, + textSize: 14, + textAlignment: Alignment.center, + padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), + backgroundColor: Color(0xff008FFF).withOpacity(0.2), + ), + selected: FSuper( + text: "Edit", + textColor: Colors.white, + textSize: 14, + padding: EdgeInsets.only(left: 6, right: 6, top: 3, bottom: 3), + backgroundColor: Color(0xff008FFF), + ), + ), + ), + ], + ), + padding: EdgeInsets.only(left: 12), + ); } - @override - void deactivate() { - super.deactivate(); - print( - 'PageC - deactivate,isCurrent = ${ModalRoute.of(context).isCurrent},isActive = ${ModalRoute.of(context).isActive}'); + Widget backgroundDemo() { + return FSuper( + width: double.infinity, + height: 40, + backgroundColor: Color(0xff000000), + child1: Row( + children: [ + FFloat( + (_) => FSuper( + text: "Surprise😃 !", + textColor: Colors.white, + ), + controller: controller2_1, + color: Color(0xff5D5D5E), + corner: FFloatCorner.all(6), + margin: EdgeInsets.only(bottom: 10), + padding: EdgeInsets.only(left: 9, right: 9, top: 3, bottom: 3), + child: FButton( + width: 72, + height: 30, + text: "esc", + textColor: Colors.white, + fontSize: 15, + corner: FButtonCorner.all(3), + padding: EdgeInsets.all(0), + color: Color(0xff373737), + effect: true, + onPressed: () { + controller2_1.show(); + }, + hoverColor: Colors.white60.withOpacity(0.3), + ), + canTouchOutside: false, + autoDismissDuration: Duration(milliseconds: 2000), + ), + const SizedBox(width: 16), + FFloat( + (_) => FSuper( + text: "HA🌝 !", + textColor: Colors.white, + ), + controller: controller2_3, + color: Color(0xff5D5D5E), + corner: FFloatCorner.all(6), + margin: EdgeInsets.only(bottom: 10), + padding: EdgeInsets.only(left: 9, right: 9, top: 3, bottom: 3), + child: FButton( + width: 72, + height: 30, + corner: FButtonCorner.all(3), + imageAlignment: ImageAlignment.left, + image: Icon(Icons.add, color: Colors.white, size: 18), + padding: EdgeInsets.all(0), + color: Color(0xff373737), + effect: true, + onPressed: () { + controller2_3.show(); + }, + hoverColor: Colors.white60.withOpacity(0.3), + ), + animDuration: Duration(milliseconds: 800), + ), + ], + ), + child1Alignment: Alignment.centerLeft, + child1Margin: EdgeInsets.only(left: 20), + child2: FFloat( + (_) => FSuper( + width: 200, + height: 40, + child1Alignment: Alignment.centerLeft, + child1: Icon(Icons.search, color: Colors.white, size: 18), + child1Margin: EdgeInsets.only(left: 9), + child2: SizedBox( + width: 150, + height: 35, + child: TextField( + maxLines: 1, + decoration: InputDecoration( + border: InputBorder.none, + hintText: "SEARCH", + hintStyle: TextStyle( + color: Colors.white70, + fontSize: 15, + ), + ), + style: TextStyle( + color: Colors.white, + fontSize: 15, + ), + cursorColor: Colors.white30, + ), + ), + child2Alignment: Alignment.centerLeft, + child2Margin: EdgeInsets.only(left: (9.0 + 18.0 + 9.0)), + ), + controller: controller2_2, + color: Colors.black.withOpacity(0.95), + backgroundColor: Colors.black26, + corner: FFloatCorner.all(20), + margin: EdgeInsets.only(bottom: 10, left: 10), + child: FButton( + width: 72, + height: 30, + corner: FButtonCorner.all(3), + imageAlignment: ImageAlignment.left, + image: Icon(Icons.search, color: Colors.white, size: 18), + padding: EdgeInsets.all(0), + color: Color(0xff373737), + effect: true, + onPressed: () { + controller2_2.show(); + }, + hoverColor: Colors.white60.withOpacity(0.3), + ), + alignment: FFloatAlignment.topRight, + triangleAlignment: TriangleAlignment.end, + triangleOffset: Offset(-39, 0), + ), + child2Alignment: Alignment.centerRight, + child2Margin: EdgeInsets.only(right: 20), + ); } - @override - void dispose() { - super.dispose(); - print('PageC - dispose'); + Widget baseDemo() { + return Container( + width: double.infinity, + height: 100, + alignment: Alignment.center, + child: FFloat( + (_) => createFloat1(), + controller: controller1, + padding: EdgeInsets.only(left: 9, right: 9, top: 6, bottom: 6), + corner: FFloatCorner.all(10), + alignment: floatAlignment1, + canTouchOutside: false, + child: buildChild1(), + animDuration: null, + ), + ); + } + + FSuper buildChild1() { + return FSuper( + width: 200, + height: 50, + textAlignment: Alignment.center, + text: text_1, + textColor: mainTextNormalColor, + textSize: 18, + corner: Corner.all(10), + backgroundColor: Colors.white, + padding: EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 12), + shadowColor: mainShadowColor, + shadowBlur: 5.0, + shadowOffset: Offset(2.0, 2.0), + ); + } + + Widget createFloat1() { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Found me!", + style: TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + const SizedBox(width: 6), + InkWell( + onTap: () => controller1.dismiss(), + child: Padding( + padding: const EdgeInsets.all(0), + child: Icon( + Icons.close, + size: 12, + color: Colors.white, + ), + ), + ), + ], + ); + } + + SizedBox buildMiddleMargin() { + return const SizedBox( + height: 26, + ); + } + + SizedBox buildSmallMargin() { + return const SizedBox( + height: 18, + ); + } + + Padding buildDesc(String desc) { + return Padding( + padding: const EdgeInsets.all(8), + child: Text( + desc, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey, + fontSize: 12, + ), + )); + } + + Container buildTitle(String title) { + return Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.all(9), + color: Color(0xffe0e0e0).withOpacity(0.38), + child: Text( + title, + style: TextStyle(color: mainTextSubColor.withOpacity(0.7)), + ), + ); + } + + FFloatAlignment randomFloatAlignment(FFloatAlignment alignment) { + switch (alignment) { + case FFloatAlignment.topLeft: + return FFloatAlignment.topCenter; + case FFloatAlignment.topCenter: + return FFloatAlignment.topRight; + case FFloatAlignment.topRight: + return FFloatAlignment.bottomLeft; + case FFloatAlignment.bottomLeft: + return FFloatAlignment.bottomCenter; + case FFloatAlignment.bottomCenter: + return FFloatAlignment.bottomRight; + case FFloatAlignment.bottomRight: + return FFloatAlignment.leftTop; + case FFloatAlignment.leftTop: + return FFloatAlignment.leftCenter; + case FFloatAlignment.leftCenter: + return FFloatAlignment.leftBottom; + case FFloatAlignment.leftBottom: + return FFloatAlignment.rightTop; + case FFloatAlignment.rightTop: + return FFloatAlignment.rightCenter; + case FFloatAlignment.rightCenter: + return FFloatAlignment.rightBottom; + case FFloatAlignment.rightBottom: + return FFloatAlignment.topLeft; + } + return FFloatAlignment.topCenter; } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f0a175f..cc2f695 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -23,6 +23,11 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.3 + fsuper: ^0.1.5 + fbutton: ^1.0.4 + fradio: + git: + url: git@gitlab.alibaba-inc.com:fapi/fradio.git dev_dependencies: flutter_test: @@ -40,9 +45,8 @@ flutter: uses-material-design: true # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg + assets: + - assets/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. diff --git a/lib/ffloat.dart b/lib/ffloat.dart index 32cebd3..7864740 100644 --- a/lib/ffloat.dart +++ b/lib/ffloat.dart @@ -3,12 +3,51 @@ import 'dart:math'; import 'package:flutter/material.dart'; +/// 描述三角的相对位置 +/// [start] - 三角在 [FFloat] 上下侧,表示三角和 [FFloat] 左边缘对齐;三角在 [FFloat] 左右侧,表示三角和 [FFloat] 上边缘对齐 +/// [center] - 三角在 [FFloat] 上下侧,表示三角水平居中;三角在 [FFloat] 左右侧,表示三角垂直居中 +/// [end] - 三角在 [FFloat] 上下侧,表示三角和 [FFloat] 右边缘对齐;三角在 [FFloat] 左右侧,表示三角和 [FFloat] 下边缘对齐 +/// +/// Describe the relative position of the triangle +/// [start]-The triangle is above and below [FFloat], indicating that the triangle is aligned with the left edge of [FFloat]; +/// the triangle is on the left and right of [FFloat], indicating that the triangle is aligned with the top edge of [FFloat] +/// [center] - The triangle is above and below [FFloat], indicating that the triangle is horizontally centered; +/// the triangle is on the left and right sides of [FFloat], indicating that the triangle is vertically centered +/// [end] - The triangle is above and below [FFloat], indicating that the triangle is aligned with the right edge of [FFloat]; +/// the triangle is on the left and right of [FFloat], indicating that the triangle is aligned with the bottom edge of [FFloat] enum TriangleAlignment { start, center, end, } +/// 描述 [FFloat] 相对于锚点元素的位置。 +/// topLeft - 在锚点元素【上方】,且【左边缘】与锚点元素对齐 +/// topCenter - 在锚点元素【上方】,且水平居中 +/// topRight - 在锚点元素【上方】,且【右边缘】与锚点元素对齐 +/// bottomLeft - 在锚点元素【下方】,且【左边缘】与锚点元素对齐 +/// bottomCenter - 在锚点元素【下方】,且水平居中 +/// bottomRight - 在锚点元素【下方】,且【右边缘】与锚点元素对齐 +/// leftTop - 在锚点元素【左侧】,且【上边缘】与锚点元素对齐 +/// leftCenter - 在锚点元素【左侧】,且垂直居中 +/// leftBottom - 在锚点元素【左侧】,且【下边缘】与锚点元素对齐 +/// rightTop - 在锚点元素【右侧】,且【上边缘】与锚点元素对齐 +/// rightCenter - 在锚点元素【右侧】,且垂直居中 +/// rightBottom - 在锚点元素【右侧】,且【下边缘】与锚点元素对齐 +/// +/// Description [FFloat] The position relative to the anchor element. +/// topLeft - In the anchor element [above], and the [leftEdge] is aligned with the anchor element +/// topCenter - In the anchor element [above], and horizontally centered +/// topRight - In the anchor element [above], and the [rightEdge] is aligned with the anchor element +/// bottomLeft - In the anchor element [below], and the [leftEdge] is aligned with the anchor element +/// bottomCenter - In the anchor element [below], and horizontally centered +/// bottomRight - In the anchor element [below], and the [rightEdge] is aligned with the anchor element +/// leftTop - In the anchor element [left], and the [upperEdge] is aligned with the anchor element +/// leftCenter - In the anchor element [left], and vertically centered +/// leftBottom - In the anchor element [left], and the [bottomEdge] is aligned with the anchor element +/// rightTop - In the anchor element [right], and the [upperEdge] is aligned with the anchor element +/// rightCenter - In the anchor element [right], and vertically centered +/// rightBottom - In the anchor element [right side], and the [bottomEdge] is aligned with the anchor element enum FFloatAlignment { topLeft, topCenter, @@ -24,30 +63,152 @@ enum FFloatAlignment { rightBottom, } +/// 用于返回一个 [Widget],如果只更新内容区域的话,通过 setter((){}) 进行 +/// +/// Used to return a [Widget], if only the content area is updated, through setter (() {}) +typedef FloatBuilder = Widget Function(StateSetter setter); + +/// [FFloat] 能够在屏幕的任意位置浮出一个组件。甚至可以基于 [child] 锚点组件来动态的确定漂浮组件的位置。 +/// [FFloat] 同时提供了绝妙的配置选项。圆角、描边、背景、偏移、装饰三角。 +/// [FFloat] 设置了 [FFloatController] 控制器,可以方便的随时对漂浮组件进行控制。 +/// +/// [FFloat] can float a component anywhere on the screen. You can even dynamically determine the position of the floating component based on the [child] anchor component. +/// [FFloat] also provides wonderful configuration options. Fillet, stroke, background, offset, decorative triangle. +/// [FFloat] The [FFloatController] controller is set, which can easily control the floating component at any time. class FFloat extends StatefulWidget { - final double triangleWidth; - final double triangleHeight; - final TriangleAlignment triangleAlignment; - final Offset triangleOffset; + /// 通过 [FloatBuilder] 返回 [FFloat] 的内容组件。 + /// 如果只更新内容区域的话,通过 setter((){}) 进行 + /// + /// [FloatBuilder] returns the content component of [FFloat]. + /// If only the content area is updated, proceed via setter (() {}) + final FloatBuilder builder; + + /// [FFloat] 的颜色 + /// + /// [FFloat] colors final Color color; + + /// 锚点组件 + /// + /// Anchor component final Widget child; + + /// 位置。通过 [location] 指定 [FFloat] 的位置后,基于锚点确定位置的所有配置将失效。 + /// + /// position. After specifying the location of [FFloat] through [location], all configurations that determine the location based on the anchor point will be invalid. final Offset location; - FFloatAlignment alignment; + + /// [FFloat] 基于 [child] 锚点元素的相对位置。 + /// + /// [FFloat] Based on the relative position of the [child] anchor element. + final FFloatAlignment alignment; + + /// [FFloat] 基于相对确定锚定点的间距 + /// + /// [FFloat] Determine the distance between anchor points based on relative final EdgeInsets margin; + + /// [FFloat] 内部间距 + /// + /// [FFloat] Internal spacing final EdgeInsets padding; - final Color strokeColor; - final double strokeWidth; - final FFloatCorner corner; - final FFloatCornerStyle cornerStyle; - final Widget float; + + /// 点击 [FFloat] 范围外区域是否隐藏。 + /// + /// Click [FFloat] to hide the area outside the range. final bool canTouchOutside; + + /// [FFloat] 浮出时,背景区域的颜色 + /// + /// [FFloat] The color of the background area when floating final Color backgroundColor; - final bool hideTriangle; + + /// 自动消失时长。如果为 null,就不会自动消失。 + /// + /// Duration of automatic disappearance. If it is null, it will not disappear automatically. final Duration autoDismissDuration; + + /// 通过 [FFloatController] 可以控制 [FFloat] 的显示/隐藏。详见 [FFloatController]。 + /// + /// [FFloatController] can control the display / hide of [FFloat]. See [FFloatController] for details. final FFloatController controller; + /// 显示/隐藏动效时长。默认 `Duration(milliseconds: 100)` + /// + /// Show / hide animation duration. Default `Duration (milliseconds: 100)` + final Duration animDuration; + + /// 三角的宽 + /// + /// The width of the triangle + final double triangleWidth; + + /// 三角的高 + /// + /// The height of the triangle + final double triangleHeight; + + /// 三角的相对位置。详见 [TriangleAlignment]。 + /// + /// The relative position of the triangle. See [TriangleAlignment] for details. + final TriangleAlignment triangleAlignment; + + /// 三角的位置偏移 + /// + /// Triangle position offset + final Offset triangleOffset; + + /// 是否隐藏装饰三角 + /// + /// Whether to hide the decorative triangle + final bool hideTriangle; + + /// 描边颜色 + /// + /// Stroke color + final Color strokeColor; + + /// 描边宽度 + /// + /// Stroke width + final double strokeWidth; + + /// 圆角。详见 [FFloatCorner]。 + /// + /// Corner. See [FFloatCorner] for details. + final FFloatCorner corner; + + /// 圆角样式。详见 [FFloatCornerStyle]。 + /// + /// Corner style. See [FFloatCornerStyle] for details. + final FFloatCornerStyle cornerStyle; + + /// 设置组件阴影颜色 + /// + /// Set component shadow color + Color shadowColor; + + /// 设置组件阴影偏移 + /// + /// Set component shadow offset + Offset shadowOffset; + + /// 设置组件高斯与阴影形状卷积的标准偏差。 + /// + /// Sets the standard deviation of the component's Gaussian convolution with the shadow shape. + double shadowBlur; + + /// 设置组件渐变色背景。会覆盖 [backgroundColor] + /// 你可选择 [LinearGradient],[RadialGradient],[SweepGradient] 等.. + /// + /// Sets the gradient background of the component. [backgroundColor] + /// You can choose [LinearGradient], [RadialGradient], [SweepGradient], etc .. + Gradient gradient; + + _FFloat _float; + FFloat( - this.float, { + this.builder, { this.child, this.location, this.margin = EdgeInsets.zero, @@ -63,14 +224,56 @@ class FFloat extends StatefulWidget { this.corner, this.cornerStyle = FFloatCornerStyle.round, this.backgroundColor = Colors.transparent, + this.gradient, this.canTouchOutside = true, this.hideTriangle = false, this.autoDismissDuration, this.controller, + this.animDuration = const Duration(milliseconds: 100), + this.shadowBlur = 1.0, + this.shadowColor, + this.shadowOffset, }); @override _FFloatState createState() => _FFloatState(); + + void show(BuildContext context) { + _float = _FFloat( + context, + builder, + location: location, + margin: margin, + triangleWidth: triangleWidth, + triangleHeight: triangleHeight, + triangleAlignment: triangleAlignment, + triangleOffset: triangleOffset, + alignment: alignment, + padding: padding, + color: color, + strokeColor: strokeColor, + strokeWidth: strokeWidth, + corner: corner, + cornerStyle: cornerStyle, + backgroundColor: backgroundColor, + gradient: gradient, + canTouchOutside: canTouchOutside, + hideTriangle: hideTriangle, + autoDismissDuration: autoDismissDuration, + controller: controller, + animDuration: animDuration, + shadowBlur: shadowBlur, + shadowColor: shadowColor, + shadowOffset: shadowOffset, + ); + _float.show(); + } + + void dismiss() { + if (_float != null) { + _float.dismiss(); + } + } } class _FFloatState extends State { @@ -78,33 +281,49 @@ class _FFloatState extends State { Offset childLocation; Size childSize; - OverlayEntry _overlayEntry; - bool _isShow = false; - - bool get isShow => _isShow; - - set isShow(bool value) { - if (_isShow == value) return; - _isShow = value; - if (widget.controller != null) { - widget.controller.isShow = value; - } - } - - Timer dismissTimer; - - _FTipContentController ffloatContentController; + _FFloat _float; @override void initState() { - super.initState(); - if (widget.controller != null) { - widget.controller._state = this; - } - ffloatContentController = new _FTipContentController(); + init(); if (widget.location == null) { postUpdateCallback(); } + super.initState(); + } + + void init() { + createFloat(context); + } + + void createFloat(BuildContext context) { + _float = _FFloat( + context, + widget.builder, + location: widget.location, + margin: widget.margin, + triangleWidth: widget.triangleWidth, + triangleHeight: widget.triangleHeight, + triangleAlignment: widget.triangleAlignment, + triangleOffset: widget.triangleOffset, + alignment: widget.alignment, + padding: widget.padding, + color: widget.color, + strokeColor: widget.strokeColor, + strokeWidth: widget.strokeWidth, + corner: widget.corner, + cornerStyle: widget.cornerStyle, + backgroundColor: widget.backgroundColor, + gradient: widget.gradient, + canTouchOutside: widget.canTouchOutside, + hideTriangle: widget.hideTriangle, + autoDismissDuration: widget.autoDismissDuration, + controller: widget.controller, + animDuration: widget.animDuration, + shadowBlur: widget.shadowBlur, + shadowColor: widget.shadowColor, + shadowOffset: widget.shadowOffset, + ); } void postUpdateCallback() { @@ -122,8 +341,8 @@ class _FFloatState extends State { needUpdate = true; childSize = size; } - if (ffloatContentController != null && needUpdate && isShow) { - ffloatContentController.update(childSize, childLocation); + if (_float != null && needUpdate) { + _float.update(childSize, childLocation); } postUpdateCallback(); }); @@ -131,31 +350,64 @@ class _FFloatState extends State { @override void dispose() { -// print("dispose"); - if (widget.controller != null) { - widget.controller.dispose(); - } - if (ffloatContentController != null) { - ffloatContentController.dispose(); + if (_float != null) { + _float.dispose(); + _float = null; } - dismiss(); super.dispose(); } @override void didUpdateWidget(FFloat oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.controller != null) { - widget.controller._state = this; - } - if (isShow) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) return; - _showTip(); - }); + if (_float != null) { + asyncParams(); + if (_float.isShow) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + if (_float != null) { + _float._showFloat(); + } + }); + } } } + void asyncParams() { + if (_float == null) return; + _float + ..context = context + ..builder = widget.builder + ..location = widget.location + ..margin = widget.margin + ..triangleWidth = widget.triangleWidth + ..triangleHeight = widget.triangleHeight + ..triangleAlignment = widget.triangleAlignment + ..triangleOffset = widget.triangleOffset + ..alignment = widget.alignment + ..padding = widget.padding + ..color = widget.color + ..strokeColor = widget.strokeColor + ..strokeWidth = widget.strokeWidth + ..corner = widget.corner + ..cornerStyle = widget.cornerStyle + ..backgroundColor = widget.backgroundColor + ..gradient = widget.gradient + ..canTouchOutside = widget.canTouchOutside + ..hideTriangle = widget.hideTriangle + ..autoDismissDuration = widget.autoDismissDuration + ..controller = widget.controller + ..animDuration = widget.animDuration + ..shadowBlur = widget.shadowBlur + ..shadowColor = widget.shadowColor + ..shadowOffset = widget.shadowOffset; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + @override Widget build(BuildContext context) { if (widget.location != null) { @@ -165,7 +417,7 @@ class _FFloatState extends State { key: key, builder: (context, _) { return GestureDetector( - onTap: showTip, + onTap: handleOnTap, child: widget.child, ); }, @@ -173,53 +425,189 @@ class _FFloatState extends State { } } - void showTip() { - /// 防止重复显示 + void handleOnTap() { + if (_float != null) { + asyncParams(); + _float.show(); + } + } +} + +class _FFloat { + double triangleWidth; + double triangleHeight; + TriangleAlignment triangleAlignment; + Offset triangleOffset; + Color color; + Widget child; + FFloatAlignment alignment; + EdgeInsets margin; + EdgeInsets padding; + Color strokeColor; + double strokeWidth; + FFloatCorner corner; + FFloatCornerStyle cornerStyle; + FloatBuilder builder; + bool canTouchOutside; + Color backgroundColor; + Gradient gradient; + bool hideTriangle; + Duration autoDismissDuration; + FFloatController controller; + Offset location; + Duration animDuration; + double shadowBlur; + Color shadowColor; + Offset shadowOffset; + + Offset childLocation; + Size childSize; + + OverlayEntry _overlayEntry; + bool _isShow = false; + + bool get isShow => _isShow; + + set isShow(bool value) { + if (_isShow == value) return; + _isShow = value; + if (controller != null) { + controller.isShow = value; + } + } + + Timer dismissTimer; + + _FTipContentController ffloatContentController; + + ValueNotifier notifier; + + BuildContext context; + + _FFloat( + this.context, + this.builder, { + this.child, + this.location, + this.margin = EdgeInsets.zero, + this.triangleWidth = 12, + this.triangleHeight = 6, + this.triangleAlignment = TriangleAlignment.center, + this.triangleOffset = Offset.zero, + this.alignment = FFloatAlignment.topCenter, + this.padding, + this.color = _FFloatContent.DefaultColor, + this.strokeColor, + this.strokeWidth, + this.corner, + this.cornerStyle = FFloatCornerStyle.round, + this.backgroundColor = Colors.transparent, + this.gradient, + this.canTouchOutside = true, + this.hideTriangle = false, + this.autoDismissDuration, + this.controller, + this.animDuration, + this.shadowBlur = 1.0, + this.shadowColor, + this.shadowOffset, + }) { + init(); + } + + void init() { + notifier = new ValueNotifier(0); + notifier.addListener(() { + if (notifier.value == 0) { + realDismiss(); + } + }); + if (controller != null) { + controller._show = () { + show(); + }; + controller._dismiss = () { + dismiss(); + }; + controller._rebuildShow = () { + rebuildShow(); + }; + } + ffloatContentController = new _FTipContentController(); + } + + void update(Size anchorSize, Offset location) { + childSize = anchorSize; + childLocation = location; + if (ffloatContentController != null && isShow) { + ffloatContentController.update(anchorSize, location); + } + } + + void dispose() { + if (controller != null) { + controller.dispose(); + } + if (ffloatContentController != null) { + ffloatContentController.dispose(); + } + if (dismissTimer != null && dismissTimer.isActive) { + dismissTimer.cancel(); + dismissTimer = null; + } + } + + void show() { + /// Prevent duplicate display if (isShow) return; - isShow = true; - _showTip(); + _showFloat(); } - void rebuildShowTip() { - isShow = true; - _showTip(); + void rebuildShow() { + _showFloat(); } - void _showTip() { + void _showFloat() { + final bool hasShow = isShow; + isShow = true; if (_overlayEntry != null) { _overlayEntry.remove(); } OverlayState overlayState = Overlay.of(context); _overlayEntry = OverlayEntry(builder: (BuildContext context) { - Widget tipContent = _FFloatContent( - (widget.location != null ? widget.location : childLocation) ?? - Offset.zero, - widget.float, + Widget floatContent = _FFloatContent( + (location != null ? location : childLocation) ?? Offset.zero, + builder, anchorSize: childSize ?? Size.zero, - alignment: widget.location != null - ? FFloatAlignment.bottomLeft - : widget.alignment, - triangleWidth: widget.triangleWidth, - triangleHeight: widget.triangleHeight, - triangleOffset: widget.triangleOffset, - triangleAlignment: widget.triangleAlignment, - margin: widget.margin, - padding: widget.padding, - color: widget.color, - strokeColor: widget.strokeColor, - strokeWidth: widget.strokeWidth, - corner: widget.corner, - cornerStyle: widget.cornerStyle, - backgroundColor: widget.backgroundColor, - onTouchBackground: widget.canTouchOutside ? dismiss : null, - hideTriangle: widget.hideTriangle, + alignment: location != null ? FFloatAlignment.bottomLeft : alignment, + triangleWidth: triangleWidth, + triangleHeight: triangleHeight, + triangleOffset: triangleOffset, + triangleAlignment: triangleAlignment, + margin: margin, + padding: padding, + color: color, + strokeColor: strokeColor, + strokeWidth: strokeWidth, + corner: corner, + cornerStyle: cornerStyle, + backgroundColor: backgroundColor, + gradient: gradient, + onTouchBackground: canTouchOutside ? dismiss : null, + hideTriangle: hideTriangle, controller: ffloatContentController, + notifier: notifier, + animDuration: animDuration, + shadowOffset: shadowOffset, + shadowColor: shadowColor, + shadowBlur: shadowBlur, + initShow: hasShow, ); - return tipContent; + return floatContent; }); overlayState.insert(_overlayEntry); - if (widget.autoDismissDuration != null && dismissTimer == null) { - dismissTimer = Timer(widget.autoDismissDuration, () { + if (autoDismissDuration != null && dismissTimer == null) { + dismissTimer = Timer(autoDismissDuration, () { if (_overlayEntry != null) { dismissTimer = null; dismiss(); @@ -234,6 +622,12 @@ class _FFloatState extends State { dismissTimer.cancel(); dismissTimer = null; } + if (notifier != null) { + notifier.value = 1; + } + } + + void realDismiss() { if (_overlayEntry != null) { _overlayEntry.remove(); _overlayEntry = null; @@ -244,6 +638,7 @@ class _FFloatState extends State { class _FFloatContent extends StatefulWidget { static const Color DefaultColor = Color(0x7F000000); + FloatBuilder builder; Size anchorSize; Offset location; Widget child; @@ -260,13 +655,20 @@ class _FFloatContent extends StatefulWidget { FFloatCorner corner; FFloatCornerStyle cornerStyle; Color backgroundColor; + Gradient gradient; VoidCallback onTouchBackground; bool hideTriangle; _FTipContentController controller; + ValueNotifier notifier; + Duration animDuration; + double shadowBlur; + Color shadowColor; + Offset shadowOffset; + bool initShow; _FFloatContent( this.location, - this.child, { + this.builder, { this.anchorSize = Size.zero, this.triangleWidth = 12, this.triangleHeight = 6, @@ -281,24 +683,57 @@ class _FFloatContent extends StatefulWidget { this.corner, this.cornerStyle = FFloatCornerStyle.round, this.backgroundColor = Colors.transparent, + this.gradient, this.onTouchBackground, this.hideTriangle = false, this.controller, + this.notifier, + this.animDuration, + this.shadowColor, + this.shadowOffset, + this.shadowBlur = 1.0, + this.initShow, }); @override _FFloatContentState createState() => _FFloatContentState(); } -class _FFloatContentState extends State<_FFloatContent> { +class _FFloatContentState extends State<_FFloatContent> + with TickerProviderStateMixin { GlobalKey key = GlobalKey(); Size areaSize; Offset location; Size anchorSize; + AnimationController animationController; + Animation scaleAnimation; + bool init; @override void initState() { + init = true; super.initState(); + animationController = new AnimationController( + vsync: this, + duration: widget.animDuration ?? Duration(milliseconds: 0)); + scaleAnimation = + Tween(begin: 0.0, end: 1.0).animate(animationController); + scaleAnimation.addListener(() { + setState(() {}); + }); + scaleAnimation.addStatusListener((status) { +// print('scaleAnimation.status = ${status.toString()}'); + if ((status == AnimationStatus.dismissed || + status == AnimationStatus.completed) && + widget.notifier != null && + widget.notifier.value == 1) { + widget.notifier.value = 0; + } + }); + animationController.forward(); + if (widget.notifier != null) { + widget.notifier.addListener(onNotifier); + } location = widget.location; anchorSize = widget.anchorSize; if (widget.controller != null) { @@ -307,6 +742,15 @@ class _FFloatContentState extends State<_FFloatContent> { postUpdateCallback(); } + void onNotifier() { + if (mounted && + widget.notifier != null && + widget.notifier.value == 1 && + animationController != null) { + animationController.reverse(from: 1.0); + } + } + void postUpdateCallback() { WidgetsBinding.instance.addPostFrameCallback((time) { if (!mounted) return; @@ -324,6 +768,20 @@ class _FFloatContentState extends State<_FFloatContent> { super.didUpdateWidget(oldWidget); location = widget.location; anchorSize = widget.anchorSize; +// if (animationController != null) { +// animationController.forward(); +// } + } + + @override + void dispose() { + if (widget.notifier != null) { + widget.notifier.removeListener(onNotifier); + } + if (animationController != null) { + animationController.dispose(); + } + super.dispose(); } @override @@ -349,7 +807,8 @@ class _FFloatContentState extends State<_FFloatContent> { ); } children.add(background); - children.add(buildFloatContent()); + Widget content = buildFloatContent(); + children.add(content); return Stack( overflow: Overflow.visible, children: children, @@ -382,12 +841,25 @@ class _FFloatContentState extends State<_FFloatContent> { borderRadius: borderRadius, side: borderSide, ); - var decoration = ShapeDecoration(color: color, shape: shape); + var decoration = ShapeDecoration( + color: widget.gradient == null ? color : null, + gradient: widget.gradient, + shape: shape, + shadows: widget.shadowColor != null && widget.shadowBlur != 0 + ? [ + BoxShadow( + color: widget.shadowColor, + offset: widget.shadowOffset ?? Offset(0, 0), + blurRadius: widget.shadowBlur, + ) + ] + : null, + ); Widget content = Container( decoration: decoration, padding: widget.padding, key: key, - child: widget.child, + child: widget.builder != null ? widget.builder(setState) : Container(), ); children.add(content); if (areaSize != null) { @@ -403,6 +875,7 @@ class _FFloatContentState extends State<_FFloatContent> { child: CustomPaint( size: Size(widget.triangleWidth, widget.triangleHeight), painter: _TrianglePainter( + gradient: widget.gradient, color: color, strokeColor: widget.strokeColor, strokeWidth: widget.strokeWidth, @@ -420,9 +893,13 @@ class _FFloatContentState extends State<_FFloatContent> { offstage: areaSize == null, child: Material( color: Colors.transparent, - child: Stack( - overflow: Overflow.visible, - children: children, + child: Transform.scale( + scale: scaleAnimation.value, + alignment: matchScaleAnim(widget.anchorSize == Size.zero), + child: Stack( + overflow: Overflow.visible, + children: children, + ), ), ), ), @@ -430,6 +907,37 @@ class _FFloatContentState extends State<_FFloatContent> { return floatContent; } + Alignment matchScaleAnim(bool center) { + if (center) return Alignment.center; + switch (widget.alignment) { + case FFloatAlignment.topLeft: + return Alignment.bottomLeft; + case FFloatAlignment.topCenter: + return Alignment.bottomCenter; + case FFloatAlignment.topRight: + return Alignment.bottomRight; + case FFloatAlignment.bottomLeft: + return Alignment.topLeft; + case FFloatAlignment.bottomCenter: + return Alignment.topCenter; + case FFloatAlignment.bottomRight: + return Alignment.topRight; + case FFloatAlignment.leftTop: + return Alignment.topRight; + case FFloatAlignment.leftCenter: + return Alignment.centerRight; + case FFloatAlignment.leftBottom: + return Alignment.bottomRight; + case FFloatAlignment.rightTop: + return Alignment.topLeft; + case FFloatAlignment.rightCenter: + return Alignment.centerLeft; + case FFloatAlignment.rightBottom: + return Alignment.bottomLeft; + } + return Alignment.center; + } + Offset calculateAreaOffset() { if (areaSize == null) return Offset.zero; Offset offset = Offset( @@ -726,11 +1234,17 @@ enum FFloatCornerStyle { bevel, } +/// 通过 [FFloatController] 可以控制 [FFloat] 的显示、隐藏,以及感知状态变化。 +/// +/// [FFloatController] can control [FFloat] display, hide, and sense state changes. class FFloatController { VoidCallback _callback; bool _isShow = false; + /// [FFloat] 是否显示 + /// + /// [FFloat] Whether to display bool get isShow => _isShow; set isShow(bool value) { @@ -741,31 +1255,49 @@ class FFloatController { } } - _FFloatState _state; +// _FFloatState _state; + VoidCallback _show; + VoidCallback _dismiss; + VoidCallback _rebuildShow; + + /// 隐藏 [FFloat] + /// + /// Hide [FFloat] void dismiss() { - if (_state != null && _state.mounted) { - _state.dismiss(); + if (_dismiss != null) { + _dismiss(); } } + /// 显示 [FFloat]。如果已经显示,将不会再次重建。 + /// + /// Show [FFloat]。If it is already displayed, it will not be rebuilt again. void show() { - if (_state != null && _state.mounted) { - _state.showTip(); + if (_show != null) { + _show(); } } + /// 显示 [FFloat]。会重建。 + /// + /// [FFloat] is displayed. Will rebuild. void rebuildShow() { - if (_state != null && _state.mounted) { - _state.rebuildShowTip(); + if (_rebuildShow != null) { + _rebuildShow(); } } + /// 销毁 + /// + /// destroy dispose() { - _state = null; _callback = null; } + /// 设置监听。当 [FFloat] 显示状态发生变化的时候会回调。 + /// + /// Set up monitoring. It will be called back when [FFloat] display status changes. setStateChangedListener(VoidCallback listener) { _callback = listener; } @@ -775,28 +1307,43 @@ class _TrianglePainter extends CustomPainter { Color color; double strokeWidth; Color strokeColor; + Gradient gradient; _TrianglePainter({ this.color = _FFloatContent.DefaultColor, this.strokeWidth = 0, this.strokeColor, + this.gradient, }); @override void paint(Canvas canvas, Size size) { - Paint paint = Paint() - ..isAntiAlias = true - ..color = color - ..strokeWidth = strokeWidth ?? 0 - ..style = PaintingStyle.fill; + Paint paint = Paint(); + + if (gradient != null) { + paint + ..isAntiAlias = true + ..strokeWidth = strokeWidth ?? 0 + ..style = PaintingStyle.fill + ..shader = + gradient.createShader(Rect.fromLTRB(0, 0, size.width, size.height)); + } else { + paint + ..isAntiAlias = true + ..color = color + ..strokeWidth = strokeWidth ?? 0 + ..style = PaintingStyle.fill; + } Path path = Path(); path.moveTo(size.width / 2, 0); path.lineTo(0, size.height); path.lineTo(size.width, size.height); path.close(); canvas.drawPath(path, paint); + if (strokeColor != null && strokeWidth != null && strokeWidth > 0) { paint + ..shader = null ..color = strokeColor ..style = PaintingStyle.stroke; canvas.drawPath(path, paint); diff --git a/pubspec.yaml b/pubspec.yaml index ec7d71f..68f636f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ name: ffloat -description: A new Flutter plugin. -version: 0.0.1 -author: -homepage: +description: FFloat, although simple and easy to use, can satisfy all your imagination of the floating layer. +version: 1.0.0 +author: CoorChice +homepage: https://github.com/Fliggy-Mobile/ffloat environment: sdk: ">=2.7.0 <3.0.0"