diff --git a/lib/home/bloc/home_state.dart b/lib/home/bloc/home_state.dart index 515a068..fc4e056 100644 --- a/lib/home/bloc/home_state.dart +++ b/lib/home/bloc/home_state.dart @@ -72,39 +72,6 @@ class HomeState extends Equatable { return ParsedSummary(elements: elements); } - bool get isWelcomeVisible => - status == Status.welcome || status == Status.welcomeToAskQuestion; - bool get isQuestionVisible => - status == Status.welcomeToAskQuestion || status == Status.askQuestion; - bool get isThinkingVisible => - status == Status.askQuestionToThinking || - status == Status.thinking || - status == Status.thinkingToResults || - status == Status.resultsToThinking; - bool get isResultsVisible => - status == Status.thinkingToResults || - status == Status.results || - status == Status.resultsToSourceAnswers || - status == Status.seeSourceAnswers || - status == Status.sourceAnswersBackToResults; - bool get isMovingToSeeSourceAnswers => - status == Status.resultsToSourceAnswers || - status == Status.seeSourceAnswers || - status == Status.sourceAnswersBackToResults; - bool get isSeeSourceAnswersVisible => status == Status.seeSourceAnswers; - bool get isDashOnLeft => - status == Status.welcome || - status == Status.welcomeToAskQuestion || - status == Status.askQuestion || - status == Status.askQuestionToThinking || - status == Status.thinking || - status == Status.thinkingToResults || - status == Status.results; - bool get isDashOnRight => - status == Status.resultsToSourceAnswers || - status == Status.seeSourceAnswers || - status == Status.sourceAnswersBackToResults; - HomeState copyWith({ Status? status, String? query, @@ -133,3 +100,40 @@ class HomeState extends Equatable { answerFeedbacks, ]; } + +extension StatusX on Status { + bool get isWelcomeVisible => + this == Status.welcome || this == Status.welcomeToAskQuestion; + bool get isQuestionVisible => + this == Status.welcomeToAskQuestion || + this == Status.askQuestion || + this == Status.askQuestionToThinking; + bool get isThinkingVisible => + this == Status.askQuestionToThinking || + this == Status.thinking || + this == Status.thinkingToResults || + this == Status.resultsToThinking; + bool get isResultsVisible => + this == Status.thinkingToResults || + this == Status.results || + this == Status.resultsToSourceAnswers || + this == Status.seeSourceAnswers || + this == Status.sourceAnswersBackToResults; + bool get isMovingToSeeSourceAnswers => + this == Status.resultsToSourceAnswers || + this == Status.seeSourceAnswers || + this == Status.sourceAnswersBackToResults; + bool get isSeeSourceAnswersVisible => this == Status.seeSourceAnswers; + bool get isDashOnLeft => + this == Status.welcome || + this == Status.welcomeToAskQuestion || + this == Status.askQuestion || + this == Status.askQuestionToThinking || + this == Status.thinking || + this == Status.thinkingToResults || + this == Status.results; + bool get isDashOnRight => + this == Status.resultsToSourceAnswers || + this == Status.seeSourceAnswers || + this == Status.sourceAnswersBackToResults; +} diff --git a/lib/home/view/home_page.dart b/lib/home/view/home_page.dart index 076b450..549b975 100644 --- a/lib/home/view/home_page.dart +++ b/lib/home/view/home_page.dart @@ -25,13 +25,13 @@ class HomeView extends StatelessWidget { @override Widget build(BuildContext context) { - final state = context.watch().state; + final status = context.select((HomeBloc bloc) => bloc.state.status); return Scaffold( backgroundColor: VertexColors.arctic, body: Stack( children: [ - if (state.isWelcomeVisible) + if (status.isWelcomeVisible) const Positioned( top: 0, bottom: 0, @@ -39,16 +39,16 @@ class HomeView extends StatelessWidget { right: 0, child: Background(), ), - if (state.isWelcomeVisible) const WelcomeView(), - if (state.isQuestionVisible) const QuestionView(), - if (state.isThinkingVisible) const ThinkingView(), - if (state.isResultsVisible) const ResultsView(), + if (status.isWelcomeVisible) const WelcomeView(), + if (status.isThinkingVisible) const ThinkingView(), + if (status.isQuestionVisible) const QuestionView(), + if (status.isResultsVisible) const ResultsView(), Positioned( top: 40, left: 48, - child: Logo(hasDarkBackground: state.isSeeSourceAnswersVisible), + child: Logo(hasDarkBackground: status.isSeeSourceAnswersVisible), ), - if (state.isDashOnRight) + if (status.isDashOnRight) const Positioned( bottom: 50, right: 50, @@ -57,7 +57,7 @@ class HomeView extends StatelessWidget { key: _dashRightKey, ), ), - if (state.isDashOnLeft) + if (status.isDashOnLeft) const Positioned( bottom: 50, left: 50, diff --git a/lib/home/widgets/question_view.dart b/lib/home/widgets/question_view.dart index c11ee8c..299ccb5 100644 --- a/lib/home/widgets/question_view.dart +++ b/lib/home/widgets/question_view.dart @@ -13,18 +13,22 @@ class QuestionView extends StatefulWidget { class QuestionViewState extends State with TickerProviderStateMixin, TransitionScreenMixin { - late Animation _opacity; + late Animation _offsetVerticalIn; + late Animation _offsetVerticalOut; @override List get forwardEnterStatuses => [Status.welcomeToAskQuestion]; + @override + List get forwardExitStatuses => [Status.askQuestionToThinking]; + @override void initializeTransitionController() { super.initializeTransitionController(); enterTransitionController = AnimationController( vsync: this, - duration: const Duration(seconds: 1), + duration: const Duration(milliseconds: 1500), )..addStatusListener((status) { if (status == AnimationStatus.completed) { context.read().add(const AskQuestion()); @@ -33,29 +37,29 @@ class QuestionViewState extends State exitTransitionController = AnimationController( vsync: this, - duration: const Duration(seconds: 1), + duration: const Duration(milliseconds: 1500), ); } @override void initState() { super.initState(); + _offsetVerticalIn = + Tween(begin: const Offset(0, 1), end: Offset.zero).animate( + CurvedAnimation( + parent: enterTransitionController, + curve: Curves.decelerate, + ), + ); - _opacity = - Tween(begin: 0, end: 1).animate(enterTransitionController); - } - - @override - Widget build(BuildContext context) { - return FadeTransition( - opacity: _opacity, - child: const _QuestionView(), + _offsetVerticalOut = + Tween(begin: Offset.zero, end: const Offset(0, -1.5)).animate( + CurvedAnimation( + parent: exitTransitionController, + curve: Curves.decelerate, + ), ); } -} - -class _QuestionView extends StatelessWidget { - const _QuestionView(); @override Widget build(BuildContext context) { @@ -68,14 +72,27 @@ class _QuestionView extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - l10n.questionScreenTitle, - textAlign: TextAlign.center, - style: textTheme.displayLarge - ?.copyWith(color: VertexColors.flutterNavy), + ClipRRect( + child: SlideTransition( + position: _offsetVerticalIn, + child: SlideTransition( + position: _offsetVerticalOut, + child: Text( + l10n.questionScreenTitle, + textAlign: TextAlign.center, + style: textTheme.displayLarge + ?.copyWith(color: VertexColors.flutterNavy), + ), + ), + ), ), const SizedBox(height: 40), - const SearchBox(), + ClipRRect( + child: SlideTransition( + position: _offsetVerticalOut, + child: const SearchBox(), + ), + ), ], ), ), diff --git a/lib/home/widgets/results_view.dart b/lib/home/widgets/results_view.dart index 209859e..a2999cd 100644 --- a/lib/home/widgets/results_view.dart +++ b/lib/home/widgets/results_view.dart @@ -70,7 +70,7 @@ class _ResultsView extends StatelessWidget { @override Widget build(BuildContext context) { - final state = context.watch().state; + final status = context.select((HomeBloc bloc) => bloc.state.status); final response = context.select((HomeBloc bloc) => bloc.state.vertexResponse); @@ -89,7 +89,7 @@ class _ResultsView extends StatelessWidget { child: SearchBoxView(), ), ), - if (state.isMovingToSeeSourceAnswers) + if (status.isMovingToSeeSourceAnswers) Positioned( top: _questionBoxHeight + _searchBarTopPadding + 32, right: 100, @@ -429,7 +429,7 @@ class _AiResponseState extends State<_AiResponse> ); }, ), - if (!state.isSeeSourceAnswersVisible) + if (!state.status.isSeeSourceAnswersVisible) const SeeSourceAnswersButton(), ], ), diff --git a/lib/home/widgets/thinking_view.dart b/lib/home/widgets/thinking_view.dart index af8a563..9248129 100644 --- a/lib/home/widgets/thinking_view.dart +++ b/lib/home/widgets/thinking_view.dart @@ -14,39 +14,89 @@ class ThinkingView extends StatefulWidget { class ThinkingViewState extends State with TickerProviderStateMixin, TransitionScreenMixin { - late Animation _opacity; + late Animation _opacityIn; + late Animation _opacityOut; + late Animation _offsetVerticalIn; + late Animation _offsetVerticalOut; @override List get forwardEnterStatuses => [Status.askQuestionToThinking, Status.resultsToThinking]; + @override + List get forwardExitStatuses => [Status.thinkingToResults]; + @override void initializeTransitionController() { super.initializeTransitionController(); enterTransitionController = AnimationController( vsync: this, - duration: const Duration(seconds: 1), + duration: const Duration(milliseconds: 1500), ); exitTransitionController = AnimationController( vsync: this, - duration: const Duration(seconds: 1), + duration: const Duration(milliseconds: 1500), ); } @override void initState() { super.initState(); - _opacity = + _opacityIn = Tween(begin: 0, end: 1).animate(enterTransitionController); + + _opacityOut = + Tween(begin: 1, end: 0).animate(exitTransitionController); + + _offsetVerticalIn = + Tween(begin: const Offset(0, 1), end: Offset.zero).animate( + CurvedAnimation( + parent: enterTransitionController, + curve: Curves.decelerate, + ), + ); + + _offsetVerticalOut = + Tween(begin: Offset.zero, end: const Offset(0, -1.5)).animate( + CurvedAnimation( + parent: exitTransitionController, + curve: Curves.decelerate, + ), + ); } @override Widget build(BuildContext context) { - return FadeTransition( - opacity: _opacity, - child: const ThinkingAnimationView(), + final query = context.select((HomeBloc bloc) => bloc.state.query); + + return Stack( + children: [ + FadeTransition( + opacity: _opacityIn, + child: FadeTransition( + opacity: _opacityOut, + child: const ThinkingAnimationView(), + ), + ), + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ClipRRect( + child: SlideTransition( + position: _offsetVerticalIn, + child: SlideTransition( + position: _offsetVerticalOut, + child: TextArea(query: query), + ), + ), + ), + ], + ), + ), + ], ); } } @@ -99,77 +149,47 @@ class ThinkingAnimation extends Phased { @override Widget build(BuildContext context) { - final query = context.select((HomeBloc bloc) => bloc.state.query); - return Stack( - children: [ - Align( - child: CirclesAnimation( - state: state, - ), - ), - Align( - child: TextArea(query: query, state: state), - ), - ], + return Align( + child: CirclesAnimation( + state: state, + ), ); } } class TextArea extends StatelessWidget { @visibleForTesting - const TextArea({required this.query, required this.state, super.key}); + const TextArea({required this.query, super.key}); final String query; - final PhasedState state; @override Widget build(BuildContext context) { final l10n = context.l10n; final textTheme = Theme.of(context).textTheme; - const slideDuration = Duration(milliseconds: 1200); - const opacityDuration = Duration(milliseconds: 800); - return AnimatedOpacity( - duration: opacityDuration, - opacity: state.phaseValue( - values: { - ThinkingAnimationPhase.thinkingOut: 0, - }, - defaultValue: 1, - ), - child: AnimatedSlide( - curve: Curves.decelerate, - duration: slideDuration, - offset: state.phaseValue( - values: { - ThinkingAnimationPhase.initial: const Offset(0, 1), - }, - defaultValue: Offset.zero, + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.thinkingHeadline, + textAlign: TextAlign.center, + style: + textTheme.bodyMedium?.copyWith(color: VertexColors.flutterNavy), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - l10n.thinkingHeadline, - textAlign: TextAlign.center, - style: textTheme.bodyMedium - ?.copyWith(color: VertexColors.flutterNavy), - ), - const SizedBox( - height: 30, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 300), - child: Text( - query, - textAlign: TextAlign.center, - style: textTheme.displayLarge - ?.copyWith(color: VertexColors.flutterNavy), - ), - ), - ], + const SizedBox( + height: 30, ), - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 300), + child: Text( + query, + textAlign: TextAlign.center, + style: textTheme.displayLarge + ?.copyWith(color: VertexColors.flutterNavy), + ), + ), + ], ); } } diff --git a/lib/home/widgets/welcome_view.dart b/lib/home/widgets/welcome_view.dart index 5451254..2cbfe26 100644 --- a/lib/home/widgets/welcome_view.dart +++ b/lib/home/widgets/welcome_view.dart @@ -13,8 +13,8 @@ class WelcomeView extends StatefulWidget { class WelcomeViewState extends State with TickerProviderStateMixin, TransitionScreenMixin { - late Animation _offset; - late Animation _opacity; + late Animation _offsetIn; + late Animation _offsetOut; @override List get forwardEnterStatuses => [Status.welcome]; @@ -28,12 +28,12 @@ class WelcomeViewState extends State enterTransitionController = AnimationController( vsync: this, - duration: const Duration(seconds: 1), + duration: const Duration(milliseconds: 1500), ); exitTransitionController = AnimationController( vsync: this, - duration: const Duration(seconds: 1), + duration: const Duration(milliseconds: 1200), ); } @@ -41,57 +41,69 @@ class WelcomeViewState extends State void initState() { super.initState(); - _offset = Tween(begin: const Offset(0, 1), end: Offset.zero) - .animate(enterTransitionController); - - _opacity = - Tween(begin: 1, end: 0).animate(exitTransitionController); - } + _offsetIn = + Tween(begin: const Offset(0, 1), end: Offset.zero).animate( + CurvedAnimation( + parent: enterTransitionController, + curve: Curves.decelerate, + ), + ); - @override - Widget build(BuildContext context) { - return SlideTransition( - position: _offset, - child: FadeTransition( - opacity: _opacity, - child: const _WelcomeView(), + _offsetOut = + Tween(begin: Offset.zero, end: const Offset(0, -1.5)).animate( + CurvedAnimation( + parent: exitTransitionController, + curve: Curves.decelerate, ), ); } -} - -class _WelcomeView extends StatelessWidget { - const _WelcomeView(); @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; final l10n = context.l10n; - final state = context.watch().state; + final status = context.select((HomeBloc bloc) => bloc.state.status); return IgnorePointer( - ignoring: !state.isWelcomeVisible, + ignoring: !status.isWelcomeVisible, child: Center( child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - l10n.initialScreenTitle, - textAlign: TextAlign.center, - style: textTheme.displayLarge?.copyWith( - color: VertexColors.flutterNavy, + ClipRRect( + child: SlideTransition( + position: _offsetIn, + child: SlideTransition( + position: _offsetOut, + child: Text( + l10n.initialScreenTitle, + textAlign: TextAlign.center, + style: textTheme.displayLarge?.copyWith( + color: VertexColors.flutterNavy, + ), + ), + ), ), ), const SizedBox(height: 40), - PrimaryIconCTA( - icon: vertexIcons.arrowForward.image( - color: VertexColors.googleBlue, + ClipRRect( + child: SlideTransition( + position: _offsetIn, + child: SlideTransition( + position: _offsetOut, + child: PrimaryIconCTA( + icon: vertexIcons.arrowForward.image( + color: VertexColors.googleBlue, + ), + label: l10n.startAsking, + onPressed: () => context + .read() + .add(const FromWelcomeToQuestion()), + ), + ), ), - label: l10n.startAsking, - onPressed: () => - context.read().add(const FromWelcomeToQuestion()), ), ], ), diff --git a/test/home/widgets/question_view_test.dart b/test/home/widgets/question_view_test.dart index 66e0fbf..3334d54 100644 --- a/test/home/widgets/question_view_test.dart +++ b/test/home/widgets/question_view_test.dart @@ -44,6 +44,16 @@ void main() { expect(forwardEnterStatuses, equals([Status.welcomeToAskQuestion])); }); + testWidgets('animates out when exit', (tester) async { + await tester.pumpApp(bootstrap()); + + final forwardExitStatuses = tester + .state(find.byType(QuestionView)) + .forwardExitStatuses; + + expect(forwardExitStatuses, equals([Status.askQuestionToThinking])); + }); + testWidgets( 'calls AskQuestion writing on enter', (WidgetTester tester) async { diff --git a/test/home/widgets/thinking_view_test.dart b/test/home/widgets/thinking_view_test.dart index 313ef71..45e498e 100644 --- a/test/home/widgets/thinking_view_test.dart +++ b/test/home/widgets/thinking_view_test.dart @@ -58,6 +58,16 @@ void main() { ); }); + testWidgets('animates out when exit', (tester) async { + await tester.pumpApp(bootstrap()); + + final forwardExitStatuses = tester + .state(find.byType(ThinkingView)) + .forwardExitStatuses; + + expect(forwardExitStatuses, equals([Status.thinkingToResults])); + }); + group('ThinkingAnimationView', () { Widget bootstrap(PhasedState state) => BlocProvider.value(