初始化
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user