From e772eb9748789f1ff726ecb762f52a5381b87556 Mon Sep 17 00:00:00 2001 From: Hugo Walbecq Date: Wed, 14 Aug 2024 14:54:39 +0200 Subject: [PATCH 1/4] feat: animation goal progress indicator when switching theme --- lib/ui/widgets/goal_progress_indicator.dart | 75 +++++++++++++++------ 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/lib/ui/widgets/goal_progress_indicator.dart b/lib/ui/widgets/goal_progress_indicator.dart index ce14370..f70803b 100644 --- a/lib/ui/widgets/goal_progress_indicator.dart +++ b/lib/ui/widgets/goal_progress_indicator.dart @@ -5,7 +5,7 @@ import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class GoalProgressIndicator extends StatelessWidget { +class GoalProgressIndicator extends StatefulWidget { const GoalProgressIndicator({ super.key, this.size = AppSpacing.xxxlg, @@ -19,6 +19,30 @@ class GoalProgressIndicator extends StatelessWidget { final double value; final bool isGradient; + @override + State createState() => _GoalProgressIndicatorState(); +} + +class _GoalProgressIndicatorState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 1), + vsync: this, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -35,28 +59,38 @@ class GoalProgressIndicator extends StatelessWidget { if (monthlySpendingLimitGoal != 0) { value = transactions.spent.abs() / monthlySpendingLimitGoal; } - final displayValue = (value * 100).toInt(); + + _animation = Tween(begin: widget.value, end: value).animate( + _controller, + ); + _controller.forward(); return SizedBox( - height: size, - width: size, - child: CustomPaint( - painter: CircleProgressPainter( - colorScheme: coloScheme, - isGradient: isGradient, - value: value, - ), - child: Center( - child: DefaultTextStyle( - style: textTheme.titleMedium!, - textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, + height: widget.size, + width: widget.size, + child: AnimatedBuilder( + animation: _animation, + builder: (context, child) { + final displayValue = (_animation.value * 100).toInt(); + return CustomPaint( + painter: CircleProgressPainter( + colorScheme: coloScheme, + isGradient: widget.isGradient, + value: _animation.value, ), - child: Text( - '$displayValue%', + child: Center( + child: DefaultTextStyle( + style: textTheme.titleMedium!, + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + ), + child: Text( + '$displayValue%', + ), + ), ), - ), - ), + ); + }, ), ); } @@ -133,6 +167,7 @@ class CircleProgressPainter extends CustomPainter { @override bool shouldRepaint(covariant CircleProgressPainter oldDelegate) { return colorScheme != oldDelegate.colorScheme || - isGradient != oldDelegate.isGradient; + isGradient != oldDelegate.isGradient || + value != oldDelegate.value; } } From ac18ac3a2a902dc01eba57b7329b86d94ec09ed4 Mon Sep 17 00:00:00 2001 From: Hugo Walbecq Date: Wed, 14 Aug 2024 16:38:42 +0200 Subject: [PATCH 2/4] feat: update begin animation from last value if exist --- lib/ui/widgets/goal_progress_indicator.dart | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/ui/widgets/goal_progress_indicator.dart b/lib/ui/widgets/goal_progress_indicator.dart index f70803b..7937793 100644 --- a/lib/ui/widgets/goal_progress_indicator.dart +++ b/lib/ui/widgets/goal_progress_indicator.dart @@ -26,7 +26,7 @@ class GoalProgressIndicator extends StatefulWidget { class _GoalProgressIndicatorState extends State with TickerProviderStateMixin { late AnimationController _controller; - late Animation _animation; + late Animation? _animation; @override void initState() { @@ -60,23 +60,31 @@ class _GoalProgressIndicatorState extends State value = transactions.spent.abs() / monthlySpendingLimitGoal; } - _animation = Tween(begin: widget.value, end: value).animate( + var begin = 0.0; + if (_controller.isCompleted) { + begin = _animation!.value; + } + + _animation = Tween(begin: begin, end: value).animate( _controller, ); - _controller.forward(); + _controller + ..reset() + ..forward(); return SizedBox( height: widget.size, width: widget.size, child: AnimatedBuilder( - animation: _animation, + animation: _animation!, builder: (context, child) { - final displayValue = (_animation.value * 100).toInt(); + final value = _animation?.value ?? 0; + final displayValue = (value * 100).toInt(); return CustomPaint( painter: CircleProgressPainter( colorScheme: coloScheme, isGradient: widget.isGradient, - value: _animation.value, + value: value, ), child: Center( child: DefaultTextStyle( From f07e698208087ae0f4940f48bfcafd0ee75bcb40 Mon Sep 17 00:00:00 2001 From: Hugo Walbecq Date: Wed, 14 Aug 2024 17:50:57 +0200 Subject: [PATCH 3/4] test: test animation starting from last value --- lib/ui/widgets/goal_progress_indicator.dart | 16 ++---- .../widgets/goal_progress_indicator_test.dart | 57 ++++++++++++++++++- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/lib/ui/widgets/goal_progress_indicator.dart b/lib/ui/widgets/goal_progress_indicator.dart index 7937793..72c5c7b 100644 --- a/lib/ui/widgets/goal_progress_indicator.dart +++ b/lib/ui/widgets/goal_progress_indicator.dart @@ -9,14 +9,11 @@ class GoalProgressIndicator extends StatefulWidget { const GoalProgressIndicator({ super.key, this.size = AppSpacing.xxxlg, - this.value = 0, this.isGradient = false, }); final double size; - /// Value as a percentage between 0.0 and 1.0. - final double value; final bool isGradient; @override @@ -26,7 +23,7 @@ class GoalProgressIndicator extends StatefulWidget { class _GoalProgressIndicatorState extends State with TickerProviderStateMixin { late AnimationController _controller; - late Animation? _animation; + late Animation _animation; @override void initState() { @@ -62,23 +59,22 @@ class _GoalProgressIndicatorState extends State var begin = 0.0; if (_controller.isCompleted) { - begin = _animation!.value; + _controller.reset(); + begin = _animation.value; } _animation = Tween(begin: begin, end: value).animate( _controller, ); - _controller - ..reset() - ..forward(); + _controller.forward(); return SizedBox( height: widget.size, width: widget.size, child: AnimatedBuilder( - animation: _animation!, + animation: _animation, builder: (context, child) { - final value = _animation?.value ?? 0; + final value = _animation.value; final displayValue = (value * 100).toInt(); return CustomPaint( painter: CircleProgressPainter( diff --git a/test/src/ui/widgets/goal_progress_indicator_test.dart b/test/src/ui/widgets/goal_progress_indicator_test.dart index 798167d..c1b586c 100644 --- a/test/src/ui/widgets/goal_progress_indicator_test.dart +++ b/test/src/ui/widgets/goal_progress_indicator_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:bloc_test/bloc_test.dart'; import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:financial_dashboard/ui/ui.dart'; @@ -19,6 +21,59 @@ class _MockFinancialDataBloc implements FinancialDataBloc {} void main() { + group('$GoalProgressIndicator', () { + late FinancialDataBloc financialDataBloc; + + setUp(() { + financialDataBloc = _MockFinancialDataBloc(); + }); + + testWidgets('animates from last value if present', (tester) async { + final streamController = StreamController(); + final beginState = FinancialDataState( + currentSavings: 12456, + savingsDataPoints: createSampleData(), + monthlySpendingLimitGoal: 100, + transactions: createSampleTransactions(), + ); + final endState = FinancialDataState( + currentSavings: 12456, + savingsDataPoints: createSampleData(), + monthlySpendingLimitGoal: 1000, + transactions: createSampleTransactions(), + ); + whenListen( + financialDataBloc, + streamController.stream, + initialState: beginState, + ); + + var value = beginState.transactions.spent.abs() / + beginState.monthlySpendingLimitGoal; + var displayValue = (value * 100).toInt(); + + await tester.pumpExperience( + GoalProgressIndicator(), + financialDataBloc: financialDataBloc, + ); + + await tester.pumpAndSettle(); + expect(find.text('$displayValue%'), findsOneWidget); + + streamController.add(endState); + + await tester.pump(); + expect(find.text('$displayValue%'), findsOneWidget); + + await tester.pumpAndSettle(); + + value = + endState.transactions.spent.abs() / endState.monthlySpendingLimitGoal; + displayValue = (value * 100).toInt(); + expect(find.text('$displayValue%'), findsOneWidget); + }); + }); + group('CircleProgressPainter', () { late FinancialDataBloc financialDataBloc; @@ -36,7 +91,7 @@ void main() { group('$GoalProgressIndicator', () { testWidgets('renders without gradient', (tester) async { await tester.pumpExperience( - GoalProgressIndicator(value: 1), + GoalProgressIndicator(), financialDataBloc: financialDataBloc, ); await tester.pumpAndSettle(); From cdc5717c1e4956d68cd8e4b9afce27bce9052539 Mon Sep 17 00:00:00 2001 From: Hugo Walbecq Date: Wed, 14 Aug 2024 17:55:00 +0200 Subject: [PATCH 4/4] feat: replace controller reset call --- lib/ui/widgets/goal_progress_indicator.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/widgets/goal_progress_indicator.dart b/lib/ui/widgets/goal_progress_indicator.dart index 72c5c7b..fd062d4 100644 --- a/lib/ui/widgets/goal_progress_indicator.dart +++ b/lib/ui/widgets/goal_progress_indicator.dart @@ -59,8 +59,8 @@ class _GoalProgressIndicatorState extends State var begin = 0.0; if (_controller.isCompleted) { - _controller.reset(); begin = _animation.value; + _controller.reset(); } _animation = Tween(begin: begin, end: value).animate(