Skip to content
This repository has been archived by the owner on Jan 9, 2024. It is now read-only.

Commit

Permalink
Merge branch 'main' of https://github.com/VGVentures/dash_ai_search i…
Browse files Browse the repository at this point in the history
…nto feat/question-animation
  • Loading branch information
omartinma committed Dec 7, 2023
2 parents 59e21c9 + c5e405d commit aa3a719
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 145 deletions.
192 changes: 72 additions & 120 deletions lib/home/widgets/thinking_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand All @@ -12,6 +11,8 @@ class ThinkingView extends StatefulWidget {
State<ThinkingView> createState() => ThinkingViewState();
}

const _thinkingDuration = Duration(milliseconds: 1500);

class ThinkingViewState extends State<ThinkingView>
with TickerProviderStateMixin, TransitionScreenMixin {
late Animation<double> _opacityIn;
Expand All @@ -32,12 +33,12 @@ class ThinkingViewState extends State<ThinkingView>

enterTransitionController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
duration: _thinkingDuration,
);

exitTransitionController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
duration: _thinkingDuration,
);
}

Expand Down Expand Up @@ -77,7 +78,7 @@ class ThinkingViewState extends State<ThinkingView>
opacity: _opacityIn,
child: FadeTransition(
opacity: _opacityOut,
child: const ThinkingAnimationView(),
child: const PulseAnimationView(),
),
),
Center(
Expand All @@ -101,57 +102,86 @@ class ThinkingViewState extends State<ThinkingView>
}
}

enum ThinkingAnimationPhase {
initial,
thinkingIn,
thinkingOut,
}

class ThinkingAnimationView extends StatefulWidget {
const ThinkingAnimationView({
super.key,
@visibleForTesting this.animationState,
});

final PhasedState<ThinkingAnimationPhase>? animationState;
class PulseAnimationView extends StatefulWidget {
const PulseAnimationView({super.key});

@override
State<ThinkingAnimationView> createState() => _ThinkingAnimationViewState();
State<PulseAnimationView> createState() => _PulseAnimationViewState();
}

class _ThinkingAnimationViewState extends State<ThinkingAnimationView> {
late final _state = widget.animationState ??
PhasedState<ThinkingAnimationPhase>(
values: ThinkingAnimationPhase.values,
initialValue: ThinkingAnimationPhase.initial,
);
const _pulseDuration = Duration(milliseconds: 2000);

class _PulseAnimationViewState extends State<PulseAnimationView>
with SingleTickerProviderStateMixin {
late AnimationController pulseTransitionController;
late Animation<double> _scale;

@override
Widget build(BuildContext context) {
return BlocListener<HomeBloc, HomeState>(
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<double>(begin: 1.05, end: .6).animate(pulseTransitionController);
pulseTransitionController.repeat(reverse: true);
}
}

class ThinkingAnimation extends Phased<ThinkingAnimationPhase> {
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,
),
),
),
),
),
),
);
},
),
);
}
Expand Down Expand Up @@ -193,81 +223,3 @@ class TextArea extends StatelessWidget {
);
}
}

class CirclesAnimation extends StatelessWidget {
const CirclesAnimation({
required this.state,
super.key,
});

final PhasedState<ThinkingAnimationPhase> 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,
),
),
),
),
),
),
),
);
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class _QuestionTextFieldState extends State<QuestionInputTextField>
color: VertexColors.flutterNavy,
),
autofillHints: null,
onSubmitted: (_) => widget.onActionPressed(),
decoration: InputDecoration(
filled: true,
prefixIcon: Padding(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,28 @@ void main() {
expect(text, equals('test'));
});

testWidgets('calls onActionPressed on enter key', (tester) async {
var called = false;
await tester.pumpApp(
Material(
child: QuestionInputTextField(
icon: SizedBox.shrink(),
hint: 'hint',
actionText: 'actionText',
onActionPressed: () {
called = true;
},
onTextUpdated: (_) {},
),
),
);

await tester.enterText(find.byType(TextField), '');
await tester.testTextInput.receiveAction(TextInputAction.done);

expect(called, equals(true));
});

group('when shouldDisplayClearTextButton is false', () {
testWidgets('calls onActionPressed clicking on PrimaryCTA',
(tester) async {
Expand Down
31 changes: 6 additions & 25 deletions test/home/widgets/thinking_view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -30,15 +29,15 @@ 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);
});

testWidgets('renders correctly when in portrait mode', (tester) async {
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);
});

Expand Down Expand Up @@ -68,48 +67,30 @@ void main() {
expect(forwardExitStatuses, equals([Status.thinkingToResults]));
});

group('ThinkingAnimationView', () {
Widget bootstrap(PhasedState<ThinkingAnimationPhase> 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<ThinkingAnimationPhase>(
values: ThinkingAnimationPhase.values,
initialValue: ThinkingAnimationPhase.initial,
);
final streamController = StreamController<HomeState>();
whenListen(
homeBloc,
streamController.stream,
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),
);
},
);
});
Expand Down

0 comments on commit aa3a719

Please sign in to comment.