import 'package:flutter/material.dart'; /// 「加入购物车」式飞行动画:把某个组件的副本从原位置缩小飞到目标位置。 /// /// 挂在 Overlay 上而不是弹窗内部——先测量、再关弹窗、最后飞,这样蒙层干净消失, /// 飞行层浮在真实页面之上;放弹窗里做的话背景遮罩会一直压着,不像"收进去"。 class FlyToOverlay { FlyToOverlay._(); /// [context] 调用方(弹窗)的 context,用来找根 Overlay —— 不能用 Get.overlayContext, /// 那拿到的是 Overlay 自身的 context,而 Overlay.of 只往祖先找,必然抛 "No Overlay widget found" /// [sourceKey] 起飞组件(**必须在关闭弹窗前调用**,否则拿不到位置) /// [target] 目标区域(屏幕坐标);[child] 飞行途中显示的内容,通常是起飞组件的副本 static void play({ required BuildContext context, required GlobalKey sourceKey, required Rect target, required Widget child, Duration duration = const Duration(milliseconds: 700), }) { final box = sourceKey.currentContext?.findRenderObject() as RenderBox?; if (box == null || !box.hasSize) return; // 量不到就不飞,不能因为动画报错 final begin = box.localToGlobal(Offset.zero) & box.size; // rootOverlay:挂到最顶层,弹窗 pop 掉之后飞行层还得继续存活 final overlay = Overlay.maybeOf(context, rootOverlay: true); if (overlay == null) return; late OverlayEntry entry; entry = OverlayEntry( builder: (_) => _FlyView( begin: begin, end: target, duration: duration, // Overlay 被整体销毁时 entry 已不在树上,再 remove 会踩 assert onDone: () { if (entry.mounted) entry.remove(); }, child: child, ), ); overlay.insert(entry); } } class _FlyView extends StatefulWidget { final Rect begin; final Rect end; final Duration duration; final VoidCallback onDone; final Widget child; const _FlyView({ required this.begin, required this.end, required this.duration, required this.onDone, required this.child, }); @override State<_FlyView> createState() => _FlyViewState(); } class _FlyViewState extends State<_FlyView> with SingleTickerProviderStateMixin { late final AnimationController _ctr = AnimationController(vsync: this, duration: widget.duration); // easeInOutCubic:起步慢、中段快、**末尾减速**。不用 easeIn 系是因为那样末尾越飞越快, // 到落点时一闪而过,用户来不及把弹窗和浮窗对应起来 late final Animation _rect = RectTween(begin: widget.begin, end: widget.end) .animate(CurvedAnimation(parent: _ctr, curve: Curves.easeInOutCubic)); @override void initState() { super.initState(); _ctr.addStatusListener((s) { if (s == AnimationStatus.completed) widget.onDone(); }); _ctr.forward(); } @override void dispose() { _ctr.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _ctr, builder: (_, child) { final r = _rect.value ?? widget.begin; // 全程基本保持不透明,只在最后 12% 收尾淡出:太早淡出会让人看不清落到哪儿了 final t = _ctr.value; final opacity = t < 0.88 ? 1.0 : (1 - (t - 0.88) / 0.12).clamp(0.0, 1.0); return Positioned( left: r.left, top: r.top, width: r.width, height: r.height, child: IgnorePointer( child: Opacity(opacity: opacity, child: child), ), ); }, // 内容按起飞尺寸渲染一次,交给 FittedBox 等比缩放,避免每帧重新布局图片 child: FittedBox( fit: BoxFit.fill, child: SizedBox( width: widget.begin.width, height: widget.begin.height, child: widget.child, ), ), ); } }