Skip to content

Commit 41e590b

Browse files
committed
feat(navigation): add animations and transitions
1 parent 71f80d0 commit 41e590b

17 files changed

+238
-27
lines changed

docs/NAVIGATION.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,16 @@ To add a new route in the app, follow these steps:
107107
4. Document the new route in the [SCREENS.md](/docs/SCREENS.md) file.
108108
5. Navigate to the route using `MyNewRoute().go(context)`.
109109

110-
## TODO: Animations
110+
## Animations
111+
112+
Page transitions can be animated by overriding the `buildPage` method in the route class.
113+
114+
The [animations](https://pub.dev/packages/animations) is a 1st-party Flutter package that provides a collection of ready-made Material animations that can be used to animate the page transitions.
115+
116+
A helper class [PageTransitions](/lib/app/navigation/router/page_transitions.dart) is provided to capture and re-use the different page transitions in the app.
117+
118+
> ♻️ When navigating to a new route with the same page transition as the current one, the animation will not be triggered because the widget tree remains the same. To avoid this, use the `key` parameter in the animation widget to tell Flutter that it is a new widget and the animation should be triggered for the new page as well.
119+
120+
Branch animations can be provided using [flutter_animate](https://pub.dev/packages/flutter_animate) or regular Flutter widgets. Add the animation directly on the build method of the page widget.
121+
122+
The [AnimatedBranchContainer](/lib/presentation/shared/design_system/utils/animated_branch_container.dart) widget provides a fade animation when switching between branches.

docs/TOOLING.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Accessibility can also be tested on iOS by using the [XCode Accessibility Scanne
104104

105105
The app has two themes: Dark and Light. These are applied throughout the app in an interactive way via [AdaptiveThemeCubit](/lib/presentation/shared/adaptive_theme/adaptive_theme_cubit.dart).
106106

107-
The app will switch to the corresponding theme depending on the currently displayed screen via [ThemeRouteListener](/lib/app/navigation/listener/theme_route_listener.dart).
107+
To switch between themes, create a new RouteListener and set the new theme using `AdaptiveThemeCubit.of(context).setLight/DarkTheme` based on the constant route `name` or `path` values.
108108

109109
### Theme Extensions
110110

@@ -325,6 +325,8 @@ This page is entirely optional and is no longer part of the OS app launch proces
325325

326326
This project uses [flutter_animate](https://pub.dev/flutter_animate) package to add animations to Widgets. To use it, simply import the package and add `animate()` method at the end of the Widget that will be animated. Add the animation effect inside the `effects` parameter.
327327

328+
A constants class [Anims](/lib/presentation/shared/design_system/theme/anims.dart) is provided to define the different the Duration and Curves used for all animations in the app.
329+
328330
### App version
329331

330332
Make sure to add the app version somewhere on the user settings/profile so we can communicate more effectively with users. You can use the [DSAppVersion](/lib/presentation/shared/design_system/views/ds_app_version.dart) widget for that.

lib/app/navigation/redirect/console_route_redirect.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22

33
import 'package:flutter/widgets.dart';
44
import 'package:flutter_template/app/navigation/redirect/route_redirect.dart';
5+
import 'package:flutter_template/app/navigation/router/app_routes.dart';
56
import 'package:go_router/go_router.dart';
67

78
class ConsoleRouteRedirect implements RouteRedirect {
@@ -16,7 +17,8 @@ class ConsoleRouteRedirect implements RouteRedirect {
1617
BuildContext context,
1718
GoRouterState state,
1819
) {
19-
if (state.matchedLocation == "/console" && !internalBuild) {
20+
if (state.matchedLocation.startsWith(const ConsoleRoute().location) &&
21+
internalBuild) {
2022
// Redirect console to default location if not allowed
2123
return "/";
2224
}

lib/app/navigation/router/app_routes.dart

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/widgets.dart';
22
import 'package:flutter_template/app/navigation/navigator_holder.dart';
3+
import 'package:flutter_template/app/navigation/router/page_transitions.dart';
34
import 'package:flutter_template/presentation/articles/articles_page.dart';
45
import 'package:flutter_template/presentation/articles/blank_page.dart';
56
import 'package:flutter_template/presentation/articles/detail/article_detail_page.dart';
@@ -245,8 +246,13 @@ class SettingsRoute extends GoRouteData {
245246
const SettingsRoute();
246247

247248
@override
248-
Widget build(BuildContext context, GoRouterState state) {
249-
return const SettingsPage();
249+
Page<void> buildPage(BuildContext context, GoRouterState state) {
250+
return PageTransitions.sharedAxisX(
251+
context: context,
252+
state: state,
253+
key: const ValueKey("SettingsRouteTransition"),
254+
child: const SettingsPage(),
255+
);
250256
}
251257
}
252258

@@ -256,6 +262,18 @@ class AccountDetailsRoute extends GoRouteData {
256262
this.name,
257263
});
258264

265+
@override
266+
Page<void> buildPage(BuildContext context, GoRouterState state) {
267+
return PageTransitions.sharedAxisX(
268+
context: context,
269+
state: state,
270+
key: const ValueKey("AccountDetailsRouteTransition"),
271+
child: AccountDetailsPage(
272+
name: name,
273+
),
274+
);
275+
}
276+
259277
@override
260278
Widget build(BuildContext context, GoRouterState state) {
261279
return AccountDetailsPage(name: name);
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import 'package:animations/animations.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_template/util/extensions/context_extension.dart';
4+
import 'package:go_router/go_router.dart';
5+
6+
abstract class PageTransitions {
7+
//#region Shared Axis
8+
static CustomTransitionPage sharedAxisY({
9+
LocalKey? key,
10+
required BuildContext context,
11+
required GoRouterState state,
12+
required Widget child,
13+
}) {
14+
return _sharedAxis(
15+
key: key,
16+
context: context,
17+
state: state,
18+
child: child,
19+
type: SharedAxisTransitionType.vertical,
20+
);
21+
}
22+
23+
static CustomTransitionPage sharedAxisX({
24+
LocalKey? key,
25+
required BuildContext context,
26+
required GoRouterState state,
27+
required Widget child,
28+
}) {
29+
return _sharedAxis(
30+
key: key,
31+
context: context,
32+
state: state,
33+
child: child,
34+
type: SharedAxisTransitionType.horizontal,
35+
);
36+
}
37+
38+
static CustomTransitionPage sharedAxisZ({
39+
LocalKey? key,
40+
required BuildContext context,
41+
required GoRouterState state,
42+
required Widget child,
43+
}) {
44+
return _sharedAxis(
45+
key: key,
46+
context: context,
47+
state: state,
48+
child: child,
49+
type: SharedAxisTransitionType.scaled,
50+
);
51+
}
52+
53+
static CustomTransitionPage _sharedAxis({
54+
LocalKey? key,
55+
required BuildContext context,
56+
required GoRouterState state,
57+
required Widget child,
58+
required SharedAxisTransitionType type,
59+
}) {
60+
return CustomTransitionPage(
61+
key: key,
62+
child: child,
63+
transitionsBuilder: (context, animation, secondaryAnimation, child) {
64+
return SharedAxisTransition(
65+
animation: animation,
66+
secondaryAnimation: secondaryAnimation,
67+
transitionType: type,
68+
fillColor: context.colorScheme.background,
69+
child: child,
70+
);
71+
},
72+
);
73+
}
74+
//#endregion
75+
76+
//#region Fade Through
77+
static CustomTransitionPage fadeThrough({
78+
LocalKey? key,
79+
required BuildContext context,
80+
required GoRouterState state,
81+
required Widget child,
82+
}) {
83+
return CustomTransitionPage(
84+
key: key,
85+
child: child,
86+
transitionsBuilder: (context, animation, secondaryAnimation, child) {
87+
return FadeThroughTransition(
88+
animation: animation,
89+
secondaryAnimation: secondaryAnimation,
90+
fillColor: context.colorScheme.background,
91+
child: child,
92+
);
93+
},
94+
);
95+
}
96+
//#endregion
97+
}

lib/presentation/bottom_navigation/bottom_navigation_page.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_template/app/navigation/router/app_routes.dart';
3+
import 'package:flutter_template/presentation/shared/design_system/utils/animated_branch_container.dart';
34
import 'package:go_router/go_router.dart';
45

6+
// Example: https://github.com/flutter/packages/blob/main/packages/go_router_builder/example/lib/stateful_shell_route_example.dart
57
class BottomNavigationPage extends StatelessWidget {
68
const BottomNavigationPage({
79
required this.navigationShell,
@@ -24,13 +26,16 @@ class BottomNavigationPage extends StatelessWidget {
2426
actions: [
2527
IconButton(
2628
onPressed: () {
27-
context.push(SettingsRoute().location);
29+
const SettingsRoute().push(context);
2830
},
2931
icon: const Icon(Icons.settings),
3032
),
3133
],
3234
),
33-
body: children[navigationShell.currentIndex],
35+
body: AnimatedBranchContainer(
36+
currentIndex: navigationShell.currentIndex,
37+
children: children,
38+
),
3439
bottomNavigationBar: BottomNavigationBar(
3540
items: const [
3641
BottomNavigationBarItem(

lib/presentation/settings/account_details_page.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_animate/flutter_animate.dart';
3+
import 'package:flutter_template/presentation/shared/design_system/theme/anims.dart';
24

35
class AccountDetailsPage extends StatelessWidget {
46
final String? name;
@@ -12,7 +14,10 @@ class AccountDetailsPage extends StatelessWidget {
1214
return Scaffold(
1315
appBar: AppBar(title: const Text("Account Details")),
1416
body: Center(
15-
child: Text("Name: $name"),
17+
child: Text("Name: $name").animate().rotate(
18+
duration: Anims.defaultDuration,
19+
curve: Anims.defaultCurve,
20+
),
1621
),
1722
);
1823
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import 'package:flutter/widgets.dart';
2+
3+
abstract class Anims {
4+
const Anims._();
5+
// Durations
6+
static const Duration defaultDuration = Duration(milliseconds: 300);
7+
static const Duration tapDuration = Duration(milliseconds: 200);
8+
9+
// Curves
10+
static const Curve defaultCurve = Curves.easeInOut;
11+
static const Curve tapCurve = Curves.fastEaseInToSlowEaseOut;
12+
}

lib/presentation/shared/design_system/theme/app_colors.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ abstract class AppColors {
99
static const white = Color(0xFFFFFFFF);
1010
static const error = Color(0xFFFF5F55);
1111
static const disabled = Color(0xFFB5B5B5);
12+
13+
// Opacity
14+
static const tapOpacity = 0.4;
1215
}

lib/presentation/shared/design_system/theme/app_theme.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
22
import 'package:flutter_template/presentation/shared/design_system/theme/dimens.dart';
33

44
abstract class AppTheme {
5+
const AppTheme._();
6+
57
static ButtonThemeData buttonTheme(
68
ButtonThemeData base, {
79
required Color borderColor,

lib/presentation/shared/design_system/theme/dimens.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:flutter/material.dart';
22

3-
class Dimens {
3+
abstract class Dimens {
4+
const Dimens._();
5+
46
// Zero
57
static const double zero = 0.0;
68

lib/presentation/shared/design_system/theme/theme_factory.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import 'package:flutter_template/presentation/shared/design_system/theme/app_the
66
import 'package:flutter_template/presentation/shared/design_system/theme/dimens.dart';
77

88
abstract class ThemeFactory {
9+
const ThemeFactory._();
10+
911
static SystemUiOverlayStyle lightSystemUIOverlayStyle =
1012
SystemUiOverlayStyle.dark.copyWith(
1113
statusBarColor: Colors.transparent,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:collection/collection.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_template/presentation/shared/design_system/theme/anims.dart';
4+
5+
/// Custom branch Navigator container that provides animated transitions
6+
/// when switching branches.
7+
class AnimatedBranchContainer extends StatelessWidget {
8+
/// Creates a AnimatedBranchContainer
9+
const AnimatedBranchContainer({
10+
super.key,
11+
required this.currentIndex,
12+
required this.children,
13+
});
14+
15+
/// The index (in [children]) of the branch Navigator to display.
16+
final int currentIndex;
17+
18+
/// The children (branch Navigators) to display in this container.
19+
final List<Widget> children;
20+
21+
@override
22+
Widget build(BuildContext context) {
23+
return Stack(
24+
children: children.mapIndexed(
25+
(int index, Widget navigator) {
26+
return AnimatedOpacity(
27+
curve: Anims.defaultCurve,
28+
duration: Anims.defaultDuration,
29+
opacity: index == currentIndex ? 1 : 0,
30+
child: _branchNavigatorWrapper(index, navigator),
31+
);
32+
},
33+
).toList());
34+
}
35+
36+
Widget _branchNavigatorWrapper(int index, Widget navigator) => IgnorePointer(
37+
ignoring: index != currentIndex,
38+
child: TickerMode(
39+
enabled: index == currentIndex,
40+
child: navigator,
41+
),
42+
);
43+
}

lib/presentation/shared/design_system/views/ds_opacity_gesture_detector.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import 'package:flutter/material.dart';
2+
import 'package:flutter_template/presentation/shared/design_system/theme/anims.dart';
3+
import 'package:flutter_template/presentation/shared/design_system/theme/app_colors.dart';
24
import 'package:flutter_template/presentation/shared/design_system/utils/conditional_parent_widget.dart';
35

46
class DSOpacityGestureDetector extends StatefulWidget {
@@ -24,9 +26,9 @@ class DSOpacityGestureDetector extends StatefulWidget {
2426

2527
class _DSOpacityGestureDetectorState extends State<DSOpacityGestureDetector> {
2628
static const double _maxOpacity = 1;
27-
static const double _minOpacity = 0.4;
28-
static const _animationCurve = Curves.easeInOutCubic;
29-
static const _animationDuration = Duration(milliseconds: 200);
29+
static const double _minOpacity = AppColors.tapOpacity;
30+
static const _animationCurve = Anims.tapCurve;
31+
static const _animationDuration = Anims.tapDuration;
3032

3133
double opacity = _maxOpacity;
3234
@override

pubspec.lock

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ packages:
2525
url: "https://pub.dev"
2626
source: hosted
2727
version: "5.13.0"
28+
animations:
29+
dependency: "direct main"
30+
description:
31+
name: animations
32+
sha256: ef57563eed3620bd5d75ad96189846aca1e033c0c45fc9a7d26e80ab02b88a70
33+
url: "https://pub.dev"
34+
source: hosted
35+
version: "2.0.8"
2836
another_flushbar:
2937
dependency: "direct main"
3038
description:
@@ -130,14 +138,6 @@ packages:
130138
url: "https://pub.dev"
131139
source: hosted
132140
version: "7.2.10"
133-
build_verify:
134-
dependency: "direct dev"
135-
description:
136-
name: build_verify
137-
sha256: abbb9b9eda076854ac1678d284c053a5ec608e64da741d0801f56d4bbea27e23
138-
url: "https://pub.dev"
139-
source: hosted
140-
version: "3.1.0"
141141
built_collection:
142142
dependency: transitive
143143
description:
@@ -551,6 +551,14 @@ packages:
551551
description: flutter
552552
source: sdk
553553
version: "0.0.0"
554+
flutter_animate:
555+
dependency: "direct main"
556+
description:
557+
name: flutter_animate
558+
sha256: "1dbc1aabfb8ec1e9d9feed2b675c21fb6b0a11f99be53ec3bc0f1901af6a8eb7"
559+
url: "https://pub.dev"
560+
source: hosted
561+
version: "4.3.0"
554562
flutter_bloc:
555563
dependency: "direct main"
556564
description:

0 commit comments

Comments
 (0)