seed spins in and fades to top left to draw attention to vocab increment
+ minor refactoring
This commit is contained in:
parent
16ea5ea563
commit
9afee1cca1
3 changed files with 106 additions and 93 deletions
|
|
@ -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<Offset>(
|
||||
begin: const Offset(0, 0),
|
||||
|
|
|
|||
|
|
@ -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<NewWordOverlay>
|
||||
with TickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
late final Animation<double> _xpScaleAnim;
|
||||
late final Animation<double> _fadeAnim;
|
||||
late final Animation<Alignment> _alignmentAnim;
|
||||
late final Animation<Offset> _offsetAnim;
|
||||
bool pointsBlast = false;
|
||||
Widget xpSeedWidget = const SizedBox();
|
||||
AnimationController? _controller;
|
||||
Animation<double>? _xpScaleAnim;
|
||||
Animation<double>? _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<Offset>(
|
||||
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<double>(1.0);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue