floating_item.dart 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. // ignore_for_file: must_be_immutable
  2. import 'dart:async';
  3. import 'package:flutter/material.dart';
  4. import 'dart:ui' as ui;
  5. import 'package:vnoteapp/components/floating_window/click_notification.dart';
  6. /// [FloatingItem]一个单独功能完善的列表项类
  7. class FloatingItem extends StatefulWidget {
  8. FloatingItem({
  9. super.key,
  10. required this.top,
  11. required this.isLeft,
  12. required this.title,
  13. required this.imageProvider,
  14. required this.index,
  15. required this.left,
  16. required this.isEntering,
  17. this.width,
  18. });
  19. /// [index] 列表项的索引值
  20. int index;
  21. /// [top]列表项的y坐标值
  22. double top;
  23. /// [left]列表项的x坐标值
  24. double left;
  25. ///[isLeft] 列表项是否在左侧,否则是右侧
  26. bool isLeft;
  27. /// [title] 列表项的文字说明
  28. String title;
  29. ///[imageProvider] 列表项Logo的imageProvider
  30. ImageProvider imageProvider;
  31. ///[width] 屏幕宽度的 1 / 2
  32. double? width;
  33. ///[isEntering] 列表项是否触发进场动画
  34. bool isEntering;
  35. @override
  36. _FloatingItemState createState() => _FloatingItemState();
  37. /// 全部列表项执行退场动画
  38. static void reverse() {
  39. for (int i = 0; i < _FloatingItemState.animationControllers.length; ++i) {
  40. if (!_FloatingItemState.animationControllers[i]
  41. .toString()
  42. .contains('DISPOSED')) {
  43. _FloatingItemState.animationControllers[i].reverse();
  44. }
  45. }
  46. }
  47. /// 全部列表项执行进场动画
  48. static void forward() {
  49. for (int i = 0; i < _FloatingItemState.animationControllers.length; ++i) {
  50. if (!_FloatingItemState.animationControllers[i]
  51. .toString()
  52. .contains('DISPOSED')) {
  53. _FloatingItemState.animationControllers[i].forward();
  54. }
  55. }
  56. }
  57. /// 每次更新时释放所有动画资源,清空动画控制器列表
  58. static void resetList() {
  59. for (int i = 0; i < _FloatingItemState.animationControllers.length; ++i) {
  60. if (!_FloatingItemState.animationControllers[i]
  61. .toString()
  62. .contains('DISPOSED')) {
  63. _FloatingItemState.animationControllers[i].dispose();
  64. }
  65. }
  66. _FloatingItemState.animationControllers.clear();
  67. _FloatingItemState.animationControllers = [];
  68. }
  69. }
  70. class _FloatingItemState extends State<FloatingItem>
  71. with TickerProviderStateMixin {
  72. /// [isPress] 列表项是否被按下
  73. bool isPress = false;
  74. ///[image] 列表项Logo的[ui.Image]对象,用于绘制Logo
  75. ui.Image? image;
  76. /// [animationController] 列表关闭动画的控制器
  77. AnimationController? animationController;
  78. /// [animationController] 所有列表项的动画控制器列表
  79. static List<AnimationController> animationControllers = [];
  80. /// [animation] 列表项的关闭动画
  81. Animation? animation;
  82. @override
  83. void initState() {
  84. // TODO: implement initState
  85. isPress = false;
  86. /// 获取Logo的ui.Image对象
  87. loadImageByProvider(widget.imageProvider).then((value) {
  88. setState(() {
  89. image = value;
  90. });
  91. });
  92. super.initState();
  93. }
  94. @override
  95. Widget build(BuildContext context) {
  96. return Positioned(
  97. left: widget.left,
  98. top: widget.top,
  99. child: GestureDetector(
  100. /// 监听按下事件,在点击区域内则将[isPress]设为true,若在关闭区域内则不做任何操作
  101. onPanDown: (details) {
  102. if (widget.isLeft) {
  103. /// 点击区域内
  104. if (details.globalPosition.dx < widget.width!) {
  105. setState(() {
  106. isPress = true;
  107. });
  108. }
  109. } else {
  110. /// 点击区域内
  111. if (details.globalPosition.dx < widget.width! * 2 - 50) {
  112. setState(() {
  113. isPress = true;
  114. });
  115. }
  116. }
  117. },
  118. /// 监听抬起事件
  119. onTapUp: (details) async {
  120. /// 通过左右列表项来决定关闭的区域,以及选中区域,触发相应的关闭或选中事件
  121. if (widget.isLeft) {
  122. /// 位于关闭区域
  123. if (details.globalPosition.dx >= widget.width! && !isPress) {
  124. /// 等待关闭动画执行完毕
  125. await animationController?.reverse();
  126. /// 通知父级触发关闭事件
  127. ClickNotification(deletedIndex: widget.index).dispatch(context);
  128. } else {
  129. /// 通知父级触发相应的点击事件
  130. ClickNotification(clickIndex: widget.index).dispatch(context);
  131. }
  132. } else {
  133. /// 位于关闭区域
  134. if (details.globalPosition.dx >= widget.width! * 2 - 50.0 &&
  135. !isPress) {
  136. /// 设置从中间返回至边缘的关闭动画
  137. await animationController?.reverse();
  138. /// 通知父级触发关闭事件
  139. ClickNotification(deletedIndex: widget.index).dispatch(context);
  140. } else {
  141. /// 通知父级触发选中事件
  142. ClickNotification(clickIndex: widget.index).dispatch(context);
  143. }
  144. }
  145. /// 抬起后取消选中
  146. setState(() {
  147. isPress = false;
  148. });
  149. },
  150. onTapCancel: () {
  151. /// 超出范围取消选中
  152. setState(() {
  153. isPress = false;
  154. });
  155. },
  156. child: Container(
  157. color: Colors.red,
  158. ),
  159. ),
  160. );
  161. // CustomPaint(
  162. // size: Size(widget.width! + 50.0, 50.0),
  163. // painter: FloatingItemPainter(
  164. // title: widget.title,
  165. // isLeft: widget.isLeft,
  166. // isPress: isPress,
  167. // image: image,
  168. // ))));
  169. }
  170. /// 通过ImageProvider获取ui.image
  171. Future<ui.Image> loadImageByProvider(
  172. ImageProvider provider, {
  173. ImageConfiguration config = ImageConfiguration.empty,
  174. }) async {
  175. Completer<ui.Image> completer = Completer<ui.Image>(); //完成的回调
  176. ImageStreamListener listener;
  177. ImageStream stream = provider.resolve(config); //获取图片流
  178. listener = ImageStreamListener((ImageInfo frame, bool sync) {
  179. //监听
  180. final ui.Image image = frame.image;
  181. completer.complete(image); //完成
  182. // stream.removeListener(listener); //移除监听
  183. });
  184. stream.addListener(listener); //添加监听
  185. return completer.future; //返回
  186. }
  187. @override
  188. void didUpdateWidget(FloatingItem oldWidget) {
  189. // TODO: implement didUpdateWidget
  190. animationController = AnimationController(
  191. vsync: this, duration: const Duration(milliseconds: 100));
  192. /// 初始化进场动画
  193. if (widget.isLeft) {
  194. animation = Tween<double>(begin: -(widget.width! + 50.0), end: 0.0)
  195. .animate(animationController!)
  196. ..addListener(() {
  197. setState(() {
  198. widget.left = animation!.value;
  199. });
  200. });
  201. } else {
  202. animation =
  203. Tween<double>(begin: widget.width! * 2, end: widget.width! - 50.0)
  204. .animate(animationController!)
  205. ..addListener(() {
  206. setState(() {
  207. widget.left = animation!.value;
  208. });
  209. });
  210. }
  211. animationControllers.add(animationController!);
  212. /// 执行进场动画
  213. if (animationController!.status == AnimationStatus.dismissed &&
  214. widget.isEntering) {
  215. animationController!.forward();
  216. }
  217. /// 无需执行进场动画,将列表项置于动画末尾
  218. else {
  219. animationController!.forward(from: 100.0);
  220. }
  221. super.didUpdateWidget(oldWidget);
  222. }
  223. @override
  224. void dispose() {
  225. // TODO: implement dispose
  226. /// 释放动画资源,避免内存泄漏
  227. if (!animationController.toString().toString().contains('DISPOSED')) {
  228. animationController!.dispose();
  229. }
  230. super.dispose();
  231. }
  232. }