├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── floating.iml ├── floating_android.iml ├── lib ├── button_widget.dart ├── floating │ ├── assist │ │ ├── Point.dart │ │ ├── animation_helper.dart │ │ ├── floating_data.dart │ │ ├── floating_slide_type.dart │ │ └── slide_stop_type.dart │ ├── control │ │ ├── common_control.dart │ │ └── scroll_position_control.dart │ ├── floating.dart │ ├── listener │ │ └── event_listener.dart │ ├── manager │ │ ├── floating_manager.dart │ │ └── scroll_position_manager.dart │ ├── utils │ │ └── floating_log.dart │ └── view │ │ └── floating_view.dart ├── floating_icon.dart ├── floating_increment.dart ├── floating_play.dart ├── floating_scroll.dart ├── main.dart └── page.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | .idea/ 8 | .vagrant/ 9 | .sconsign.dblite 10 | .svn/ 11 | 12 | 13 | *.swp 14 | profile 15 | 16 | DerivedData/ 17 | 18 | 19 | .generated/ 20 | 21 | *.pbxuser 22 | *.mode1v3 23 | *.mode2v3 24 | *.perspectivev3 25 | 26 | !default.pbxuser 27 | !default.mode1v3 28 | !default.mode2v3 29 | !default.perspectivev3 30 | 31 | xcuserdata 32 | 33 | *.moved-aside 34 | 35 | *.pyc 36 | *sync/ 37 | Icon? 38 | .tags* 39 | 40 | build/ 41 | .ios/ 42 | .android/ 43 | .flutter-plugins 44 | .flutter-plugins-dependencies 45 | 46 | # Symbolication related 47 | app.*.symbols 48 | 49 | # Obfuscation related 50 | app.*.map.json 51 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited . 5 | 6 | version: 7 | revision: 83b9e99cfbb8be5215514d7fa21191961b4a620d 8 | channel: dev 9 | 10 | project_type: module 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.1.1 2 | ![修复当设置为吸附左边时,悬浮窗位置不正确的问题](https://github.com/LvKang-insist/flutter_floating/issues/37 ) 3 | 去掉屏幕宽高改变的日志 4 | 增加滑动结束后吸附边缘时的速度 edgeSpeed 5 | 6 | 7 | ### v1.1.0 8 | 修复悬浮窗外尺寸发生变化时悬浮窗位置未恢复的问题 9 | 10 | ### v1.0.8 11 | 增加获取位置的api 12 | 创建 Floating 时可根据自定义的屏幕位置(Point)创建 13 | 修改回调事件中的参数为 Point 14 | 修改部分api,优化代码 15 | 16 | ### v1.0.7 17 | 增加吸附边缘的时候的自定义吸附边缘边距,优化代码逻辑 18 | 19 | ### v1.0.6 20 | 增加悬浮窗控制状态,优化代码逻辑 21 | 22 | ### v1.0.5 23 | 增加自动回弹位置的控制,可自由选择靠左,靠右或者是自动识别 24 | 25 | ### v1.0.4 26 | 增加悬浮窗移动 Api,修改bug,提高使用体验 27 | 28 | ### v1.0.3 29 | 修改部分bug,优化代码 30 | 31 | ### v1.0.2 32 | 修改部分Api,新增对悬浮窗大小改变时悬浮窗位置的适配,优化代码 33 | 34 | ### v1.0.1 35 | 项目迁移至 flutter3.0,3.0一下可能无法使用,请自行升级flutters SDK 36 | 37 | ### v0.1.6 38 | 修复 moveEndListener 无法回调,优化内部逻辑。flutter 3.0以下使用 39 | 40 | ### v0.1.5 41 | 修复一些已知的问题,提高使用体验 42 | 43 | ### v0.1.4 44 | 由于 v0.1.3 版本导致 flutter3.0 以下无法使用,所以,flutter3.0 以下 请使用 v0.1.4,3.0及以上使用 v0.1.3即可 45 | 46 | ### v0.1.3 47 | 适配 flutter 3.O,修改一些已知问题 48 | 49 | ### v0.1.2 50 | 修改部分API,优化使用体验 51 | 52 | ### v0.1.1 53 | 修复位置缓存无作用的问题 ,新增边缘吸附的控制 ,优化使用体验... 54 | 55 | ### v0.1.0 56 | 修复点击事件的bug ,优化开发体验,自适应子组件的大小,无需手动传入大小 ,其他bug 57 | 58 | ### v0.0.1 59 | 一个灵活和强大的悬浮窗口解决方案 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 345 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # floating 2 | 3 | ![68747470733a2f2f67697465652e636f6d2f6c766b6e6167696e6973742f7069632d676f2d7069637572652d6265642f7261772f6d61737465722f696d616765732f32303232303231363138343530302e6a706567](https://raw.githubusercontent.com/LvKang-insist/PicGo/main/202206141432981.jpg) 4 | 5 | **Floating** 是一个灵活且强大的悬浮窗解决方案 6 | 7 | 8 | 9 | ### 特性 10 | 11 | - 全局的悬浮窗管理机制 12 | - 支持各项回调监听,如移动、按下等 13 | - 支持自定义是否保存悬浮窗的位置信息 14 | - 支持单页面及全局使用,可插入 N 个悬浮窗 15 | - 支持自定义禁止滑动区域,例如在 距离顶部 50 到底部的区域内滑动等 16 | - 完善的日志系统,可查看不同悬浮窗对应的 Log 17 | - 支持自定义位置方向及悬浮窗的各项指标 18 | - 支持越界回弹,边缘自动吸附(是否吸附,吸附位置可选,速度可调),多指触摸移动,自适应屏幕旋转以及小窗口等情况 19 | - 自适应悬浮窗大小 20 | - 适配悬浮窗动画,对悬浮窗大小改变时位置进行适配 21 | - 代码内可更改浮窗位置 22 | - ..... 23 | 24 | ### 打开方式 25 | 26 | 项目迁移至 flutter3.0,目前使用的SDK是 flutter 3.0.3,3.0 以下可能无法使用,请自行升级flutters SDK 27 | ``` 28 | flutter_floating: ^1.1.0 29 | ``` 30 | #### 效果图 31 | 32 | | 全局 | 小屏 | 缩放屏幕 | 33 | | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | 34 | | ![全屏悬浮窗](https://cdn.jsdelivr.net/gh/LvKang-insist/PicGo/202202171737802.gif) | ![小屏悬浮窗](https://cdn.jsdelivr.net/gh/LvKang-insist/PicGo/202202172155850.gif) | ![缩放屏幕](https://cdn.jsdelivr.net/gh/LvKang-insist/PicGo/202202172155135.gif) | 35 | | 旋转屏幕 | 多指滑动 | | 36 | | ![旋转屏幕](https://cdn.jsdelivr.net/gh/LvKang-insist/PicGo/202202171740609.gif) | ![多指滑动](https://cdn.jsdelivr.net/gh/LvKang-insist/PicGo/202202171740850.gif) | | 37 | 38 | ### 可自由控制的日志查看 39 | 40 | 创建悬浮窗的时候通过 `isShowLog` 属性控制,不同的悬浮窗 Log 会通过不同 key 显示出来 41 | 42 | ```dart 43 | I/flutter (24648): Floating_Log 1 : 按下 X:0.0 Y:150.0 44 | I/flutter (24648): Floating_Log 1 : 抬起 X:0.0 Y:150.0 45 | I/flutter (24648): Floating_Log 1 : 移动 X:0.36363636363636687 Y:150.0 46 | I/flutter (24648): Floating_Log 1 : 移动 X:0.36363636363636687 Y:149.63636363636363 47 | I/flutter (24648): Floating_Log 1 : 移动 X:0.7272727272727337 Y:149.63636363636363 48 | I/flutter (24648): Floating_Log 1 : 移动 X:1.0909090909091006 Y:149.27272727272725 49 | I/flutter (24648): Floating_Log 1 : 移动 X:1.4545454545454675 Y:149.27272727272725 50 | I/flutter (24648): Floating_Log 1 : 移动 X:1.4545454545454675 Y:148.90909090909088 51 | I/flutter (24648): Floating_Log 1 : 移动 X:0.0 Y:145.9999999999999 52 | I/flutter (24648): Floating_Log 1 : 移动结束 X:0.0 Y:145.9999999999999 53 | ``` 54 | 55 | ```dart 56 | I/flutter (24648): Floating_Log 1645091422285 : 按下 X:342.72727272727275 Y:480.9090909090909 57 | I/flutter (24648): Floating_Log 1645091422285 : 抬起 X:342.72727272727275 Y:480.9090909090909 58 | I/flutter (24648): Floating_Log 1645091422285 : 移动 X:342.72727272727275 Y:480.5454545454545 59 | I/flutter (24648): Floating_Log 1645091422285 : 移动 X:342.72727272727275 Y:480.18181818181813 60 | I/flutter (24648): Floating_Log 1645091422285 : 移动 X:342.72727272727275 Y:479.81818181818176 61 | I/flutter (24648): Floating_Log 1645091422285 : 移动 X:342.72727272727275 Y:479.4545454545454 62 | I/flutter (24648): Floating_Log 1645091422285 : 移动 X:342.72727272727275 Y:479.090909090909 63 | I/flutter (24648): Floating_Log 1645091422285 : 移动 X:342.72727272727275 Y:478.72727272727263 64 | ``` 65 | 66 | 67 | 68 | ### 使用方式 69 | 70 | #### 可选参数 71 | 72 | ```dart 73 | ///[child]需要悬浮的 widget 74 | ///[slideType],可参考[FloatingSlideType] 75 | /// 76 | ///[top],[left],[left],[bottom] 对应 [slideType], 77 | ///例如设置[slideType]为[FloatingSlideType.onRightAndBottom],则需要传入[bottom]和[right] 78 | /// 79 | ///[isPosCache]启用之后当调用之后 [Floating.close] 重新调用 [Floating.open] 后会保持之前的位置 80 | ///[isSnapToEdge]是否自动吸附边缘,默认为 true ,请注意,移动默认是有透明动画的,如需要关闭透明度动画, 81 | ///请修改 [moveOpacity]为 1 82 | ///[slideTopHeight] 滑动边界控制,可滑动到顶部的距离 83 | ///[slideBottomHeight] 滑动边界控制,可滑动到底部的距离 84 | ///[slideStopType] 移动后回弹停靠的位置 [lideStopType] 85 | Floating( 86 | Widget child, { 87 | FloatingSlideType slideType = FloatingSlideType.onRightAndBottom, 88 | double? top, 89 | double? left, 90 | double? right, 91 | double? bottom, 92 | double moveOpacity = 0.3, 93 | bool isPosCache = true, 94 | bool isShowLog = true, 95 | bool isSnapToEdge = true, 96 | this.slideTopHeight = 0, 97 | this.slideBottomHeight = 0, 98 | SlideStopType slideStopType = SlideStopType.slideStopAutoType, 99 | }) 100 | ``` 101 | 102 | #### 全局悬浮窗 103 | 104 | 全局的悬浮窗通过 FloatingManager 进行管理 105 | 106 | - 创建悬浮窗 107 | 108 | ```dart 109 | floatingOne = floatingManager.createFloating( 110 | "1",///key 111 | Floating(const FloatingIncrement(), 112 | slideType: FloatingSlideType.onLeftAndTop, 113 | isShowLog: false, 114 | slideBottomHeight: 100)); 115 | ``` 116 | 117 | - 通过 FloatingManager 获取 key 对应的悬浮窗 118 | 119 | ```dart 120 | floatingManager.getFloating("1"); 121 | ``` 122 | 123 | - 关闭 key 对应的悬浮窗 124 | 125 | ```dart 126 | floatingManager.closeFloating("1"); 127 | ``` 128 | 129 | - 关闭所有悬浮窗 130 | 131 | ```dart 132 | floatingManager.closeAllFloating(); 133 | ``` 134 | 135 | - ..... 136 | 137 | #### 单悬浮窗创建 138 | 139 | 单悬浮窗可用于某个页面中,页面退出后关闭即可。 140 | 141 | ```dart 142 | class CustomPage extends StatefulWidget { 143 | const CustomPage({Key? key}) : super(key: key); 144 | 145 | @override 146 | _CustomPageState createState() => _CustomPageState(); 147 | } 148 | 149 | class _CustomPageState extends State { 150 | late Floating floating; 151 | 152 | @override 153 | void initState() { 154 | super.initState(); 155 | floating = Floating(const FloatingIncrement(), 156 | slideType: FloatingSlideType.onLeftAndTop, 157 | isShowLog: false, 158 | slideBottomHeight: 100); 159 | floating.open(); 160 | } 161 | 162 | @override 163 | Widget build(BuildContext context) { 164 | return Scaffold( 165 | appBar: AppBar( 166 | title: const Text("功能页面"), 167 | ), 168 | body: Container(), 169 | ); 170 | } 171 | 172 | @override 173 | void dispose() { 174 | floating.close(); 175 | super.dispose(); 176 | } 177 | } 178 | ``` 179 | 180 | 181 | #### 添加悬浮窗各项回调 182 | 183 | ```dart 184 | var oneListener = FloatingListener() 185 | ..openListener = () { 186 | print('显示1'); 187 | } 188 | ..closeListener = () { 189 | print('关闭1'); 190 | } 191 | ..downListener = (x, y) { 192 | print('按下1'); 193 | } 194 | ..upListener = (x, y) { 195 | print('抬起1'); 196 | } 197 | ..moveListener = (x, y) { 198 | print('移动 $x $y 1'); 199 | } 200 | ..moveEndListener = (x, y) { 201 | print('移动结束 $x $y 1'); 202 | }; 203 | floatingOne.addFloatingListener(oneListener); 204 | ``` 205 | 206 | ### 其他使用方式 207 | 208 | - [使用方式](https://github.com/LvKang-insist/Floating/blob/master/lib/main.dart) 209 | - [悬浮窗对应方法](https://github.com/LvKang-insist/Floating/blob/master/lib/floating/floating.dart) 210 | - [全局悬浮窗管理对应方法](https://github.com/LvKang-insist/Floating/blob/master/lib/floating/manager/floating_manager.dart) 211 | - [修改悬浮窗位置](https://github.com/LvKang-insist/Floating/blob/master/lib/floating/manager/scroll_position_manager.dart) 212 | 213 | 214 | 215 | ### 最后 216 | 217 | 如果您在使用过程中有任何问题可直接发送邮件`lv345_y@163.com` 或者直接提 `Issues`,也可以直接加我wx:lv__345 218 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | 5 | # https://dart.dev/guides/language/analysis-options 6 | -------------------------------------------------------------------------------- /floating.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /floating_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /lib/button_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// @name:button_widget 4 | /// @package:widget 5 | /// @author:345 QQ:1831712732 6 | /// @time:2021/05/11 11:19 7 | /// @des: 8 | /// 9 | class ButtonWidget extends StatelessWidget { 10 | final Function callback; 11 | final EdgeInsets? margin; 12 | final String text; 13 | final double? height; 14 | final Color? background; 15 | final FontWeight? fontWeight; 16 | 17 | const ButtonWidget(this.text, this.callback, 18 | {this.margin, 19 | this.height, 20 | this.background = Colors.blue, 21 | this.fontWeight, 22 | Key? key}) 23 | : super(key: key); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Container( 28 | margin: margin ?? const EdgeInsets.all(16), 29 | height: height ?? 44, 30 | child: InkWell( 31 | child: Container( 32 | child: Text(text, 33 | style: TextStyle( 34 | color: Colors.white, fontSize: 16, fontWeight: fontWeight)), 35 | decoration: BoxDecoration( 36 | borderRadius: BorderRadius.circular(4), color: background), 37 | width: double.infinity, 38 | alignment: Alignment.center, 39 | height: 44, 40 | ), 41 | onTap: () => callback(), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/floating/assist/Point.dart: -------------------------------------------------------------------------------- 1 | /// @name:floating_manager 2 | /// @package: 3 | /// @author:345 QQ:1831712732 4 | /// @time:2022/02/11 14:50 5 | /// @des: 6 | 7 | class Point { 8 | T x; 9 | T y; 10 | 11 | Point(this.x, this.y); 12 | } 13 | -------------------------------------------------------------------------------- /lib/floating/assist/animation_helper.dart: -------------------------------------------------------------------------------- 1 | /// @name:animation_helper 2 | /// @package: 3 | /// @author:345 QQ:1831712732 4 | /// @time:2022/02/16 21:55 5 | /// @des: 6 | 7 | class FAnimationHelper {} 8 | -------------------------------------------------------------------------------- /lib/floating/assist/floating_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_floating/floating/assist/Point.dart'; 2 | 3 | import 'floating_slide_type.dart'; 4 | 5 | /// @name:floating_data 6 | /// @package: 7 | /// @author:345 QQ:1831712732 8 | /// @time:2022/02/10 17:35 9 | /// @des:悬浮窗数据记录 10 | 11 | class FloatingData { 12 | double? left; 13 | double? top; 14 | double? right; 15 | double? bottom; 16 | 17 | double snapToEdgeSpace = 0; 18 | Point? point; 19 | 20 | FloatingSlideType slideType; 21 | bool dynamicSlideType = false; 22 | 23 | FloatingData( 24 | this.slideType, { 25 | this.left, 26 | this.top, 27 | this.right, 28 | this.bottom, 29 | this.point, 30 | this.snapToEdgeSpace = 0, 31 | this.dynamicSlideType = false, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /lib/floating/assist/floating_slide_type.dart: -------------------------------------------------------------------------------- 1 | /// @name:slide_enum 2 | /// @package: 3 | /// @author:345 QQ:1831712732 4 | /// @time:2022/02/10 22:30 5 | /// @des:悬浮窗坐标的起始点位置 6 | 7 | enum FloatingSlideType { 8 | ///左上 9 | onLeftAndTop, 10 | 11 | ///左下 12 | onLeftAndBottom, 13 | 14 | ///右上 15 | onRightAndTop, 16 | 17 | ///右下 18 | onRightAndBottom, 19 | 20 | ///根据 x,y 确定位置 21 | onPoint, 22 | } 23 | -------------------------------------------------------------------------------- /lib/floating/assist/slide_stop_type.dart: -------------------------------------------------------------------------------- 1 | /// @name:slide_stop_type 2 | /// @package: 3 | /// @author:345 QQ:1831712732 4 | /// @time:2023/03/30 10:25 5 | /// @des:拖动后吸附在哪一侧 6 | 7 | enum SlideStopType { 8 | ///吸附在左侧 9 | slideStopLeftType, 10 | 11 | ///吸附在右侧 12 | slideStopRightType, 13 | 14 | ///吸附在更近的一侧 15 | slideStopAutoType, 16 | } 17 | -------------------------------------------------------------------------------- /lib/floating/control/common_control.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_floating/floating/assist/Point.dart'; 2 | 3 | /// @name:common_control 4 | /// @package: 5 | /// @author:345 QQ:1831712732 6 | /// @time:2023/04/17 20:29 7 | /// @des: 通用的回调 8 | 9 | class CommonControl { 10 | Function(bool isHide)? _hideControl; 11 | Function(bool isScroll)? _startScroll; 12 | Point point = Point(0, 0); 13 | Function(Point point)? _floatingPoint; 14 | bool _initIsScroll = false; 15 | 16 | ///设置 Floating 位置监听 17 | setFloatingPoint(Function(Point point) floatingPoint) { 18 | _floatingPoint = floatingPoint; 19 | } 20 | 21 | /// 获取 Floating 位置 22 | Point getFloatingPoint() { 23 | _floatingPoint?.call(point); 24 | return point; 25 | } 26 | 27 | ///设置初始化时是否可以滑动 28 | setInitIsScroll(bool initIsScroll) { 29 | _initIsScroll = initIsScroll; 30 | } 31 | 32 | ///获取初始化时是否可以滑动状态 33 | bool getInitIsScroll() { 34 | return _initIsScroll; 35 | } 36 | 37 | ///设置是否滑动监听 38 | setIsStartScrollListener(Function(bool isScroll) fun) { 39 | _startScroll = fun; 40 | } 41 | 42 | ///设置是否滑动 43 | setIsStartScroll(bool isScroll) { 44 | _startScroll?.call(isScroll); 45 | } 46 | 47 | ///设置隐藏状态 48 | setFloatingHide(bool isHide) { 49 | _hideControl?.call(isHide); 50 | } 51 | 52 | ///设置隐藏监听 53 | setHideControlListener(Function(bool isHide) hideControl) { 54 | _hideControl = hideControl; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/floating/control/scroll_position_control.dart: -------------------------------------------------------------------------------- 1 | /// @name:change_position_listener 2 | /// @package: 3 | /// @author:345 QQ:1831712732 4 | /// @time:2023/03/28 17:47 5 | /// @des: 6 | class ScrollPositionControl { 7 | ///滑动时间 8 | int timeMillis = 300; 9 | 10 | ///从当前滑动到距离顶部[top]的位置 11 | Function(double top)? _scrollTop; 12 | 13 | ///从当前滑动到距离左边[left]的位置 14 | Function(double left)? _scrollLeft; 15 | 16 | ///从当前滑动到距离右边[right]的位置 17 | Function(double right)? _scrollRight; 18 | 19 | ///从当前滑动到距离底部[bottom]的位置 20 | Function(double bottom)? _scrollBottom; 21 | 22 | ///从当前滑动到距离顶部[top]和左边[left]的位置 23 | Function(double top, double left)? _scrollTopLeft; 24 | 25 | ///从当前滑动到距离顶部[top]和右边[right]的位置 26 | Function(double bottom, double left)? _scrollBottomLeft; 27 | 28 | ///从当前滑动到距离底部[bottom]和右边[right]的位置 29 | Function(double top, double right)? _scrollTopRight; 30 | 31 | ///从当前滑动到距离底部[bottom]和右边[right]的位置 32 | Function(double bottom, double right)? _scrollBottomRight; 33 | 34 | setScrollTime(int timeMillis) { 35 | this.timeMillis = timeMillis; 36 | } 37 | 38 | getScrollTime() { 39 | return timeMillis; 40 | } 41 | 42 | setScrollTop(Function(double top) scrollTop) { 43 | _scrollTop = scrollTop; 44 | } 45 | 46 | setScrollLeft(Function(double left) scrollLeft) { 47 | _scrollLeft = scrollLeft; 48 | } 49 | 50 | setScrollRight(Function(double right) scrollRight) { 51 | _scrollRight = scrollRight; 52 | } 53 | 54 | setScrollBottom(Function(double bottom) scrollBottom) { 55 | _scrollBottom = scrollBottom; 56 | } 57 | 58 | setScrollTopLeft(Function(double top, double left) scrollTopLeft) { 59 | _scrollTopLeft = scrollTopLeft; 60 | } 61 | 62 | setScrollBottomLeft(Function(double bottom, double left) scrollBottomLeft) { 63 | _scrollBottomLeft = scrollBottomLeft; 64 | } 65 | 66 | setScrollTopRight(Function(double top, double right) scrollTopRight) { 67 | _scrollTopRight = scrollTopRight; 68 | } 69 | 70 | setScrollBottomRight( 71 | Function(double bottom, double right) scrollBottomRight) { 72 | _scrollBottomRight = scrollBottomRight; 73 | } 74 | 75 | scrollTop(double top) { 76 | _scrollTop?.call(top); 77 | } 78 | 79 | scrollLeft(double left) { 80 | _scrollLeft?.call(left); 81 | } 82 | 83 | scrollRight(double right) { 84 | _scrollRight?.call(right); 85 | } 86 | 87 | scrollBottom(double bottom) { 88 | _scrollBottom?.call(bottom); 89 | } 90 | 91 | scrollTopLeft(double top, double left) { 92 | _scrollTopLeft?.call(top, left); 93 | } 94 | 95 | scrollBottomLeft(double bottom, double left) { 96 | _scrollBottomLeft?.call(bottom, left); 97 | } 98 | 99 | scrollTopRight(double top, double right) { 100 | _scrollTopRight?.call(top, right); 101 | } 102 | 103 | scrollBottomRight(double bottom, double right) { 104 | _scrollBottomRight?.call(bottom, right); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/floating/floating.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_floating/floating/assist/Point.dart'; 3 | import 'package:flutter_floating/floating/assist/slide_stop_type.dart'; 4 | import 'package:flutter_floating/floating/control/common_control.dart'; 5 | import 'package:flutter_floating/floating/listener/event_listener.dart'; 6 | import 'package:flutter_floating/floating/manager/scroll_position_manager.dart'; 7 | import 'package:flutter_floating/floating/utils/floating_log.dart'; 8 | import 'package:flutter_floating/floating/view/floating_view.dart'; 9 | 10 | import 'assist/floating_data.dart'; 11 | import 'assist/floating_slide_type.dart'; 12 | import 'control/scroll_position_control.dart'; 13 | 14 | /// @name:floating 15 | /// @package: 16 | /// @author:345 QQ:1831712732 17 | /// @time:2022/02/10 14:23 18 | /// @des: 19 | 20 | class Floating { 21 | late OverlayEntry _overlayEntry; 22 | 23 | late FloatingView _floatingView; 24 | 25 | late FloatingData _floatingData; 26 | 27 | late ScrollPositionControl _scrollPositionControl; 28 | late ScrollPositionManager _scrollPositionManager; 29 | 30 | late CommonControl _commonControl; 31 | 32 | final List _listener = []; 33 | 34 | late FloatingLog _log; 35 | String logKey = ""; 36 | 37 | ///是否真正显示 38 | bool get isShowing => _isShowing; 39 | bool _isShowing = false; 40 | 41 | ///[child]需要悬浮的 widget 42 | ///[slideType],悬浮窗坐标的起始点位置,可参考[FloatingSlideType] 43 | /// 44 | ///[top],[left],[left],[bottom],[point] 对应 [slideType],设置与起始点的距离 45 | ///例如设置[slideType]为[FloatingSlideType.onRightAndBottom],则需要传入[bottom]和[right] 46 | ///设置 [slideType]为 [FloatingSlideType.onPoint] 则需要传入 [point] 47 | /// 48 | ///[isPosCache]是否在调用 [Floating.open] 时,保持上一次 [Floating.close] 前的位置 49 | ///[isSnapToEdge]是否自动吸附左右边缘,默认为 true 50 | ///请注意,移动默认是有透明动画的,如需要关闭透明度动画,请修改 [moveOpacity]为 1 51 | ///[isStartScroll] 是否允许拖动悬浮窗,默认为 true 52 | ///[slideTopHeight] 拖动范围限制,与顶部的最小距离(可设为负数) 53 | ///[slideBottomHeight] 拖动范围限制,与底部的最小距离(可设为负数) 54 | ///[snapToEdgeSpace] 吸附后回弹至与边缘的距离,不开启吸附则用于范围限制(可设为负数) 55 | ///[edgeSpeed] 吸附边缘的速度,默认 250,越大越快 56 | ///[slideStopType] 拖动后吸附在哪一侧 57 | Floating( 58 | Widget child, { 59 | FloatingSlideType slideType = FloatingSlideType.onRightAndBottom, 60 | double? top, 61 | double? left, 62 | double? right, 63 | double? bottom, 64 | Point? point, 65 | double moveOpacity = 0.3, 66 | bool isPosCache = true, 67 | bool isShowLog = true, 68 | bool isSnapToEdge = true, 69 | bool isStartScroll = true, 70 | double slideTopHeight = 0, 71 | double slideBottomHeight = 0, 72 | double snapToEdgeSpace = 0, 73 | int edgeSpeed = 250, 74 | SlideStopType slideStopType = SlideStopType.slideStopAutoType, 75 | }) { 76 | _floatingData = FloatingData(slideType, 77 | left: left, 78 | right: right, 79 | top: top, 80 | bottom: bottom, 81 | point: point, 82 | snapToEdgeSpace: snapToEdgeSpace); 83 | _log = FloatingLog(isShowLog); 84 | _commonControl = CommonControl(); 85 | _commonControl.setInitIsScroll(isStartScroll); 86 | _scrollPositionControl = ScrollPositionControl(); 87 | _scrollPositionManager = ScrollPositionManager(_scrollPositionControl); 88 | _floatingView = FloatingView( 89 | child, 90 | _floatingData, 91 | isPosCache, 92 | isSnapToEdge, 93 | _listener, 94 | _scrollPositionControl, 95 | _commonControl, 96 | _log, 97 | moveOpacity: moveOpacity, 98 | slideTopHeight: slideTopHeight, 99 | slideBottomHeight: slideBottomHeight, 100 | slideStopType: slideStopType, 101 | edgeSpeed: edgeSpeed, 102 | ); 103 | } 104 | 105 | ///打开悬浮窗 106 | ///此方法配合 [close]方法进行使用,调用[close]之后在调用此方法会丢失 Floating 状态 107 | ///否则请使用 [hideFloating] 进行隐藏,使用 [showFloating]进行显示,而不是使用 [close] 108 | open(BuildContext context) { 109 | if (_isShowing) return; 110 | _overlayEntry = OverlayEntry(builder: (context) { 111 | return _floatingView; 112 | }); 113 | Overlay.of(context).insert(_overlayEntry); 114 | _isShowing = true; 115 | _notifyOpen(); 116 | } 117 | 118 | ///关闭悬浮窗 119 | close() { 120 | if (!_isShowing) return; 121 | _overlayEntry.remove(); 122 | _isShowing = false; 123 | _notifyClose(); 124 | } 125 | 126 | ///隐藏悬浮窗,保留其状态 127 | ///只有在悬浮窗显示的状态下才可以使用,否则调用无效 128 | hideFloating() { 129 | if (!_isShowing) return; 130 | _commonControl.setFloatingHide(true); 131 | _isShowing = false; 132 | _notifyHideFloating(); 133 | } 134 | 135 | ///显示悬浮窗,恢复其状态 136 | ///只有在悬浮窗是隐藏的状态下才可以使用,否则调用无效 137 | showFloating() { 138 | if (_isShowing) return; 139 | _commonControl.setFloatingHide(false); 140 | _isShowing = true; 141 | _notifyShowFloating(); 142 | } 143 | 144 | ///添加监听 145 | addFloatingListener(FloatingEventListener listener) { 146 | _listener.contains(listener) ? null : _listener.add(listener); 147 | } 148 | 149 | ///设置是否启动悬浮窗滑动 150 | ///[isScroll] true 表示启动,否则关闭 151 | setIsStartScroll(bool isScroll) { 152 | _commonControl.setIsStartScroll(isScroll); 153 | } 154 | 155 | ///设置 [FloatingLog] 标识 156 | setLogKey(String key) { 157 | _log.logKey = key; 158 | } 159 | 160 | /// 获取滑动管理 161 | ScrollPositionManager getScrollManager() { 162 | return _scrollPositionManager; 163 | } 164 | 165 | /// 获取悬浮位置[Size.width]表示距离left距离,[Size.height]表示top距离 166 | Point getFloatingPoint() { 167 | return _commonControl.getFloatingPoint(); 168 | } 169 | 170 | _notifyClose() { 171 | _log.log("关闭"); 172 | for (var listener in _listener) { 173 | listener.closeListener?.call(); 174 | } 175 | } 176 | 177 | _notifyOpen() { 178 | _log.log("打开"); 179 | for (var listener in _listener) { 180 | listener.openListener?.call(); 181 | } 182 | } 183 | 184 | _notifyHideFloating() { 185 | _log.log("隐藏"); 186 | for (var listener in _listener) { 187 | listener.hideFloatingListener?.call(); 188 | } 189 | } 190 | 191 | _notifyShowFloating() { 192 | _log.log("显示"); 193 | for (var listener in _listener) { 194 | listener.showFloatingListener?.call(); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /lib/floating/listener/event_listener.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter_floating/floating/assist/Point.dart'; 3 | 4 | /// @name:FloatingListener 5 | /// @package: 6 | /// @author:345 QQ:1831712732 7 | /// @time:2022/02/11 23:16 8 | /// @des: 9 | 10 | class FloatingEventListener { 11 | ///打开悬浮窗 12 | Function? openListener; 13 | 14 | ///关闭悬浮窗 15 | Function? closeListener; 16 | 17 | ///影藏悬浮窗 18 | Function? hideFloatingListener; 19 | 20 | ///显示悬浮窗 21 | Function? showFloatingListener; 22 | 23 | ///手指按下 24 | Function(Point)? downListener; 25 | 26 | ///手指抬起 27 | Function(Point)? upListener; 28 | 29 | ///手指移动 30 | Function(Point)? moveListener; 31 | 32 | ///手指移动结束 33 | Function(Point)? moveEndListener; 34 | } 35 | -------------------------------------------------------------------------------- /lib/floating/manager/floating_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | import '../floating.dart'; 4 | 5 | /// @name:floating_manager 6 | /// @package: 7 | /// @author:345 QQ:1831712732 8 | /// @time:2022/02/11 14:50 9 | /// @des:[Floating] 管理者 10 | 11 | FloatingManager floatingManager = FloatingManager(); 12 | 13 | class FloatingManager { 14 | FloatingManager._single(); 15 | 16 | static final FloatingManager _manager = FloatingManager._single(); 17 | 18 | factory FloatingManager() => _manager; 19 | 20 | final Map _floatingCache = {}; 21 | 22 | static TransitionBuilder init({TransitionBuilder? builder}) { 23 | return (BuildContext context, Widget? child) { 24 | if (builder != null) { 25 | return builder(context, child); 26 | } 27 | return Container(child: child); 28 | }; 29 | } 30 | 31 | ///创建一个可全局管理的 [Floating] 32 | Floating createFloating(Object key, Floating floating) { 33 | bool contains = _floatingCache.containsKey(key); 34 | if (!contains) { 35 | _floatingCache[key] = floating..setLogKey(key.toString()); 36 | } 37 | return _floatingCache[key]!; 38 | } 39 | 40 | ///根据 [key] 拿到对应的 [Floating] 41 | Floating getFloating(Object key) { 42 | return _floatingCache[key]!; 43 | } 44 | 45 | ///查询 [key] 对应的 [Floating] 是否存在 46 | bool containsFloating(Object key) { 47 | return _floatingCache.containsKey(key); 48 | } 49 | 50 | ///关闭 [key] 对应的 [Floating] 51 | closeFloating(Object key) { 52 | var floating = _floatingCache[key]; 53 | floating?.close(); 54 | _floatingCache.remove(key); 55 | } 56 | 57 | ///关闭所有的 [Floating] 58 | closeAllFloating() { 59 | _floatingCache.forEach((key, value) => value.close()); 60 | _floatingCache.clear(); 61 | } 62 | 63 | ///悬浮窗数量 64 | int floatingSize() { 65 | return _floatingCache.length; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/floating/manager/scroll_position_manager.dart: -------------------------------------------------------------------------------- 1 | import '../control/scroll_position_control.dart'; 2 | 3 | /// @name:scroll_position_manager 4 | /// @package: 5 | /// @author:345 QQ:1831712732 6 | /// @time:2023/03/29 11:08 7 | /// @des: 手动控制悬浮窗滑动管理 ,通过[Floating.scrollManager]获取 8 | 9 | class ScrollPositionManager { 10 | final ScrollPositionControl _control; 11 | 12 | ScrollPositionManager(this._control); 13 | 14 | setScrollTime(int timeMillis) { 15 | _control.setScrollTime(timeMillis); 16 | } 17 | 18 | ///从当前滑动到距离顶部[top]的位置 19 | scrollTop(double top) { 20 | _control.scrollTop(top); 21 | } 22 | 23 | ///从当前滑动到距离左边[left]的位置 24 | scrollLeft(double left) { 25 | _control.scrollLeft(left); 26 | } 27 | 28 | ///从当前滑动到距离右边[right]的位置 29 | scrollRight(double right) { 30 | _control.scrollRight(right); 31 | } 32 | 33 | ///从当前滑动到距离底部[bottom]的位置 34 | scrollBottom(double bottom) { 35 | _control.scrollBottom(bottom); 36 | } 37 | 38 | ///从当前滑动到距离顶部[top]和左边[left]的位置 39 | scrollTopLeft(double top, double left) { 40 | _control.scrollTopLeft(top, left); 41 | } 42 | 43 | ///从当前滑动到距离顶部[top]和右边[right]的位置 44 | scrollTopRight(double top, double right) { 45 | _control.scrollTopRight(top, right); 46 | } 47 | 48 | ///从当前滑动到距离底部[bottom]和右边[right]的位置 49 | scrollBottomLeft(double bottom, double left) { 50 | _control.scrollBottomLeft(bottom, left); 51 | } 52 | 53 | ///从当前滑动到距离底部[bottom]和右边[right]的位置 54 | scrollBottomRight(double bottom, double right) { 55 | _control.scrollBottomRight(bottom, right); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/floating/utils/floating_log.dart: -------------------------------------------------------------------------------- 1 | /// @name:log 2 | /// @package: 3 | /// @author:345 QQ:1831712732 4 | /// @time:2022/02/14 21:57 5 | /// @des: 6 | 7 | class FloatingLog { 8 | FloatingLog(bool isShowLog) { 9 | init(isShowLog); 10 | } 11 | 12 | static const _separator = "="; 13 | static const _split = 14 | "$_separator$_separator$_separator$_separator$_separator$_separator$_separator$_separator$_separator"; 15 | static const _title = "Floating_Log"; 16 | static const int _limitLength = 800; 17 | static String _startLine = "$_split$_title$_split"; 18 | static String _endLine = "$_split$_separator$_separator$_separator$_split"; 19 | bool isShowLog = false; 20 | String logKey = ""; 21 | 22 | void init(bool isShowLog) { 23 | this.isShowLog = isShowLog; 24 | _startLine = "$_split$_title$_split"; 25 | var endLineStr = StringBuffer(); 26 | var cnCharReg = RegExp("[\u4e00-\u9fa5]"); 27 | for (int i = 0; i < _startLine.length; i++) { 28 | if (cnCharReg.stringMatch(_startLine[i]) != null) { 29 | endLineStr.write(_separator); 30 | } 31 | endLineStr.write(_separator); 32 | } 33 | _endLine = endLineStr.toString(); 34 | } 35 | 36 | log(dynamic obj) { 37 | if (!isShowLog) return; 38 | if (obj.toString().length < _limitLength) { 39 | _log(obj.toString()); 40 | } else { 41 | _splitLog(obj); 42 | } 43 | } 44 | 45 | void _log(String msg) { 46 | print("$_title $logKey : $msg"); 47 | } 48 | 49 | void _splitLog(String msg) { 50 | var outStr = StringBuffer(); 51 | _log(""); 52 | for (var index = 0; index < msg.length; index++) { 53 | outStr.write(msg[index]); 54 | if (index % _limitLength == 0 && index != 0) { 55 | print(outStr); 56 | outStr.clear(); 57 | var lastIndex = index + 1; 58 | if (msg.length - lastIndex < _limitLength) { 59 | var remainderStr = msg.substring(lastIndex, msg.length); 60 | print(remainderStr); 61 | break; 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/floating/view/floating_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/scheduler.dart'; 4 | import 'package:flutter_floating/floating/assist/slide_stop_type.dart'; 5 | import 'package:flutter_floating/floating/control/common_control.dart'; 6 | 7 | import '../assist/Point.dart'; 8 | import '../assist/floating_data.dart'; 9 | import '../assist/floating_slide_type.dart'; 10 | import '../control/scroll_position_control.dart'; 11 | import '../listener/event_listener.dart'; 12 | import '../utils/floating_log.dart'; 13 | import 'dart:math'; 14 | 15 | /// @name:floating 16 | /// @package: 17 | /// @author:345 QQ:1831712732 18 | /// @time:2022/02/09 22:33 19 | /// @des:悬浮窗容器 20 | 21 | class FloatingView extends StatefulWidget { 22 | final Widget child; 23 | final FloatingData floatingData; 24 | final bool isPosCache; 25 | final bool isSnapToEdge; 26 | final List _listener; 27 | final ScrollPositionControl _scrollPositionControl; 28 | final FloatingLog _log; 29 | final double slideTopHeight; 30 | final double slideBottomHeight; 31 | final double moveOpacity; // 悬浮组件透明度 32 | final SlideStopType slideStopType; 33 | final CommonControl _commonControl; 34 | final int edgeSpeed; //吸附边缘速度 35 | 36 | const FloatingView( 37 | this.child, 38 | this.floatingData, 39 | this.isPosCache, 40 | this.isSnapToEdge, 41 | this._listener, 42 | this._scrollPositionControl, 43 | this._commonControl, 44 | this._log, 45 | {Key? key, 46 | this.slideTopHeight = 0, 47 | this.slideBottomHeight = 0, 48 | this.moveOpacity = 0.3, 49 | this.edgeSpeed = 0, 50 | this.slideStopType = SlideStopType.slideStopAutoType}) 51 | : super(key: key); 52 | 53 | @override 54 | _FloatingViewState createState() => _FloatingViewState(); 55 | } 56 | 57 | class _FloatingViewState extends State 58 | with TickerProviderStateMixin { 59 | final _floatingGlobalKey = GlobalKey(); 60 | RenderBox? renderBox; 61 | 62 | double _top = 0; //悬浮窗距屏幕或父组件顶部的距离 63 | double _left = 0; //悬浮窗距屏幕或父组件左侧的距离 64 | 65 | late FloatingData _floatingData; 66 | 67 | final double _defaultWidth = 100; //默认宽度 68 | 69 | final double _defaultHeight = 100; //默认高度 70 | 71 | double _fWidth = 0; //悬浮窗宽度 72 | double _fHeight = 0; //悬浮窗高度 73 | 74 | double _parentWidth = 0; //记录屏幕或者父组件宽度 75 | double _parentHeight = 0; //记录屏幕或者父组件高度 76 | 77 | /// 对于进入或退出画中画(pip),以及分屏/折叠屏/可变小窗等场景下 78 | /// 需要处理屏幕或父组件尺寸(暂称 外尺寸)与悬浮窗的位置问题 79 | 80 | /// 在保证预留距离的前提下,当外尺寸变化时,悬浮窗位置需要按比例调整,以尽量显示 81 | /// 即 边距 与 剩余尺寸 之比。剩余尺寸 = 外尺寸 - 预留距离 - 预留范围内的悬浮窗高度。 82 | /// 正常情况下,初始化、悬浮窗移动、悬浮窗尺寸变化时,调用 [_setPositionToRemainRatio] 83 | /// 更新时,如果剩余尺寸 ≤ 0,不计算、不改变该比例,以便外尺寸恢复时复原悬浮窗位置。 84 | /// 且若此时比例为空,则根据FloatingSlideType赋予初值0或1 85 | /// 86 | /// 当外尺寸变化时,调用 [_calcNewPositionByRatio] 计算坐标,以使悬浮窗尽可能显示 87 | 88 | // 顶部边距与剩余高度之比 89 | /// _top / (_parentHeight - slideTopHeight - slideBottomHeight - heightInRange) 90 | double? _topToRemainHeightRatio; 91 | 92 | // 左侧边距与剩余宽度之比 93 | /// _left / (_parentWidth - snapToEdgeSpace * 2 - widthInRange) 94 | double? _leftToRemainWidthRatio; 95 | 96 | double _opacity = 1.0; // 悬浮组件透明度 97 | 98 | bool _isInitPosition = false; 99 | 100 | late Widget _contentWidget; 101 | 102 | late AnimationController _slideController; //动画控制器 103 | late Animation _slideAnimation; //动画 104 | late AnimationController _scrollController; //动画控制器 105 | 106 | bool isHide = false; 107 | bool _isStartScroll = true; //是否启动悬浮窗滑动 108 | 109 | @override 110 | void initState() { 111 | super.initState(); 112 | _floatingData = widget.floatingData; 113 | widget._commonControl.setHideControlListener( 114 | (isHide) => setState(() => this.isHide = isHide)); 115 | _isStartScroll = widget._commonControl.getInitIsScroll(); 116 | widget._commonControl 117 | .setIsStartScrollListener((isScroll) => _isStartScroll = isScroll); 118 | widget._commonControl.setFloatingPoint((Point point) { 119 | point.x = _left; 120 | point.y = _top; 121 | }); 122 | _contentWidget = _content(); 123 | _slideController = AnimationController( 124 | duration: const Duration(milliseconds: 0), vsync: this); 125 | _slideAnimation = Tween(begin: 0.0, end: 0.0).animate(_slideController); 126 | _scrollController = AnimationController( 127 | duration: const Duration(milliseconds: 0), vsync: this); 128 | _setScrollControl(); 129 | setState(() { 130 | _setParentHeightAndWidget(); 131 | _resetFloatingSize(); 132 | _initPosition(); 133 | }); 134 | WidgetsBinding.instance.addPostFrameCallback((_) { 135 | _setPositionToRemainRatio(); 136 | }); 137 | } 138 | 139 | @override 140 | Widget build(BuildContext context) { 141 | return Stack( 142 | children: [ 143 | Positioned( 144 | left: _left, 145 | top: _top, 146 | child: AnimatedOpacity( 147 | opacity: _opacity, 148 | curve: Curves.easeOut, 149 | duration: const Duration(milliseconds: 200), 150 | child: Offstage( 151 | offstage: isHide, 152 | child: OrientationBuilder(builder: (context, orientation) { 153 | _checkScreenChange(); 154 | return Opacity( 155 | child: _contentWidget, 156 | opacity: _isInitPosition ? 1 : 0, 157 | ); 158 | }), 159 | )), 160 | ) 161 | ], 162 | ); 163 | } 164 | 165 | _content() { 166 | return GestureDetector( 167 | behavior: HitTestBehavior.opaque, 168 | onTapDown: (details) => _notifyDown(_left, _top), 169 | onTapCancel: () => _notifyUp(_left, _top), 170 | //滑动 171 | onPanUpdate: (DragUpdateDetails details) { 172 | if (!_checkStartScroll()) return; 173 | _left += details.delta.dx; 174 | _top += details.delta.dy; 175 | _opacity = widget.moveOpacity; 176 | _changePosition(); 177 | _notifyMove(_left, _top); 178 | }, 179 | //滑动结束 180 | onPanEnd: (DragEndDetails details) { 181 | if (!_checkStartScroll()) return; 182 | _changePosition(); 183 | //停止后靠边操作 184 | _animateMovePosition(); 185 | }, 186 | //滑动取消 187 | onPanCancel: () { 188 | if (!_checkStartScroll()) return; 189 | _changePosition(); 190 | }, 191 | child: Container( 192 | key: _floatingGlobalKey, 193 | child: NotificationListener( 194 | onNotification: (notification) { 195 | if (notification is SizeChangedLayoutNotification && 196 | _isFloatingChangeSize()) { 197 | _setParentHeightAndWidget(); 198 | _resetFloatingSize(); 199 | setState(() { 200 | setSlide(); 201 | _setPositionToRemainRatio(); 202 | _saveCacheData(_left, _top); 203 | }); 204 | } 205 | return false; 206 | }, 207 | child: SizeChangedLayoutNotifier(child: widget.child)), 208 | ), 209 | ); 210 | } 211 | 212 | ///floating 宽高是否改变,true 表示改变 213 | bool _isFloatingChangeSize() { 214 | renderBox ??= 215 | _floatingGlobalKey.currentContext?.findRenderObject() as RenderBox?; 216 | var w = renderBox?.size.width ?? _defaultWidth; 217 | var h = renderBox?.size.height ?? _defaultHeight; 218 | return w != _fWidth || h != _fHeight; 219 | } 220 | 221 | _resetFloatingSize() { 222 | renderBox ??= 223 | _floatingGlobalKey.currentContext?.findRenderObject() as RenderBox?; 224 | _fWidth = renderBox?.size.width ?? _defaultWidth; 225 | _fHeight = renderBox?.size.height ?? _defaultHeight; 226 | } 227 | 228 | ///边界判断 229 | _changePosition() { 230 | var type = _floatingData.slideType; 231 | //定义一个左边界; 232 | List leftBorder = [0, _parentWidth - _fWidth]; 233 | // 开启吸附时,_floatingData.snapToEdgeSpace为负值则扩展边界,为正由回弹处理 234 | // 未开启吸附时,_floatingData.snapToEdgeSpace直接作为边界 235 | if (_floatingData.snapToEdgeSpace < 0 || !widget.isSnapToEdge) { 236 | leftBorder[0] += _floatingData.snapToEdgeSpace; 237 | leftBorder[1] -= _floatingData.snapToEdgeSpace; 238 | } 239 | // 处理无法移动的情况 240 | if (leftBorder[1] < leftBorder[0]) { 241 | if (type == FloatingSlideType.onRightAndBottom || 242 | type == FloatingSlideType.onRightAndTop) { 243 | leftBorder[0] = leftBorder[1]; 244 | } else { 245 | leftBorder[1] = leftBorder[0]; 246 | } 247 | } 248 | _left = max(leftBorder[0], min(leftBorder[1], _left)); 249 | //定义一个上边界 250 | List topBorder = [ 251 | widget.slideTopHeight, 252 | _parentHeight - _fHeight - widget.slideBottomHeight 253 | ]; 254 | // 处理无法移动的情况 255 | if (topBorder[1] < topBorder[0]) { 256 | if (type == FloatingSlideType.onRightAndBottom || 257 | type == FloatingSlideType.onLeftAndBottom) { 258 | topBorder[0] = topBorder[1]; 259 | } else { 260 | topBorder[1] = topBorder[0]; 261 | } 262 | } 263 | _top = max(topBorder[0], min(topBorder[1], _top)); 264 | setState(() { 265 | _saveCacheData(_left, _top); 266 | }); 267 | } 268 | 269 | ///中线回弹动画 270 | _animateMovePosition() { 271 | if (!widget.isSnapToEdge) { 272 | _recoverOpacity(); 273 | _saveCacheData(_left, _top); 274 | _setPositionToRemainRatio(); 275 | _notifyMoveEnd(_left, _top); 276 | return; 277 | } 278 | double toPositionX = 0; 279 | double needMoveLength = 0; 280 | 281 | void _setPositionToLeft() { 282 | needMoveLength = _left; //靠左边的距离 283 | toPositionX = 0 + _floatingData.snapToEdgeSpace; //回到左边缘距离 284 | } 285 | 286 | void _setPositionToRight() { 287 | needMoveLength = (_parentWidth - _left - _fWidth); //靠右边的距离 288 | toPositionX = 289 | _parentWidth - _fWidth - _floatingData.snapToEdgeSpace; //回到右边缘距离 290 | } 291 | 292 | switch (widget.slideStopType) { 293 | case SlideStopType.slideStopLeftType: 294 | _setPositionToLeft(); 295 | break; 296 | case SlideStopType.slideStopRightType: 297 | _setPositionToRight(); 298 | break; 299 | case SlideStopType.slideStopAutoType: 300 | double centerX = _left + _fWidth / 2.0; //中心点位置 301 | (centerX < _parentWidth / 2) 302 | ? _setPositionToLeft() 303 | : _setPositionToRight(); 304 | break; 305 | } 306 | 307 | //根据滑动距离计算滑动时间 308 | double parent = (needMoveLength / (_parentWidth / 2.0)); 309 | int time = (widget.edgeSpeed * parent).ceil(); 310 | 311 | //执行动画 312 | _animationSlide(_left, toPositionX, time, () { 313 | //恢复透明度 314 | _recoverOpacity(); 315 | _setPositionToRemainRatio(); 316 | _saveCacheData(_left, _top); 317 | //结束后进行通知 318 | _notifyMoveEnd(_left, _top); 319 | }); 320 | } 321 | 322 | _animationSlide( 323 | double left, double toPositionX, int time, Function completed) { 324 | _slideController.dispose(); 325 | _slideController = AnimationController( 326 | duration: Duration(milliseconds: time), vsync: this); 327 | _slideAnimation = 328 | Tween(begin: left, end: toPositionX * 1.0).animate(_slideController); 329 | //回弹动画 330 | _slideAnimation.addListener(() { 331 | _left = _slideAnimation.value.toDouble(); 332 | setState(() { 333 | _saveCacheData(_left, _top); 334 | _notifyMove(_left, _top); 335 | }); 336 | }); 337 | _slideController.addStatusListener((status) { 338 | if (status == AnimationStatus.completed) { 339 | completed.call(); 340 | } 341 | }); 342 | _slideController.forward(); 343 | } 344 | 345 | _setScrollControl() { 346 | var control = widget._scrollPositionControl; 347 | control.setScrollTop((top) => _scrollY(top)); 348 | control.setScrollLeft((left) => _scrollX(left)); 349 | control.setScrollRight((right) => _scrollX(_parentWidth - right - _fWidth)); 350 | control.setScrollBottom( 351 | (bottom) => _scrollY(_parentHeight - bottom - _fHeight)); 352 | 353 | control.setScrollTopLeft((top, left) => _scrollXY(left, top)); 354 | control.setScrollTopRight( 355 | (top, right) => _scrollXY(_parentWidth - right - _fWidth, top)); 356 | control.setScrollBottomLeft( 357 | (bottom, left) => _scrollXY(left, _parentHeight - bottom - _fHeight)); 358 | control.setScrollBottomRight((bottom, right) => _scrollXY( 359 | _parentWidth - right - _fWidth, _parentHeight - bottom - _fHeight)); 360 | } 361 | 362 | _scrollXY(double x, double y) { 363 | if ((x > 0 || y > 0) && (_left != x || _top != y)) { 364 | var control = widget._scrollPositionControl; 365 | _scrollController.dispose(); 366 | _scrollController = AnimationController( 367 | duration: Duration(milliseconds: control.timeMillis), vsync: this); 368 | var t = Tween(begin: _top, end: y).animate(_scrollController); 369 | var l = Tween(begin: _left, end: x).animate(_scrollController); 370 | _scrollController.addListener(() { 371 | _top = t.value.toDouble(); 372 | _left = l.value.toDouble(); 373 | setState(() { 374 | _saveCacheData(_left, _top); 375 | _notifyMove(_left, _top); 376 | }); 377 | }); 378 | _scrollController.forward(); 379 | } 380 | } 381 | 382 | _scrollX(double left) { 383 | if (left > 0 && _left != left) { 384 | var control = widget._scrollPositionControl; 385 | _scrollController.dispose(); 386 | _scrollController = AnimationController( 387 | duration: Duration(milliseconds: control.timeMillis), vsync: this); 388 | var anim = Tween(begin: _left, end: left).animate(_scrollController); 389 | anim.addListener(() { 390 | _left = anim.value.toDouble(); 391 | setState(() { 392 | _saveCacheData(_left, _top); 393 | _notifyMove(_left, _top); 394 | }); 395 | }); 396 | _scrollController.forward(); 397 | } 398 | } 399 | 400 | _scrollY(double top) { 401 | if (top > 0 && _top != top) { 402 | var control = widget._scrollPositionControl; 403 | _scrollController.dispose(); 404 | _scrollController = AnimationController( 405 | duration: Duration(milliseconds: control.timeMillis), vsync: this); 406 | var anim = Tween(begin: _top, end: top).animate(_scrollController); 407 | anim.addListener(() { 408 | _top = anim.value.toDouble(); 409 | setState(() { 410 | _saveCacheData(_left, _top); 411 | _notifyMove(_left, _top); 412 | }); 413 | }); 414 | _scrollController.forward(); 415 | } 416 | } 417 | 418 | ///恢复透明度 419 | _recoverOpacity() { 420 | if (_opacity != 1.0) { 421 | setState(() => _opacity = 1.0); 422 | } 423 | } 424 | 425 | _initPosition() { 426 | //使用缓存 427 | if (widget.isPosCache) { 428 | //如果之前没有缓存数据 429 | if (_floatingData.top == null || _floatingData.left == null) { 430 | setInitSlide(); 431 | } else { 432 | _setCacheData(); 433 | } 434 | } else { 435 | setInitSlide(); 436 | } 437 | _isInitPosition = true; 438 | } 439 | 440 | ///检测是否开启滑动 441 | bool _checkStartScroll() { 442 | return _isStartScroll; 443 | } 444 | 445 | ///判断屏幕是否发生改变 446 | _checkScreenChange() { 447 | //如果屏幕宽高为0,直接退出 448 | if (_parentWidth == 0 || _parentHeight == 0) return; 449 | var width = MediaQuery.of(context).size.width; 450 | var height = MediaQuery.of(context).size.height; 451 | if (width != _parentWidth || height != _parentHeight) { 452 | _parentWidth = width; 453 | _parentHeight = height; 454 | setState(() { 455 | _calcNewPositionByRatio(); 456 | }); 457 | _saveCacheData(_left, _top); 458 | } 459 | } 460 | 461 | // 悬浮窗尺寸变化时,根据起始点重新计算坐标 462 | setSlide() { 463 | // 计算可用高度和宽度 464 | double availableHeight = 465 | _parentHeight - widget.slideTopHeight - widget.slideBottomHeight; 466 | double availableWidth = _parentWidth - _floatingData.snapToEdgeSpace * 2; 467 | // 计算剩余高度和宽度 468 | double remainHeight = availableHeight - _fHeight; 469 | double remainWidth = availableWidth - _fWidth; 470 | // 无法完全显示:从起始点角落边缘开始显示 471 | // 可完全显示,但需要调整:从右下角边缘开始显示 472 | void _adjustBottom() { 473 | double currentBottom = _parentHeight - _top - _fHeight; 474 | // 需要向上调整才能完全显示 475 | if (currentBottom <= widget.slideBottomHeight) { 476 | _top = _parentHeight - widget.slideBottomHeight - _fHeight; 477 | } 478 | } 479 | 480 | void _adjustRight() { 481 | double currentRight = _parentWidth - _left - _fWidth; 482 | // 需要向左调整才能完全显示 483 | if (currentRight <= _floatingData.snapToEdgeSpace) { 484 | _left = _parentWidth - _floatingData.snapToEdgeSpace - _fWidth; 485 | } 486 | } 487 | 488 | void _topSet() { 489 | if (remainHeight <= 0) { 490 | _top = widget.slideTopHeight; 491 | } else { 492 | _adjustBottom(); 493 | } 494 | } 495 | 496 | void _bottomSet() { 497 | if (remainHeight <= 0) { 498 | _top = _parentHeight - widget.slideBottomHeight - _fHeight; 499 | } else { 500 | _adjustBottom(); 501 | } 502 | } 503 | 504 | void _leftSet() { 505 | if (remainWidth <= 0) { 506 | _left = _floatingData.snapToEdgeSpace; 507 | } else { 508 | _adjustRight(); 509 | } 510 | } 511 | 512 | void _rightSet() { 513 | if (remainWidth <= 0) { 514 | _left = _parentWidth - _fWidth - _floatingData.snapToEdgeSpace; 515 | } else { 516 | _adjustRight(); 517 | } 518 | } 519 | 520 | switch (_floatingData.slideType) { 521 | case FloatingSlideType.onLeftAndTop: 522 | case FloatingSlideType.onPoint: 523 | _leftSet(); 524 | _topSet(); 525 | break; 526 | case FloatingSlideType.onLeftAndBottom: 527 | _leftSet(); 528 | _bottomSet(); 529 | break; 530 | case FloatingSlideType.onRightAndTop: 531 | _rightSet(); 532 | _topSet(); 533 | break; 534 | case FloatingSlideType.onRightAndBottom: 535 | _rightSet(); 536 | _bottomSet(); 537 | break; 538 | } 539 | _saveCacheData(_left, _top); 540 | } 541 | 542 | setInitSlide() { 543 | void _topInit() { 544 | _top = _floatingData.top ?? widget.slideTopHeight; 545 | } 546 | 547 | void _leftInit() { 548 | _left = _floatingData.left ?? _floatingData.snapToEdgeSpace; 549 | } 550 | 551 | void _rightInit() { 552 | _left = _parentWidth - 553 | (_floatingData.right ?? _floatingData.snapToEdgeSpace) - 554 | _fWidth; 555 | } 556 | 557 | void _bottomInit() { 558 | _top = _parentHeight - 559 | (_floatingData.bottom ?? widget.slideBottomHeight) - 560 | _fHeight; 561 | } 562 | 563 | switch (_floatingData.slideType) { 564 | case FloatingSlideType.onLeftAndTop: 565 | _topInit(); 566 | _leftInit(); 567 | break; 568 | case FloatingSlideType.onLeftAndBottom: 569 | _leftInit(); 570 | _bottomInit(); 571 | break; 572 | case FloatingSlideType.onRightAndTop: 573 | _rightInit(); 574 | _topInit(); 575 | break; 576 | case FloatingSlideType.onRightAndBottom: 577 | _rightInit(); 578 | _bottomInit(); 579 | break; 580 | case FloatingSlideType.onPoint: 581 | _top = _floatingData.point?.y ?? widget.slideBottomHeight; 582 | _left = _floatingData.point?.x ?? _floatingData.snapToEdgeSpace; 583 | break; 584 | } 585 | _saveCacheData(_left, _top); 586 | } 587 | 588 | ///清除缓存数据 589 | _clearCacheData() { 590 | _floatingData.left = null; 591 | _floatingData.top = null; 592 | _floatingData.right = null; 593 | _floatingData.bottom = null; 594 | } 595 | 596 | ///保存缓存位置 597 | _saveCacheData(double left, double top) { 598 | if (widget.isPosCache) { 599 | _floatingData.left = left; 600 | _floatingData.top = top; 601 | } 602 | } 603 | 604 | ///设置缓存数据 605 | _setCacheData() { 606 | _top = _floatingData.top ?? 0; 607 | _left = _floatingData.left ?? 0; 608 | } 609 | 610 | _setParentHeightAndWidget() { 611 | if (_parentHeight == 0 || _parentWidth == 0) { 612 | _parentWidth = MediaQuery.of(context).size.width; 613 | _parentHeight = MediaQuery.of(context).size.height; 614 | } 615 | } 616 | 617 | _calcNewTopByRatio() { 618 | void setBySlide() { 619 | if (_floatingData.slideType == FloatingSlideType.onLeftAndBottom || 620 | _floatingData.slideType == FloatingSlideType.onRightAndBottom) { 621 | _top = _parentHeight - widget.slideBottomHeight - _fHeight; 622 | } else { 623 | _top = widget.slideTopHeight; 624 | } 625 | } 626 | 627 | // 可用高度,减去顶部和底部的预留高度 628 | double availableHeight = 629 | _parentHeight - widget.slideTopHeight - widget.slideBottomHeight; 630 | if (availableHeight <= 0) { 631 | //可用高度小于等于0时,设置在初始位置 632 | setBySlide(); 633 | return; 634 | } 635 | // 悬浮窗可用高度范围内的最小高度 636 | double heightInRange = min(availableHeight, _fHeight); 637 | // 计算剩余高度 638 | double remainHeight = availableHeight - heightInRange; 639 | if (remainHeight <= 0) { 640 | //剩余高度小于等于0时,设置在初始位置 641 | setBySlide(); 642 | } else { 643 | // 根据剩余高度和距离顶部的高度比,计算新的顶部距离 644 | _top = _topToRemainHeightRatio! * remainHeight; 645 | } 646 | } 647 | 648 | // 处理吸附在左右两侧的情况 649 | _calcNewLeftWhenSnapToEdge() { 650 | _slideLeft() { 651 | _left = _floatingData.snapToEdgeSpace; 652 | } 653 | _slideRight() { 654 | _left = _parentWidth - _fWidth - _floatingData.snapToEdgeSpace; 655 | } 656 | switch (widget.slideStopType) { 657 | case SlideStopType.slideStopLeftType: 658 | _slideLeft(); 659 | break; 660 | case SlideStopType.slideStopRightType: 661 | _slideRight(); 662 | break; 663 | case SlideStopType.slideStopAutoType: 664 | var centerX = _parentWidth / 2.0; //中心位置 665 | ((_left + _fWidth / 2) < centerX) ? _slideLeft() : _slideRight(); 666 | break; 667 | } 668 | } 669 | 670 | _calcNewLeftByRatio() { 671 | void setBySlide() { 672 | if (_floatingData.slideType == FloatingSlideType.onRightAndBottom || 673 | _floatingData.slideType == FloatingSlideType.onRightAndTop) { 674 | _left = _parentWidth - _fWidth - _floatingData.snapToEdgeSpace; 675 | } else { 676 | _left = _floatingData.snapToEdgeSpace; 677 | } 678 | } 679 | 680 | //计算可用宽度,减去左右两侧的预留宽度 681 | double availableWidth = _parentWidth - _floatingData.snapToEdgeSpace * 2; 682 | if (availableWidth <= 0) { 683 | setBySlide(); 684 | if (widget.isSnapToEdge) _calcNewLeftWhenSnapToEdge(); 685 | return; 686 | } 687 | double widthInRange = min(availableWidth, _fWidth); 688 | double remainWidth = availableWidth - widthInRange; 689 | if (remainWidth <= 0) { 690 | setBySlide(); 691 | } else { 692 | _left = _leftToRemainWidthRatio! * remainWidth; 693 | } 694 | if (widget.isSnapToEdge) _calcNewLeftWhenSnapToEdge(); 695 | } 696 | 697 | _calcNewPositionByRatio() { 698 | _calcNewTopByRatio(); 699 | _calcNewLeftByRatio(); 700 | } 701 | 702 | _setTopToRemainHeightRatio() { 703 | double initWhenNoRemainHeight() { 704 | switch (_floatingData.slideType) { 705 | case FloatingSlideType.onLeftAndTop: 706 | case FloatingSlideType.onRightAndTop: 707 | case FloatingSlideType.onPoint: 708 | return 0; 709 | case FloatingSlideType.onLeftAndBottom: 710 | case FloatingSlideType.onRightAndBottom: 711 | return 1; 712 | } 713 | } 714 | 715 | // 计算可用高度,减去顶部和底部的预留高度 716 | double availableHeight = 717 | _parentHeight - widget.slideTopHeight - widget.slideBottomHeight; 718 | if (availableHeight <= 0) { 719 | //可用高度小于等于0时,设置比例为初始值 720 | _topToRemainHeightRatio ??= initWhenNoRemainHeight(); 721 | return; 722 | } 723 | // 计算悬浮窗在可用高度范围内的高度 724 | double heightInRange = min(availableHeight - _top, _fHeight); 725 | // 根据外部高度和悬浮窗高度计算剩余高度 726 | double remainHeight = _parentHeight - heightInRange; 727 | if (remainHeight <= 0) { 728 | //剩余高度小于等于0时,设置比例为初始值 729 | _topToRemainHeightRatio ??= initWhenNoRemainHeight(); 730 | } else { 731 | //计算顶部距离与剩余高度之比 732 | _topToRemainHeightRatio = _top / remainHeight; 733 | } 734 | } 735 | 736 | _setLeftToRemainWidthRatio() { 737 | double initWhenNoRemainWidth() { 738 | switch (_floatingData.slideType) { 739 | case FloatingSlideType.onLeftAndTop: 740 | case FloatingSlideType.onLeftAndBottom: 741 | return 0; 742 | case FloatingSlideType.onRightAndTop: 743 | case FloatingSlideType.onRightAndBottom: 744 | case FloatingSlideType.onPoint: 745 | return 1; 746 | } 747 | } 748 | 749 | // 计算可用宽度,减去左右两侧的预留宽度 750 | double availableWidth = _parentWidth - _floatingData.snapToEdgeSpace * 2; 751 | if (availableWidth <= 0) { 752 | //可用宽度小于等于0时,设置比例为初始值 753 | _leftToRemainWidthRatio ??= initWhenNoRemainWidth(); 754 | return; 755 | } 756 | // 计算悬浮窗在可用宽度范围内的宽度 757 | double widthInRange = min(availableWidth - _left, _fWidth); 758 | // 根据外部宽度和悬浮窗宽度计算剩余宽度 759 | double remainWidth = _parentWidth - widthInRange; 760 | if (remainWidth <= 0) { 761 | //剩余宽度小于等于0时,设置比例为初始值 762 | _leftToRemainWidthRatio ??= initWhenNoRemainWidth(); 763 | } else { 764 | //计算左侧距离与剩余宽度之比 765 | _leftToRemainWidthRatio = _left / remainWidth; 766 | } 767 | } 768 | 769 | _setPositionToRemainRatio() { 770 | _setTopToRemainHeightRatio(); 771 | _setLeftToRemainWidthRatio(); 772 | } 773 | 774 | _notifyMove(double x, double y) { 775 | widget._log.log("移动 X:$x Y:$y"); 776 | for (var element in widget._listener) { 777 | element.moveListener?.call(Point(x, y)); 778 | } 779 | } 780 | 781 | _notifyMoveEnd(double x, double y) { 782 | widget._log.log("移动结束 X:$x Y:$y"); 783 | for (var element in widget._listener) { 784 | element.moveEndListener?.call(Point(x, y)); 785 | } 786 | } 787 | 788 | _notifyDown(double x, double y) { 789 | widget._log.log("按下 X:$x Y:$y"); 790 | for (var element in widget._listener) { 791 | element.downListener?.call(Point(x, y)); 792 | } 793 | } 794 | 795 | _notifyUp(double x, double y) { 796 | widget._log.log("抬起 X:$x Y:$y"); 797 | for (var element in widget._listener) { 798 | element.upListener?.call(Point(x, y)); 799 | } 800 | } 801 | 802 | @override 803 | void setState(VoidCallback fn) { 804 | if (mounted) { 805 | final schedulerPhase = SchedulerBinding.instance.schedulerPhase; 806 | if (schedulerPhase == SchedulerPhase.persistentCallbacks) { 807 | SchedulerBinding.instance.addPostFrameCallback((timeStamp) { 808 | super.setState(fn); 809 | }); 810 | } else { 811 | super.setState(fn); 812 | } 813 | } 814 | } 815 | 816 | @override 817 | void dispose() { 818 | super.dispose(); 819 | _slideController.dispose(); 820 | } 821 | } 822 | -------------------------------------------------------------------------------- /lib/floating_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// @name:floating_icon 4 | /// @package: 5 | /// @author:345 QQ:1831712732 6 | /// @time:2022/06/02 17:50 7 | /// @des: 8 | 9 | class FloatingIcon extends StatelessWidget { 10 | const FloatingIcon({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | color: Colors.amberAccent, 16 | child: const Icon(Icons.add_photo_alternate, size: 70), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/floating_increment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'floating/manager/floating_manager.dart'; 5 | 6 | /// @name:floating_increment 7 | /// @package: 8 | /// @author:345 QQ:1831712732 9 | /// @time:2022/02/10 23:21 10 | /// @des: 11 | 12 | class FloatingIncrement extends StatefulWidget { 13 | const FloatingIncrement({Key? key}) : super(key: key); 14 | 15 | @override 16 | _FloatingIncrementState createState() => _FloatingIncrementState(); 17 | } 18 | 19 | class _FloatingIncrementState extends State { 20 | int _counter = 0; 21 | double width = 80; 22 | double height = 80; 23 | double x = 30; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Material( 28 | color: Colors.transparent, 29 | child: GestureDetector( 30 | onTap: () { 31 | setState(() { 32 | if (width < 200) { 33 | width = width + x; 34 | height = height + x; 35 | } else { 36 | width = width - x; 37 | height = height - x; 38 | } 39 | _counter++; 40 | }); 41 | }, 42 | child: AnimatedContainer( 43 | width: width, 44 | height: height, 45 | decoration: BoxDecoration( 46 | color: Colors.blue, borderRadius: BorderRadius.circular(50)), 47 | alignment: Alignment.center, 48 | duration: const Duration(milliseconds: 300), 49 | child: Text( 50 | '放大缩小$_counter', 51 | style: const TextStyle(fontWeight: FontWeight.bold), 52 | ), 53 | ), 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/floating_play.dart: -------------------------------------------------------------------------------- 1 | /// @name:play_floating 2 | /// @package: 3 | /// @author:345 QQ:1831712732 4 | /// @time:2023/03/27 14:44 5 | /// @des: 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_floating/page.dart'; 8 | 9 | class FloatingPlay extends StatelessWidget { 10 | const FloatingPlay({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final size = MediaQuery.of(context).size; 15 | return Material( 16 | child: GestureDetector( 17 | onTap: () { 18 | Navigator.of(context).push(MaterialPageRoute(builder: (context) { 19 | return const CustomPage(); 20 | })); 21 | }, 22 | child: child(size), 23 | ), 24 | ); 25 | } 26 | 27 | Widget child(Size size) { 28 | return Container( 29 | width: size.width, 30 | padding: const EdgeInsets.symmetric(vertical: 10), 31 | decoration: const BoxDecoration(color: Colors.white, boxShadow: [ 32 | BoxShadow( 33 | color: Colors.black12, 34 | offset: Offset(0, 0), 35 | blurRadius: 16, 36 | ) 37 | ]), 38 | child: StreamBuilder(builder: (context, snapshot) { 39 | return Row( 40 | mainAxisSize: MainAxisSize.max, 41 | children: [ 42 | Container( 43 | color: Colors.white, 44 | padding: const EdgeInsets.symmetric(horizontal: 8), 45 | child: Stack( 46 | children: [ 47 | ClipRRect( 48 | borderRadius: const BorderRadius.all(Radius.circular(4)), 49 | child: 50 | Container(color: Colors.grey, width: 56, height: 32)), 51 | Positioned( 52 | right: 2, 53 | top: 2, 54 | child: Container( 55 | padding: const EdgeInsets.fromLTRB(4, 3, 4, 3), 56 | decoration: BoxDecoration( 57 | color: Colors.white, 58 | borderRadius: BorderRadius.circular(20)), 59 | child: const Text( 60 | "哈哈红红火火恍恍惚惚", 61 | style: TextStyle(fontSize: 8, color: Colors.amber), 62 | ), 63 | ), 64 | ) 65 | ], 66 | ), 67 | ), 68 | const Expanded( 69 | child: SizedBox( 70 | height: 22, 71 | child: Text('欢迎使用一键式悬浮窗组件', 72 | style: TextStyle(fontSize: 15, color: Colors.redAccent), 73 | ), 74 | )), 75 | StreamBuilder(builder: (context, snapshot) { 76 | return GestureDetector( 77 | behavior: HitTestBehavior.opaque, 78 | onTap: () {}, 79 | child: Container( 80 | height: 32, 81 | width: 24, 82 | margin: const EdgeInsets.symmetric(horizontal: 8), 83 | color: Colors.purple, 84 | ), 85 | ); 86 | }, stream: null,), 87 | GestureDetector( 88 | behavior: HitTestBehavior.opaque, 89 | onTap: () {}, 90 | child: Container( 91 | margin: const EdgeInsets.symmetric(horizontal: 8), 92 | child: Container( 93 | color: Colors.pink, 94 | width: 24, 95 | height: 24, 96 | ), 97 | ), 98 | ), 99 | GestureDetector( 100 | behavior: HitTestBehavior.opaque, 101 | onTap: () {}, 102 | child: Padding( 103 | padding: const EdgeInsets.symmetric(horizontal: 8), 104 | child: Container( 105 | color: Colors.yellow, 106 | width: 24, 107 | height: 24, 108 | ), 109 | ), 110 | ) 111 | ], 112 | ); 113 | }, stream: null,), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/floating_scroll.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'floating/manager/floating_manager.dart'; 5 | 6 | /// @name:floating_increment 7 | /// @package: 8 | /// @author:345 QQ:1831712732 9 | /// @time:2022/02/10 23:21 10 | /// @des: 11 | 12 | class FloatingScroll extends StatefulWidget { 13 | const FloatingScroll({Key? key}) : super(key: key); 14 | 15 | @override 16 | _FloatingScrollState createState() => _FloatingScrollState(); 17 | } 18 | 19 | class _FloatingScrollState extends State { 20 | int _counter = 0; 21 | double width = 80; 22 | double height = 80; 23 | double x = 30; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Material( 28 | color: Colors.transparent, 29 | child: GestureDetector( 30 | onTap: () { 31 | if (x > 240) { 32 | x = 30; 33 | }else{ 34 | x = x + 30; 35 | } 36 | var floating = floatingManager.getFloating("2"); 37 | floating.getScrollManager().scrollTopLeft(x, x); 38 | }, 39 | child: AnimatedContainer( 40 | width: width, 41 | height: height, 42 | decoration: BoxDecoration( 43 | color: Colors.yellow, borderRadius: BorderRadius.circular(50)), 44 | alignment: Alignment.center, 45 | duration: const Duration(milliseconds: 300), 46 | child: Text( 47 | '点击移动$_counter', 48 | style: const TextStyle(fontWeight: FontWeight.bold), 49 | ), 50 | ), 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_floating/floating_icon.dart'; 3 | import 'package:flutter_floating/floating_scroll.dart'; 4 | import 'button_widget.dart'; 5 | import 'floating/assist/floating_slide_type.dart'; 6 | import 'floating/assist/slide_stop_type.dart'; 7 | import 'floating/floating.dart'; 8 | import 'floating/listener/event_listener.dart'; 9 | import 'floating/manager/floating_manager.dart'; 10 | import 'floating_increment.dart'; 11 | import 'page.dart'; 12 | 13 | void main() => runApp(const MyApp()); 14 | 15 | class MyApp extends StatelessWidget { 16 | const MyApp({Key? key}) : super(key: key); 17 | 18 | static GlobalKey globalKey = GlobalKey(); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return MaterialApp( 23 | title: 'Flutter Demo', 24 | navigatorKey: globalKey, 25 | theme: ThemeData( 26 | primarySwatch: Colors.blue, 27 | ), 28 | home: const MyHomePage(title: 'Floating'), 29 | ); 30 | } 31 | } 32 | 33 | class MyHomePage extends StatefulWidget { 34 | const MyHomePage({Key? key, required this.title}) : super(key: key); 35 | 36 | final String title; 37 | 38 | @override 39 | State createState() => _MyHomePageState(); 40 | } 41 | 42 | class _MyHomePageState extends State { 43 | late Floating floatingOne; 44 | late Floating floatingTwo; 45 | 46 | @override 47 | void initState() { 48 | super.initState(); 49 | 50 | //因为获取状态栏高度,所以延时一帧 51 | floatingOne = floatingManager.createFloating( 52 | "1", 53 | Floating(const FloatingIcon(), 54 | slideType: FloatingSlideType.onRightAndBottom, 55 | isShowLog: false, 56 | isSnapToEdge: false, 57 | isPosCache: true, 58 | moveOpacity: 1, 59 | left: 100, 60 | bottom: 100, 61 | slideBottomHeight: 100)); 62 | 63 | floatingTwo = floatingManager.createFloating( 64 | "2", 65 | Floating( 66 | const FloatingScroll(), 67 | slideType: FloatingSlideType.onPoint, 68 | isShowLog: false, 69 | right: 50, 70 | isSnapToEdge: true, 71 | snapToEdgeSpace: 50, 72 | top: 100, 73 | slideStopType: SlideStopType.slideStopAutoType, 74 | )); 75 | var twoListener = FloatingEventListener() 76 | ..closeListener = () { 77 | // var point = floatingTwo.getFloatingPoint(); 78 | // print('关闭 ${point.x} -- ${point.y}'); 79 | } 80 | ..hideFloatingListener = () { 81 | // var point = floatingTwo.getFloatingPoint(); 82 | // print('隐藏 ${point.x} -- ${point.y}'); 83 | } 84 | ..moveEndListener = (point) { 85 | // var point = floatingTwo.getFloatingPoint(); 86 | // print('移动结束 ${point.x} -- ${point.y}'); 87 | }; 88 | floatingTwo.addFloatingListener(twoListener); 89 | } 90 | 91 | void _startCustomPage() { 92 | Navigator.of(context).push(MaterialPageRoute(builder: (context) { 93 | return const CustomPage(); 94 | })); 95 | } 96 | 97 | var isOpen = false; 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | return Scaffold( 102 | appBar: AppBar( 103 | title: Text(widget.title), 104 | ), 105 | body: Center( 106 | child: SingleChildScrollView( 107 | child: Column( 108 | // horizontal). 109 | mainAxisAlignment: MainAxisAlignment.center, 110 | children: [ 111 | const SizedBox(height: 30), 112 | ButtonWidget( 113 | "显示/关闭左上角没有回弹的悬浮窗", 114 | () { 115 | var floating = floatingManager.getFloating("1"); 116 | floating.isShowing 117 | ? floating.close() 118 | : floating.open(context); 119 | }, 120 | ), 121 | ButtonWidget("显示右上角悬浮窗", () { 122 | if (!isOpen) { 123 | floatingTwo.open(context); 124 | isOpen = true; 125 | } else { 126 | floatingTwo.showFloating(); 127 | } 128 | }), 129 | ButtonWidget("隐藏右上角悬浮窗", () { 130 | floatingTwo.hideFloating(); 131 | }), 132 | ButtonWidget("添加没有透明度动画的悬浮窗", () { 133 | floatingManager 134 | .createFloating( 135 | DateTime.now().millisecondsSinceEpoch, 136 | Floating(const FloatingIcon(), 137 | slideType: FloatingSlideType.onLeftAndTop, 138 | left: 0, 139 | isShowLog: false, 140 | isPosCache: true, 141 | moveOpacity: 1, 142 | top: floatingManager.floatingSize() * 80)) 143 | .open(context); 144 | }), 145 | ButtonWidget("添加禁止滑动到状态栏和底部的悬浮窗", () { 146 | floatingManager 147 | .createFloating( 148 | DateTime.now().millisecondsSinceEpoch, 149 | Floating(const FloatingIncrement(), 150 | slideType: FloatingSlideType.onRightAndBottom, 151 | right: 100, 152 | slideStopType: SlideStopType.slideStopLeftType, 153 | bottom: floatingManager.floatingSize() * 80, 154 | //禁止滑动到状态栏 155 | edgeSpeed: 100, 156 | slideTopHeight: MediaQuery.of(context).padding.top, 157 | slideBottomHeight: 60)) 158 | .open(context); 159 | }), 160 | ButtonWidget("跳转页面", () => _startCustomPage()), 161 | ], 162 | ), 163 | ), 164 | ), 165 | floatingActionButton: FloatingActionButton( 166 | onPressed: _startCustomPage, 167 | tooltip: 'Increment', 168 | child: const Text("跳"), 169 | ), // This trailing comma makes auto-formatting nicer for build methods. 170 | ); 171 | } 172 | 173 | @override 174 | void dispose() { 175 | super.dispose(); 176 | floatingTwo.close(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_floating/floating/manager/floating_manager.dart'; 3 | 4 | /// @name:page 5 | /// @package: 6 | /// @author:345 QQ:1831712732 7 | /// @time:2022/02/16 22:27 8 | /// @des: 9 | 10 | class CustomPage extends StatefulWidget { 11 | const CustomPage({Key? key}) : super(key: key); 12 | 13 | @override 14 | _CustomPageState createState() => _CustomPageState(); 15 | } 16 | 17 | class _CustomPageState extends State { 18 | 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | } 24 | 25 | // var s = Get.put(AwesomeController()); 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return Scaffold( 30 | appBar: AppBar( 31 | title: const Text("功能页面"), 32 | ), 33 | body: SingleChildScrollView( 34 | child: Column( 35 | children: [ 36 | // AwesomeView(), 37 | GestureDetector( 38 | child: const Text( 39 | "关闭悬浮窗", 40 | style: TextStyle(fontSize: 30), 41 | ), 42 | onTap: () { 43 | floatingManager.getFloating("1").close(); 44 | }, 45 | ), 46 | ], 47 | ), 48 | ), 49 | ); 50 | } 51 | 52 | @override 53 | void dispose() { 54 | super.dispose(); 55 | } 56 | } 57 | 58 | // class AwesomeController extends GetxController { 59 | // final String title = 'My Awesome View'; 60 | // } 61 | // 62 | // 63 | // 64 | // // 一定要记住传递你用来注册控制器的`Type`! 65 | // class AwesomeView extends GetView { 66 | // 67 | // @override 68 | // Widget build(BuildContext context) { 69 | // return GestureDetector( 70 | // onTap: () { 71 | // floatingManager 72 | // .createFloating( 73 | // "1", 74 | // Floating(const FloatingIncrement(), 75 | // slideType: FloatingSlideType.onLeftAndTop, 76 | // left: 0, 77 | // top: 150, 78 | // isShowLog: false, 79 | // slideBottomHeight: 100)) 80 | // .open(context); 81 | // }, 82 | // child: Container( 83 | // padding: EdgeInsets.all(20), 84 | // child: Text(controller.title), // 只需调用 "controller.something"。 85 | // ), 86 | // ); 87 | // } 88 | // } 89 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a 9 | url: "https://pub.flutter-io.cn" 10 | source: hosted 11 | version: "61.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 17 | url: "https://pub.flutter-io.cn" 18 | source: hosted 19 | version: "5.13.0" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 25 | url: "https://pub.flutter-io.cn" 26 | source: hosted 27 | version: "2.4.2" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.flutter-io.cn" 34 | source: hosted 35 | version: "2.11.0" 36 | boolean_selector: 37 | dependency: transitive 38 | description: 39 | name: boolean_selector 40 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 41 | url: "https://pub.flutter-io.cn" 42 | source: hosted 43 | version: "2.1.1" 44 | characters: 45 | dependency: transitive 46 | description: 47 | name: characters 48 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 49 | url: "https://pub.flutter-io.cn" 50 | source: hosted 51 | version: "1.3.0" 52 | clock: 53 | dependency: transitive 54 | description: 55 | name: clock 56 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "1.1.1" 60 | collection: 61 | dependency: transitive 62 | description: 63 | name: collection 64 | sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf 65 | url: "https://pub.flutter-io.cn" 66 | source: hosted 67 | version: "1.19.0" 68 | convert: 69 | dependency: transitive 70 | description: 71 | name: convert 72 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 73 | url: "https://pub.flutter-io.cn" 74 | source: hosted 75 | version: "3.1.1" 76 | coverage: 77 | dependency: transitive 78 | description: 79 | name: coverage 80 | sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "1.11.1" 84 | crypto: 85 | dependency: transitive 86 | description: 87 | name: crypto 88 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 89 | url: "https://pub.flutter-io.cn" 90 | source: hosted 91 | version: "3.0.3" 92 | cupertino_icons: 93 | dependency: "direct main" 94 | description: 95 | name: cupertino_icons 96 | sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be 97 | url: "https://pub.flutter-io.cn" 98 | source: hosted 99 | version: "1.0.5" 100 | fake_async: 101 | dependency: transitive 102 | description: 103 | name: fake_async 104 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 105 | url: "https://pub.flutter-io.cn" 106 | source: hosted 107 | version: "1.3.1" 108 | file: 109 | dependency: transitive 110 | description: 111 | name: file 112 | sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" 113 | url: "https://pub.flutter-io.cn" 114 | source: hosted 115 | version: "6.1.4" 116 | flutter: 117 | dependency: "direct main" 118 | description: flutter 119 | source: sdk 120 | version: "0.0.0" 121 | flutter_lints: 122 | dependency: "direct dev" 123 | description: 124 | name: flutter_lints 125 | sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 126 | url: "https://pub.flutter-io.cn" 127 | source: hosted 128 | version: "1.0.4" 129 | flutter_test: 130 | dependency: "direct dev" 131 | description: flutter 132 | source: sdk 133 | version: "0.0.0" 134 | frontend_server_client: 135 | dependency: transitive 136 | description: 137 | name: frontend_server_client 138 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 139 | url: "https://pub.flutter-io.cn" 140 | source: hosted 141 | version: "3.2.0" 142 | glob: 143 | dependency: transitive 144 | description: 145 | name: glob 146 | sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" 147 | url: "https://pub.flutter-io.cn" 148 | source: hosted 149 | version: "2.1.2" 150 | http_multi_server: 151 | dependency: transitive 152 | description: 153 | name: http_multi_server 154 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 155 | url: "https://pub.flutter-io.cn" 156 | source: hosted 157 | version: "3.2.1" 158 | http_parser: 159 | dependency: transitive 160 | description: 161 | name: http_parser 162 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 163 | url: "https://pub.flutter-io.cn" 164 | source: hosted 165 | version: "4.0.2" 166 | io: 167 | dependency: transitive 168 | description: 169 | name: io 170 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 171 | url: "https://pub.flutter-io.cn" 172 | source: hosted 173 | version: "1.0.4" 174 | js: 175 | dependency: transitive 176 | description: 177 | name: js 178 | sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" 179 | url: "https://pub.flutter-io.cn" 180 | source: hosted 181 | version: "0.6.5" 182 | leak_tracker: 183 | dependency: transitive 184 | description: 185 | name: leak_tracker 186 | sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" 187 | url: "https://pub.flutter-io.cn" 188 | source: hosted 189 | version: "10.0.7" 190 | leak_tracker_flutter_testing: 191 | dependency: transitive 192 | description: 193 | name: leak_tracker_flutter_testing 194 | sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" 195 | url: "https://pub.flutter-io.cn" 196 | source: hosted 197 | version: "3.0.8" 198 | leak_tracker_testing: 199 | dependency: transitive 200 | description: 201 | name: leak_tracker_testing 202 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 203 | url: "https://pub.flutter-io.cn" 204 | source: hosted 205 | version: "3.0.1" 206 | lints: 207 | dependency: transitive 208 | description: 209 | name: lints 210 | sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c 211 | url: "https://pub.flutter-io.cn" 212 | source: hosted 213 | version: "1.0.1" 214 | logging: 215 | dependency: transitive 216 | description: 217 | name: logging 218 | sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" 219 | url: "https://pub.flutter-io.cn" 220 | source: hosted 221 | version: "1.2.0" 222 | matcher: 223 | dependency: transitive 224 | description: 225 | name: matcher 226 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 227 | url: "https://pub.flutter-io.cn" 228 | source: hosted 229 | version: "0.12.16+1" 230 | material_color_utilities: 231 | dependency: transitive 232 | description: 233 | name: material_color_utilities 234 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 235 | url: "https://pub.flutter-io.cn" 236 | source: hosted 237 | version: "0.11.1" 238 | meta: 239 | dependency: transitive 240 | description: 241 | name: meta 242 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 243 | url: "https://pub.flutter-io.cn" 244 | source: hosted 245 | version: "1.15.0" 246 | mime: 247 | dependency: transitive 248 | description: 249 | name: mime 250 | sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e 251 | url: "https://pub.flutter-io.cn" 252 | source: hosted 253 | version: "1.0.4" 254 | node_preamble: 255 | dependency: transitive 256 | description: 257 | name: node_preamble 258 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 259 | url: "https://pub.flutter-io.cn" 260 | source: hosted 261 | version: "2.0.2" 262 | package_config: 263 | dependency: transitive 264 | description: 265 | name: package_config 266 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 267 | url: "https://pub.flutter-io.cn" 268 | source: hosted 269 | version: "2.1.0" 270 | path: 271 | dependency: transitive 272 | description: 273 | name: path 274 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 275 | url: "https://pub.flutter-io.cn" 276 | source: hosted 277 | version: "1.9.0" 278 | pool: 279 | dependency: transitive 280 | description: 281 | name: pool 282 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 283 | url: "https://pub.flutter-io.cn" 284 | source: hosted 285 | version: "1.5.1" 286 | pub_semver: 287 | dependency: transitive 288 | description: 289 | name: pub_semver 290 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 291 | url: "https://pub.flutter-io.cn" 292 | source: hosted 293 | version: "2.1.4" 294 | shelf: 295 | dependency: transitive 296 | description: 297 | name: shelf 298 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 299 | url: "https://pub.flutter-io.cn" 300 | source: hosted 301 | version: "1.4.1" 302 | shelf_packages_handler: 303 | dependency: transitive 304 | description: 305 | name: shelf_packages_handler 306 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 307 | url: "https://pub.flutter-io.cn" 308 | source: hosted 309 | version: "3.0.2" 310 | shelf_static: 311 | dependency: transitive 312 | description: 313 | name: shelf_static 314 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e 315 | url: "https://pub.flutter-io.cn" 316 | source: hosted 317 | version: "1.1.2" 318 | shelf_web_socket: 319 | dependency: transitive 320 | description: 321 | name: shelf_web_socket 322 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 323 | url: "https://pub.flutter-io.cn" 324 | source: hosted 325 | version: "1.0.4" 326 | sky_engine: 327 | dependency: transitive 328 | description: flutter 329 | source: sdk 330 | version: "0.0.0" 331 | source_map_stack_trace: 332 | dependency: transitive 333 | description: 334 | name: source_map_stack_trace 335 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 336 | url: "https://pub.flutter-io.cn" 337 | source: hosted 338 | version: "2.1.1" 339 | source_maps: 340 | dependency: transitive 341 | description: 342 | name: source_maps 343 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 344 | url: "https://pub.flutter-io.cn" 345 | source: hosted 346 | version: "0.10.12" 347 | source_span: 348 | dependency: transitive 349 | description: 350 | name: source_span 351 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 352 | url: "https://pub.flutter-io.cn" 353 | source: hosted 354 | version: "1.10.0" 355 | stack_trace: 356 | dependency: transitive 357 | description: 358 | name: stack_trace 359 | sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" 360 | url: "https://pub.flutter-io.cn" 361 | source: hosted 362 | version: "1.12.0" 363 | stream_channel: 364 | dependency: transitive 365 | description: 366 | name: stream_channel 367 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 368 | url: "https://pub.flutter-io.cn" 369 | source: hosted 370 | version: "2.1.2" 371 | string_scanner: 372 | dependency: transitive 373 | description: 374 | name: string_scanner 375 | sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" 376 | url: "https://pub.flutter-io.cn" 377 | source: hosted 378 | version: "1.3.0" 379 | term_glyph: 380 | dependency: transitive 381 | description: 382 | name: term_glyph 383 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 384 | url: "https://pub.flutter-io.cn" 385 | source: hosted 386 | version: "1.2.1" 387 | test: 388 | dependency: "direct dev" 389 | description: 390 | name: test 391 | sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" 392 | url: "https://pub.flutter-io.cn" 393 | source: hosted 394 | version: "1.25.8" 395 | test_api: 396 | dependency: transitive 397 | description: 398 | name: test_api 399 | sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" 400 | url: "https://pub.flutter-io.cn" 401 | source: hosted 402 | version: "0.7.3" 403 | test_core: 404 | dependency: transitive 405 | description: 406 | name: test_core 407 | sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" 408 | url: "https://pub.flutter-io.cn" 409 | source: hosted 410 | version: "0.6.5" 411 | typed_data: 412 | dependency: transitive 413 | description: 414 | name: typed_data 415 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 416 | url: "https://pub.flutter-io.cn" 417 | source: hosted 418 | version: "1.3.2" 419 | vector_math: 420 | dependency: transitive 421 | description: 422 | name: vector_math 423 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 424 | url: "https://pub.flutter-io.cn" 425 | source: hosted 426 | version: "2.1.4" 427 | vm_service: 428 | dependency: transitive 429 | description: 430 | name: vm_service 431 | sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b 432 | url: "https://pub.flutter-io.cn" 433 | source: hosted 434 | version: "14.3.0" 435 | watcher: 436 | dependency: transitive 437 | description: 438 | name: watcher 439 | sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" 440 | url: "https://pub.flutter-io.cn" 441 | source: hosted 442 | version: "1.0.2" 443 | web_socket_channel: 444 | dependency: transitive 445 | description: 446 | name: web_socket_channel 447 | sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b 448 | url: "https://pub.flutter-io.cn" 449 | source: hosted 450 | version: "2.4.0" 451 | webkit_inspection_protocol: 452 | dependency: transitive 453 | description: 454 | name: webkit_inspection_protocol 455 | sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" 456 | url: "https://pub.flutter-io.cn" 457 | source: hosted 458 | version: "1.2.0" 459 | yaml: 460 | dependency: transitive 461 | description: 462 | name: yaml 463 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 464 | url: "https://pub.flutter-io.cn" 465 | source: hosted 466 | version: "3.1.2" 467 | sdks: 468 | dart: ">=3.4.0 <4.0.0" 469 | flutter: ">=3.18.0-18.0.pre.54" 470 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_floating 2 | description: see origin 3 | version: 1.1.1 4 | homepage: https://github.com/LvKang-insist/Floating 5 | 6 | environment: 7 | sdk: ">=2.16.1 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | cupertino_icons: ^1.0.3 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | flutter_lints: ^1.0.0 19 | test: ^1.17.10 20 | flutter: 21 | 22 | uses-material-design: true 23 | 24 | module: 25 | androidX: true 26 | androidPackage: com.lvkang.floating 27 | iosBundleIdentifier: com.lvkang.floating 28 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_floating/main.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | await tester.pumpWidget(const MyApp()); 15 | 16 | expect(find.text('0'), findsOneWidget); 17 | expect(find.text('1'), findsNothing); 18 | await tester.tap(find.byIcon(Icons.add)); 19 | await tester.pump(); 20 | expect(find.text('0'), findsNothing); 21 | expect(find.text('1'), findsOneWidget); 22 | }); 23 | } 24 | --------------------------------------------------------------------------------