// ignore_for_file: must_be_immutable import 'dart:async'; import 'package:flutter/material.dart'; import 'dart:ui' as ui; import 'package:vnoteapp/components/floating_window/click_notification.dart'; /// [FloatingItem]一个单独功能完善的列表项类 class FloatingItem extends StatefulWidget { FloatingItem({ super.key, required this.top, required this.isLeft, required this.title, required this.imageProvider, required this.index, required this.left, required this.isEntering, this.width, }); /// [index] 列表项的索引值 int index; /// [top]列表项的y坐标值 double top; /// [left]列表项的x坐标值 double left; ///[isLeft] 列表项是否在左侧,否则是右侧 bool isLeft; /// [title] 列表项的文字说明 String title; ///[imageProvider] 列表项Logo的imageProvider ImageProvider imageProvider; ///[width] 屏幕宽度的 1 / 2 double? width; ///[isEntering] 列表项是否触发进场动画 bool isEntering; @override _FloatingItemState createState() => _FloatingItemState(); /// 全部列表项执行退场动画 static void reverse() { for (int i = 0; i < _FloatingItemState.animationControllers.length; ++i) { if (!_FloatingItemState.animationControllers[i] .toString() .contains('DISPOSED')) { _FloatingItemState.animationControllers[i].reverse(); } } } /// 全部列表项执行进场动画 static void forward() { for (int i = 0; i < _FloatingItemState.animationControllers.length; ++i) { if (!_FloatingItemState.animationControllers[i] .toString() .contains('DISPOSED')) { _FloatingItemState.animationControllers[i].forward(); } } } /// 每次更新时释放所有动画资源,清空动画控制器列表 static void resetList() { for (int i = 0; i < _FloatingItemState.animationControllers.length; ++i) { if (!_FloatingItemState.animationControllers[i] .toString() .contains('DISPOSED')) { _FloatingItemState.animationControllers[i].dispose(); } } _FloatingItemState.animationControllers.clear(); _FloatingItemState.animationControllers = []; } } class _FloatingItemState extends State with TickerProviderStateMixin { /// [isPress] 列表项是否被按下 bool isPress = false; ///[image] 列表项Logo的[ui.Image]对象,用于绘制Logo ui.Image? image; /// [animationController] 列表关闭动画的控制器 AnimationController? animationController; /// [animationController] 所有列表项的动画控制器列表 static List animationControllers = []; /// [animation] 列表项的关闭动画 Animation? animation; @override void initState() { // TODO: implement initState isPress = false; /// 获取Logo的ui.Image对象 loadImageByProvider(widget.imageProvider).then((value) { setState(() { image = value; }); }); super.initState(); } @override Widget build(BuildContext context) { return Positioned( left: widget.left, top: widget.top, child: GestureDetector( /// 监听按下事件,在点击区域内则将[isPress]设为true,若在关闭区域内则不做任何操作 onPanDown: (details) { if (widget.isLeft) { /// 点击区域内 if (details.globalPosition.dx < widget.width!) { setState(() { isPress = true; }); } } else { /// 点击区域内 if (details.globalPosition.dx < widget.width! * 2 - 50) { setState(() { isPress = true; }); } } }, /// 监听抬起事件 onTapUp: (details) async { /// 通过左右列表项来决定关闭的区域,以及选中区域,触发相应的关闭或选中事件 if (widget.isLeft) { /// 位于关闭区域 if (details.globalPosition.dx >= widget.width! && !isPress) { /// 等待关闭动画执行完毕 await animationController?.reverse(); /// 通知父级触发关闭事件 ClickNotification(deletedIndex: widget.index).dispatch(context); } else { /// 通知父级触发相应的点击事件 ClickNotification(clickIndex: widget.index).dispatch(context); } } else { /// 位于关闭区域 if (details.globalPosition.dx >= widget.width! * 2 - 50.0 && !isPress) { /// 设置从中间返回至边缘的关闭动画 await animationController?.reverse(); /// 通知父级触发关闭事件 ClickNotification(deletedIndex: widget.index).dispatch(context); } else { /// 通知父级触发选中事件 ClickNotification(clickIndex: widget.index).dispatch(context); } } /// 抬起后取消选中 setState(() { isPress = false; }); }, onTapCancel: () { /// 超出范围取消选中 setState(() { isPress = false; }); }, child: Container( color: Colors.red, ), ), ); // CustomPaint( // size: Size(widget.width! + 50.0, 50.0), // painter: FloatingItemPainter( // title: widget.title, // isLeft: widget.isLeft, // isPress: isPress, // image: image, // )))); } /// 通过ImageProvider获取ui.image Future loadImageByProvider( ImageProvider provider, { ImageConfiguration config = ImageConfiguration.empty, }) async { Completer completer = Completer(); //完成的回调 ImageStreamListener listener; ImageStream stream = provider.resolve(config); //获取图片流 listener = ImageStreamListener((ImageInfo frame, bool sync) { //监听 final ui.Image image = frame.image; completer.complete(image); //完成 // stream.removeListener(listener); //移除监听 }); stream.addListener(listener); //添加监听 return completer.future; //返回 } @override void didUpdateWidget(FloatingItem oldWidget) { // TODO: implement didUpdateWidget animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 100)); /// 初始化进场动画 if (widget.isLeft) { animation = Tween(begin: -(widget.width! + 50.0), end: 0.0) .animate(animationController!) ..addListener(() { setState(() { widget.left = animation!.value; }); }); } else { animation = Tween(begin: widget.width! * 2, end: widget.width! - 50.0) .animate(animationController!) ..addListener(() { setState(() { widget.left = animation!.value; }); }); } animationControllers.add(animationController!); /// 执行进场动画 if (animationController!.status == AnimationStatus.dismissed && widget.isEntering) { animationController!.forward(); } /// 无需执行进场动画,将列表项置于动画末尾 else { animationController!.forward(from: 100.0); } super.didUpdateWidget(oldWidget); } @override void dispose() { // TODO: implement dispose /// 释放动画资源,避免内存泄漏 if (!animationController.toString().toString().contains('DISPOSED')) { animationController!.dispose(); } super.dispose(); } }