diff --git a/lib/home/widgets/thinking_view.dart b/lib/home/widgets/thinking_view.dart index 9248129..04c94bb 100644 --- a/lib/home/widgets/thinking_view.dart +++ b/lib/home/widgets/thinking_view.dart @@ -3,7 +3,6 @@ import 'package:dash_ai_search/home/home.dart'; import 'package:dash_ai_search/l10n/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:phased/phased.dart'; class ThinkingView extends StatefulWidget { const ThinkingView({super.key}); @@ -12,6 +11,8 @@ class ThinkingView extends StatefulWidget { State createState() => ThinkingViewState(); } +const _thinkingDuration = Duration(milliseconds: 1500); + class ThinkingViewState extends State with TickerProviderStateMixin, TransitionScreenMixin { late Animation _opacityIn; @@ -32,12 +33,12 @@ class ThinkingViewState extends State enterTransitionController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 1500), + duration: _thinkingDuration, ); exitTransitionController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 1500), + duration: _thinkingDuration, ); } @@ -77,7 +78,7 @@ class ThinkingViewState extends State opacity: _opacityIn, child: FadeTransition( opacity: _opacityOut, - child: const ThinkingAnimationView(), + child: const PulseAnimationView(), ), ), Center( @@ -101,57 +102,86 @@ class ThinkingViewState extends State } } -enum ThinkingAnimationPhase { - initial, - thinkingIn, - thinkingOut, -} - -class ThinkingAnimationView extends StatefulWidget { - const ThinkingAnimationView({ - super.key, - @visibleForTesting this.animationState, - }); - - final PhasedState? animationState; +class PulseAnimationView extends StatefulWidget { + const PulseAnimationView({super.key}); @override - State createState() => _ThinkingAnimationViewState(); + State createState() => _PulseAnimationViewState(); } -class _ThinkingAnimationViewState extends State { - late final _state = widget.animationState ?? - PhasedState( - values: ThinkingAnimationPhase.values, - initialValue: ThinkingAnimationPhase.initial, - ); +const _pulseDuration = Duration(milliseconds: 2000); + +class _PulseAnimationViewState extends State + with SingleTickerProviderStateMixin { + late AnimationController pulseTransitionController; + late Animation _scale; @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - if (state.status == Status.thinkingToResults) { - _state.value = ThinkingAnimationPhase.thinkingOut; - } - }, - child: ThinkingAnimation( - state: _state, - ), + void initState() { + super.initState(); + + pulseTransitionController = AnimationController( + vsync: this, + duration: _pulseDuration, ); + + _scale = + Tween(begin: 1.05, end: .6).animate(pulseTransitionController); + pulseTransitionController.repeat(reverse: true); } -} -class ThinkingAnimation extends Phased { - const ThinkingAnimation({ - required super.state, - super.key, - }); + @override + void dispose() { + pulseTransitionController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { + const backgroundColor = Colors.transparent; + const borderColor = VertexColors.googleBlue; + return Align( - child: CirclesAnimation( - state: state, + child: LayoutBuilder( + builder: (context, constraints) { + final viewport = constraints.maxHeight < constraints.maxWidth + ? constraints.maxHeight + : constraints.maxWidth; + + final bigCircleRadius = viewport / 2; + final mediumCircleRadius = bigCircleRadius * .59; + final smallCircleRadius = bigCircleRadius * .27; + + return SizedBox( + width: viewport, + height: viewport, + child: ScaleTransition( + scale: _scale, + child: Circle( + dotted: true, + backgroundColor: backgroundColor, + borderColor: borderColor, + radius: bigCircleRadius, + child: Center( + child: Circle( + dotted: true, + backgroundColor: backgroundColor, + borderColor: borderColor, + radius: mediumCircleRadius, + child: Center( + child: Circle( + dotted: true, + backgroundColor: backgroundColor, + borderColor: borderColor, + radius: smallCircleRadius, + ), + ), + ), + ), + ), + ), + ); + }, ), ); } @@ -193,81 +223,3 @@ class TextArea extends StatelessWidget { ); } } - -class CirclesAnimation extends StatelessWidget { - const CirclesAnimation({ - required this.state, - super.key, - }); - - final PhasedState state; - - @override - Widget build(BuildContext context) { - const backgroundColor = Colors.transparent; - const borderColor = VertexColors.googleBlue; - const opacityDuration = Duration(milliseconds: 800); - - return LayoutBuilder( - builder: (context, constraints) { - final viewport = constraints.maxHeight < constraints.maxWidth - ? constraints.maxHeight - : constraints.maxWidth; - - final bigCircleRadius = viewport / 2; - final mediumCircleRadius = bigCircleRadius * .59; - final smallCircleRadius = bigCircleRadius * .27; - - const scaleDuration = Duration(seconds: 2); - - return SizedBox( - width: viewport, - height: viewport, - child: AnimatedOpacity( - duration: opacityDuration, - opacity: state.phaseValue( - values: { - ThinkingAnimationPhase.initial: .8, - ThinkingAnimationPhase.thinkingOut: 0, - }, - defaultValue: 1, - ), - child: AnimatedScale( - duration: scaleDuration, - curve: Curves.decelerate, - scale: state.phaseValue( - values: { - ThinkingAnimationPhase.initial: .6, - ThinkingAnimationPhase.thinkingOut: .6, - }, - defaultValue: 1.05, - ), - child: Circle( - dotted: true, - backgroundColor: backgroundColor, - borderColor: borderColor, - radius: bigCircleRadius, - child: Center( - child: Circle( - dotted: true, - backgroundColor: backgroundColor, - borderColor: borderColor, - radius: mediumCircleRadius, - child: Center( - child: Circle( - dotted: true, - backgroundColor: backgroundColor, - borderColor: borderColor, - radius: smallCircleRadius, - ), - ), - ), - ), - ), - ), - ), - ); - }, - ); - } -} diff --git a/test/home/widgets/thinking_view_test.dart b/test/home/widgets/thinking_view_test.dart index 45e498e..4fd91e0 100644 --- a/test/home/widgets/thinking_view_test.dart +++ b/test/home/widgets/thinking_view_test.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:phased/phased.dart'; import '../../helpers/helpers.dart'; @@ -30,7 +29,7 @@ void main() { testWidgets('renders correctly', (tester) async { await tester.pumpApp(bootstrap()); - expect(find.byType(CirclesAnimation), findsOneWidget); + expect(find.byType(PulseAnimationView), findsOneWidget); expect(find.byType(TextArea), findsOneWidget); }); @@ -38,7 +37,7 @@ void main() { tester.setViewSize(size: Size(600, 800)); await tester.pumpApp(bootstrap()); - expect(find.byType(CirclesAnimation), findsOneWidget); + expect(find.byType(PulseAnimationView), findsOneWidget); expect(find.byType(TextArea), findsOneWidget); }); @@ -68,24 +67,17 @@ void main() { expect(forwardExitStatuses, equals([Status.thinkingToResults])); }); - group('ThinkingAnimationView', () { - Widget bootstrap(PhasedState state) => - BlocProvider.value( + group('PulseAnimationView', () { + Widget bootstrap() => BlocProvider.value( value: homeBloc, child: Material( - child: ThinkingAnimationView( - animationState: state, - ), + child: PulseAnimationView(), ), ); testWidgets( 'animation changes correctly', (tester) async { - final animationState = PhasedState( - values: ThinkingAnimationPhase.values, - initialValue: ThinkingAnimationPhase.initial, - ); final streamController = StreamController(); whenListen( homeBloc, @@ -93,23 +85,12 @@ void main() { initialState: const HomeState(), ); - expect(animationState.value, equals(ThinkingAnimationPhase.initial)); - await tester.pumpApp(bootstrap(animationState)); - - expect( - animationState.value, - equals(ThinkingAnimationPhase.thinkingIn), - ); + await tester.pumpApp(bootstrap()); streamController.add( const HomeState(status: Status.thinkingToResults), ); await tester.pump(); - - expect( - animationState.value, - equals(ThinkingAnimationPhase.thinkingOut), - ); }, ); });