diff --git a/lib/pangea/analytics_summary/progress_indicator.dart b/lib/pangea/analytics_summary/progress_indicator.dart index 9ecc74f1f..ddd2af6e3 100644 --- a/lib/pangea/analytics_summary/progress_indicator.dart +++ b/lib/pangea/analytics_summary/progress_indicator.dart @@ -49,12 +49,10 @@ class ProgressIndicatorBadge extends StatelessWidget { class _AnimatedFloatingNumber extends StatefulWidget { final int number; final ProgressIndicatorEnum indicator; - final Duration duration; const _AnimatedFloatingNumber({ required this.number, required this.indicator, - this.duration = const Duration(milliseconds: 900), }); @override @@ -73,7 +71,8 @@ class _AnimatedFloatingNumberState extends State<_AnimatedFloatingNumber> @override void initState() { super.initState(); - _controller = AnimationController(vsync: this, duration: widget.duration); + _controller = AnimationController( + vsync: this, duration: const Duration(milliseconds: 900)); _fadeAnim = CurvedAnimation(parent: _controller, curve: Curves.easeOut); _offsetAnim = Tween( begin: const Offset(0, 0), diff --git a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart index 1127bfd41..cf16dfbcf 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/new_word_overlay.dart @@ -1,4 +1,5 @@ -import 'package:fluffychat/pangea/analytics_misc/gain_points_animation.dart'; +import 'dart:math'; + import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; import 'package:flutter/material.dart'; @@ -6,14 +7,14 @@ class NewWordOverlay extends StatefulWidget { final Widget child; final bool show; final Color overlayColor; - final VoidCallback? onComplete; + final GlobalKey cardKey; const NewWordOverlay({ super.key, required this.child, required this.show, required this.overlayColor, - this.onComplete, + required this.cardKey, }); @override @@ -22,125 +23,135 @@ class NewWordOverlay extends StatefulWidget { class _NewWordOverlayState extends State with TickerProviderStateMixin { - late final AnimationController _controller; - late final Animation _xpScaleAnim; - late final Animation _fadeAnim; - late final Animation _alignmentAnim; - late final Animation _offsetAnim; - bool pointsBlast = false; - Widget xpSeedWidget = const SizedBox(); + AnimationController? _controller; + Animation? _xpScaleAnim; + Animation? _fadeAnim; + Size size = const Size(0, 0); + Offset position = const Offset(0, 0); + OverlayEntry? _overlayEntry; + bool _animationStarted = false; + Widget? get svg => ConstructLevelEnum.seeds.icon(); - @override - void initState() { - super.initState(); + + void _initAndStartAnimation() { _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 2000), ); - _xpScaleAnim = CurvedAnimation( - parent: _controller, - curve: const Interval(0.0, 0.5, curve: Curves.bounceOut), + parent: _controller!, + curve: const Interval(0.0, 0.6, curve: Curves.easeInOut), ); _fadeAnim = CurvedAnimation( - parent: _controller, + parent: _controller!, curve: const Interval(0.7, 1.0, curve: Curves.easeOut), ); - _alignmentAnim = AlignmentTween( - begin: Alignment.center, - end: Alignment.topRight, - ).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.5, 1.0, curve: Curves.easeInOut), - ), - ); - - // Offset animation: stays at Offset.zero, then moves up and right - _offsetAnim = Tween( - begin: Offset.zero, - end: const Offset(-0.1, -0.1), - ).animate( - CurvedAnimation( - parent: _controller, - curve: const Interval(0.5, 1.0, curve: Curves.easeInOut), - ), - ); - - _controller.addListener(() { - if (!pointsBlast && _controller.value >= 0.6) { - setState(() { - pointsBlast = true; - }); - } + WidgetsBinding.instance.addPostFrameCallback((_) { + calculateSizeAndPosition(); + _showFlyingWidget(); + _controller?.forward(); }); + } - xpSeedWidget = Container( - child: svg, - ); - - if (mounted) { - _controller.forward(); + @override + void initState() { + super.initState(); + if (widget.show) { + _initAndStartAnimation(); + _animationStarted = true; } } @override void dispose() { - _controller.dispose(); + _overlayEntry?.remove(); + _controller?.dispose(); super.dispose(); } + void calculateSizeAndPosition() { + final RenderBox? box = + widget.cardKey.currentContext?.findRenderObject() as RenderBox?; + if (box != null) { + setState(() { + position = box.localToGlobal(const Offset(-455, 0)); + size = box.size; + }); + } + } + + void _showFlyingWidget() { + if (_controller == null || _xpScaleAnim == null || _fadeAnim == null) { + return; + } + _overlayEntry = OverlayEntry( + builder: (context) => AnimatedBuilder( + animation: _controller!, + builder: (context, child) { + final scale = _xpScaleAnim!.value; + final fade = 1.0 - (_fadeAnim!.value); + // Calculate t for move to top left after 0.7 + double t = 0.0; + if ((_controller!.value) >= 0.7) { + t = ((_controller!.value) - 0.7) / 0.3; + t = t.clamp(0.0, 1.0); + } + // Start position: center of card, End position: top left (0,0) + final startX = position.dx + size.width / 2 - (37 * scale); + final startY = position.dy + size.height / 2 + 20 - (37 * scale); + const endX = 0.0; + const endY = 0.0; + final currentX = startX * (1 - t) + endX * t; + final currentY = startY * (1 - t) + endY * t; + return Positioned( + left: currentX, + top: currentY, + child: Opacity( + opacity: fade, + child: Transform.rotate( + angle: scale * 2 * pi, + child: SizedBox( + width: 75 * scale, + height: 75 * scale, + child: svg ?? const SizedBox(), + ), + ), + ), + ); + }, + ), + ); + Overlay.of(context).insert(_overlayEntry!); + _controller?.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _overlayEntry?.remove(); + _overlayEntry = null; + } + }); + } + @override Widget build(BuildContext context) { - if (!widget.show) return widget.child; + if (!widget.show && !_animationStarted) return widget.child; return Stack( children: [ widget.child, Positioned( left: 5, right: 5, - top: 5, + top: 50, bottom: 5, - child: Stack( - children: [ - FadeTransition( - opacity: ReverseAnimation(_fadeAnim), - child: Container( - color: widget.overlayColor, - child: AnimatedBuilder( - animation: _controller, - builder: (context, child) { - return Align( - alignment: _alignmentAnim.value, - child: FractionalTranslation( - translation: _offsetAnim.value, - child: ScaleTransition( - scale: _xpScaleAnim, - child: Transform.scale( - scale: 2 * (.8 - _fadeAnim.value), - child: xpSeedWidget, - ), - ), - ), - ); - }, - ), - ), - ), - pointsBlast - ? const Align( - alignment: Alignment.bottomCenter, - child: PointsGainedAnimation( - points: 10, - targetID: "", - ), - ) - : const SizedBox.shrink(), - ], + child: FadeTransition( + opacity: ReverseAnimation(_fadeAnim ?? kAlwaysCompleteAnimation), + child: Container( + color: widget.overlayColor, + ), ), ), ], ); } } + +const kAlwaysCompleteAnimation = AlwaysStoppedAnimation(1.0); diff --git a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart index ad5957fed..64d3f8f1b 100644 --- a/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart +++ b/lib/pangea/toolbar/widgets/word_zoom/word_zoom_widget.dart @@ -43,8 +43,10 @@ class WordZoomWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final GlobalKey cardKey = GlobalKey(); final overlayColor = Theme.of(context).scaffoldBackgroundColor; final Widget card = Container( + key: cardKey, padding: const EdgeInsets.all(12.0), constraints: const BoxConstraints( minHeight: AppConfig.toolbarMinHeight - 8, @@ -244,10 +246,11 @@ class WordZoomWidget extends StatelessWidget { ), ), ); - // Only wrap in NewWordOverlay if wordIsNew is true + return NewWordOverlay( show: wordIsNew, overlayColor: overlayColor, + cardKey: cardKey, child: card, ); }