初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
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?> _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,
),
),
);
}
}