diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e5579c..8363166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # 0.4.0 - feat: add 'NesConfirmDialog' + - feat: add 'NesFillTransition' + - feat: add 'NesHorizontalCloseTransition' + - feat: add 'NesVerticalCloseTransition' # 0.3.0 diff --git a/example/lib/gallery/gallery_page.dart b/example/lib/gallery/gallery_page.dart index b796165..2e9685d 100644 --- a/example/lib/gallery/gallery_page.dart +++ b/example/lib/gallery/gallery_page.dart @@ -35,6 +35,8 @@ class GalleryPage extends StatelessWidget { const SizedBox(height: 32), const TextSection(), const SizedBox(height: 32), + const ScreenTransitionsSection(), + const SizedBox(height: 32), const DialogsSection(), const SizedBox(height: 32), const IterableOptionsSection(), diff --git a/example/lib/gallery/sections/screen_transitions.dart b/example/lib/gallery/sections/screen_transitions.dart new file mode 100644 index 0000000..a12d751 --- /dev/null +++ b/example/lib/gallery/sections/screen_transitions.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:nes_ui/nes_ui.dart'; + +class ScreenTransitionsSection extends StatelessWidget { + const ScreenTransitionsSection({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Screen Transitions', + style: theme.textTheme.displayMedium, + ), + const SizedBox(height: 16), + Wrap( + children: [ + NesButton( + type: NesButtonType.normal, + child: const Text('Vertical Close'), + onPressed: () { + Navigator.of(context).push( + NesVerticalCloseTransition.route( + pageBuilder: (_, __, ___) { + return _MockPage(); + }, + ), + ); + }, + ), + const SizedBox(width: 16), + NesButton( + type: NesButtonType.normal, + child: const Text('Horizontal Close'), + onPressed: () { + Navigator.of(context).push( + NesHorizontalCloseTransition.route( + pageBuilder: (_, __, ___) { + return _MockPage(); + }, + ), + ); + }, + ), + const SizedBox(width: 16), + NesButton( + type: NesButtonType.normal, + child: const Text('Screen Fill'), + onPressed: () { + Navigator.of(context).push( + NesFillTransition.route( + pageBuilder: (_, __, ___) { + return _MockPage(); + }, + ), + ); + }, + ), + ], + ), + ], + ); + } +} + +class _MockPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/logo.png'), + const SizedBox(height: 8), + NesButton( + type: NesButtonType.normal, + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Back'), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/gallery/sections/sections.dart b/example/lib/gallery/sections/sections.dart index 0ba34b6..8aa7c2a 100644 --- a/example/lib/gallery/sections/sections.dart +++ b/example/lib/gallery/sections/sections.dart @@ -7,6 +7,7 @@ export 'icon_buttons.dart'; export 'icons.dart'; export 'iterable_options.dart'; export 'key_icons.dart'; +export 'screen_transitions.dart'; export 'selection_list.dart'; export 'text_fields.dart'; export 'texts.dart'; diff --git a/lib/src/nes_ui.dart b/lib/src/nes_ui.dart index e30f36e..20c3847 100644 --- a/lib/src/nes_ui.dart +++ b/lib/src/nes_ui.dart @@ -1,3 +1,4 @@ export 'controller.dart'; +export 'screen_transitions/screen_transitions.dart'; export 'theme.dart'; export 'widgets/widgets.dart'; diff --git a/lib/src/screen_transitions/nes_fill.dart b/lib/src/screen_transitions/nes_fill.dart new file mode 100644 index 0000000..3ca32cb --- /dev/null +++ b/lib/src/screen_transitions/nes_fill.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:nes_ui/nes_ui.dart'; + +/// {@nes_fill_transition} +/// A Transition that fills the screen. +/// {@endtemplate} +class NesFillTransition extends NesOverlayTransitionWidget { + /// {@macro nes_fill_transition} + const NesFillTransition({ + super.key, + required super.animation, + required super.child, + }); + + /// Creates a route with this animation. + static PageRouteBuilder route({ + required RoutePageBuilder pageBuilder, + }) { + return PageRouteBuilder( + pageBuilder: pageBuilder, + reverseTransitionDuration: const Duration(milliseconds: 750), + transitionDuration: const Duration(milliseconds: 750), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return NesFillTransition( + animation: animation, + child: child, + ); + }, + ); + } + + @override + Widget buildOverlay(BuildContext context, double value) { + final overlayTransitionTheme = + context.nesThemeExtension(); + + final size = MediaQuery.of(context).size; + + return Stack( + alignment: Alignment.center, + children: [ + Positioned( + width: size.width * value, + height: size.height * value, + child: ColoredBox(color: overlayTransitionTheme.color), + ), + ], + ); + } +} diff --git a/lib/src/screen_transitions/nes_horizontal_close.dart b/lib/src/screen_transitions/nes_horizontal_close.dart new file mode 100644 index 0000000..0e50937 --- /dev/null +++ b/lib/src/screen_transitions/nes_horizontal_close.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:nes_ui/nes_ui.dart'; + +/// {@nes_horizontal_close_transition} +/// A Transition that looks like horizontal doors are closing in front +/// of the current screen, opening again with the new one. +/// {@endtemplate} +class NesHorizontalCloseTransition extends NesOverlayTransitionWidget { + /// {@macro nes_horizontal_close_transition} + const NesHorizontalCloseTransition({ + super.key, + required super.animation, + required super.child, + }); + + /// Creates a route with this animation. + static PageRouteBuilder route({ + required RoutePageBuilder pageBuilder, + }) { + return PageRouteBuilder( + pageBuilder: pageBuilder, + reverseTransitionDuration: const Duration(milliseconds: 750), + transitionDuration: const Duration(milliseconds: 750), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return NesHorizontalCloseTransition( + animation: animation, + child: child, + ); + }, + ); + } + + @override + Widget buildOverlay(BuildContext context, double value) { + final overlayTransitionTheme = + context.nesThemeExtension(); + final width = MediaQuery.of(context).size.width; + final half = width / 2; + + return Stack( + children: [ + Positioned( + top: 0, + bottom: 0, + left: 0, + width: half * value, + child: ColoredBox(color: overlayTransitionTheme.color), + ), + Positioned( + top: 0, + bottom: 0, + right: 0, + left: width - (half * value), + child: ColoredBox(color: overlayTransitionTheme.color), + ) + ], + ); + } +} diff --git a/lib/src/screen_transitions/nes_overlay_transition_widget.dart b/lib/src/screen_transitions/nes_overlay_transition_widget.dart new file mode 100644 index 0000000..b075b8a --- /dev/null +++ b/lib/src/screen_transitions/nes_overlay_transition_widget.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +/// {@template nes_overlay_transition_widget} +/// Base widget for all of the Nes UI transition animation. +/// +/// It splits the progress of the animation into steps, in order +/// to make it easy to compose the animation. +/// +/// {@endtemplate} +abstract class NesOverlayTransitionWidget extends StatelessWidget { + /// {@template nes_overlay_transition_widget} + const NesOverlayTransitionWidget({ + required this.animation, + required this.child, + super.key, + }); + + /// Animation. + final Animation animation; + + /// Child. + final Widget child; + + @override + Widget build(BuildContext context) { + if (animation.value == 1) { + return child; + } + + return Stack( + children: [ + if (animation.value >= .5) Positioned.fill(child: child), + if (animation.value <= .5) + Positioned.fill( + child: buildOverlay(context, animation.value * 2), + ) + else + Positioned.fill( + child: buildOverlay(context, (1 - animation.value) * 2), + ), + ], + ); + } + + /// Returns the widget that will be rendered on top of the screen/child. + Widget buildOverlay(BuildContext context, double value); +} diff --git a/lib/src/screen_transitions/nes_transition_widget.dart b/lib/src/screen_transitions/nes_transition_widget.dart new file mode 100644 index 0000000..bc32e89 --- /dev/null +++ b/lib/src/screen_transitions/nes_transition_widget.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +/// {@template nes_transition_overlay_widget} +/// Base widget for all of the Nes UI transition animation. +/// +/// It splits the progress of the animation into steps, in order +/// to make it easy to compose the animation. +/// +/// {@endtemplate} +abstract class NesTransitionOverlayWidget extends StatelessWidget { + /// {@template nes_transition_overlay_widget} + const NesTransitionOverlayWidget({ + required this.animation, + required this.child, + super.key, + }); + + /// Animation. + final Animation animation; + + /// Child. + final Widget child; + + @override + Widget build(BuildContext context) { + if (animation.value == 1) { + return child; + } + + return Stack( + children: [ + if (animation.value >= .5) Positioned.fill(child: child), + if (animation.value <= .5) + Positioned.fill( + child: buildOverlay(context, animation.value * 2), + ) + else + Positioned.fill( + child: buildOverlay(context, (1 - animation.value) * 2), + ), + ], + ); + } + + /// Returns the widget that will be rendered on top of the screen/child. + Widget buildOverlay(BuildContext context, double value); +} diff --git a/lib/src/screen_transitions/nes_vertical_close.dart b/lib/src/screen_transitions/nes_vertical_close.dart new file mode 100644 index 0000000..1f8d8b2 --- /dev/null +++ b/lib/src/screen_transitions/nes_vertical_close.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:nes_ui/nes_ui.dart'; + +/// {@nes_vertical_close_transition} +/// A Transition that looks like vertical doors are closing in front +/// of the current screen, opening again with the new one. +/// {@endtemplate} +class NesVerticalCloseTransition extends NesOverlayTransitionWidget { + /// {@macro nes_vertical_close_transition} + const NesVerticalCloseTransition({ + super.key, + required super.animation, + required super.child, + }); + + /// Creates a route with this animation. + static PageRouteBuilder route({ + required RoutePageBuilder pageBuilder, + }) { + return PageRouteBuilder( + pageBuilder: pageBuilder, + reverseTransitionDuration: const Duration(milliseconds: 750), + transitionDuration: const Duration(milliseconds: 750), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return NesVerticalCloseTransition( + animation: animation, + child: child, + ); + }, + ); + } + + @override + Widget buildOverlay(BuildContext context, double value) { + final overlayTransitionTheme = + context.nesThemeExtension(); + final height = MediaQuery.of(context).size.height; + final half = height / 2; + + return Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + height: half * value, + child: ColoredBox(color: overlayTransitionTheme.color), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + top: height - (half * value), + child: ColoredBox(color: overlayTransitionTheme.color), + ) + ], + ); + } +} diff --git a/lib/src/screen_transitions/screen_transitions.dart b/lib/src/screen_transitions/screen_transitions.dart new file mode 100644 index 0000000..425fd35 --- /dev/null +++ b/lib/src/screen_transitions/screen_transitions.dart @@ -0,0 +1,4 @@ +export 'nes_fill.dart'; +export 'nes_horizontal_close.dart'; +export 'nes_overlay_transition_widget.dart'; +export 'nes_vertical_close.dart'; diff --git a/lib/src/theme.dart b/lib/src/theme.dart index d63f6d3..057f870 100644 --- a/lib/src/theme.dart +++ b/lib/src/theme.dart @@ -184,6 +184,44 @@ class NesIconTheme extends ThemeExtension { } } +/// {@template nes_overlay_transition_theme} +/// Class with information regarding overlay transitions. +/// {@endtemplate} +class NesOverlayTransitionTheme + extends ThemeExtension { + /// {@macro nes_overlay_transition_theme} + const NesOverlayTransitionTheme({ + required this.color, + }); + + /// Color of the overlay. + final Color color; + + @override + NesOverlayTransitionTheme copyWith({ + Color? color, + }) { + return NesOverlayTransitionTheme( + color: color ?? this.color, + ); + } + + @override + NesOverlayTransitionTheme lerp( + ThemeExtension? other, + double t, + ) { + final otherExt = other as NesOverlayTransitionTheme?; + return NesOverlayTransitionTheme( + color: ColorTween( + begin: color, + end: otherExt?.color, + ).lerp(t) ?? + color, + ); + } +} + /// {@template nes_selection_list_them} /// Class with information regarding Selection Lists. /// {@endtemplate} @@ -271,6 +309,7 @@ ThemeData flutterNesTheme({ markerSize: Size(24, 24), itemMinHeight: 32, ), + NesOverlayTransitionTheme? nesOverlayTransitionTheme, Iterable> customExtensions = const [], }) { final iconTheme = nesIconTheme ?? @@ -284,6 +323,11 @@ ThemeData flutterNesTheme({ secondary: Color(0xffe5e5e5), )); + final overlayTransitionTheme = nesOverlayTransitionTheme ?? + (brightness == Brightness.light + ? const NesOverlayTransitionTheme(color: Color(0xff0d0d0d)) + : const NesOverlayTransitionTheme(color: Color(0xff8c8c8c))); + final themeData = ThemeData( brightness: brightness, colorSchemeSeed: primaryColor, @@ -292,6 +336,7 @@ ThemeData flutterNesTheme({ nesButtonTheme, iconTheme, nesSelectionListTheme, + overlayTransitionTheme, ...customExtensions, ], ); diff --git a/test/src/screen_transitions/nes_fill_test.dart b/test/src/screen_transitions/nes_fill_test.dart new file mode 100644 index 0000000..4403834 --- /dev/null +++ b/test/src/screen_transitions/nes_fill_test.dart @@ -0,0 +1,63 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:nes_ui/nes_ui.dart'; + +class _MockAnimation extends Mock implements Animation {} + +void main() { + group('NesFillTransition', () { + late Animation animation; + + setUp(() { + animation = _MockAnimation(); + }); + + testWidgets('does not renders its child before half', (tester) async { + when(() => animation.value).thenReturn(.4); + await tester.pumpWidget( + MaterialApp( + theme: flutterNesTheme(), + home: NesFillTransition( + animation: animation, + child: Text('Child'), + ), + ), + ); + + expect(find.text('Child'), findsNothing); + }); + + testWidgets('renders its child after half', (tester) async { + when(() => animation.value).thenReturn(.6); + await tester.pumpWidget( + MaterialApp( + theme: flutterNesTheme(), + home: NesFillTransition( + animation: animation, + child: Text('Child'), + ), + ), + ); + + expect(find.text('Child'), findsOneWidget); + }); + + testWidgets('renders a colored box', (tester) async { + when(() => animation.value).thenReturn(.6); + await tester.pumpWidget( + MaterialApp( + theme: flutterNesTheme(), + home: NesFillTransition( + animation: animation, + child: Text('Child'), + ), + ), + ); + + expect(find.byType(ColoredBox), findsOneWidget); + }); + }); +} diff --git a/test/src/screen_transitions/nes_horizontal_close_test.dart b/test/src/screen_transitions/nes_horizontal_close_test.dart new file mode 100644 index 0000000..cd363ca --- /dev/null +++ b/test/src/screen_transitions/nes_horizontal_close_test.dart @@ -0,0 +1,63 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:nes_ui/nes_ui.dart'; + +class _MockAnimation extends Mock implements Animation {} + +void main() { + group('NesHorizontalCloseTransition', () { + late Animation animation; + + setUp(() { + animation = _MockAnimation(); + }); + + testWidgets('does not renders its child before half', (tester) async { + when(() => animation.value).thenReturn(.4); + await tester.pumpWidget( + MaterialApp( + theme: flutterNesTheme(), + home: NesHorizontalCloseTransition( + animation: animation, + child: Text('Child'), + ), + ), + ); + + expect(find.text('Child'), findsNothing); + }); + + testWidgets('renders its child after half', (tester) async { + when(() => animation.value).thenReturn(.6); + await tester.pumpWidget( + MaterialApp( + theme: flutterNesTheme(), + home: NesHorizontalCloseTransition( + animation: animation, + child: Text('Child'), + ), + ), + ); + + expect(find.text('Child'), findsOneWidget); + }); + + testWidgets('renders two colored box', (tester) async { + when(() => animation.value).thenReturn(.6); + await tester.pumpWidget( + MaterialApp( + theme: flutterNesTheme(), + home: NesHorizontalCloseTransition( + animation: animation, + child: Text('Child'), + ), + ), + ); + + expect(find.byType(ColoredBox), findsNWidgets(2)); + }); + }); +} diff --git a/test/src/screen_transitions/nes_vertical_close_test.dart b/test/src/screen_transitions/nes_vertical_close_test.dart new file mode 100644 index 0000000..4ca3e7e --- /dev/null +++ b/test/src/screen_transitions/nes_vertical_close_test.dart @@ -0,0 +1,63 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:nes_ui/nes_ui.dart'; + +class _MockAnimation extends Mock implements Animation {} + +void main() { + group('NesVerticalCloseTransition', () { + late Animation animation; + + setUp(() { + animation = _MockAnimation(); + }); + + testWidgets('does not renders its child before half', (tester) async { + when(() => animation.value).thenReturn(.4); + await tester.pumpWidget( + MaterialApp( + theme: flutterNesTheme(), + home: NesVerticalCloseTransition( + animation: animation, + child: Text('Child'), + ), + ), + ); + + expect(find.text('Child'), findsNothing); + }); + + testWidgets('renders its child after half', (tester) async { + when(() => animation.value).thenReturn(.6); + await tester.pumpWidget( + MaterialApp( + theme: flutterNesTheme(), + home: NesVerticalCloseTransition( + animation: animation, + child: Text('Child'), + ), + ), + ); + + expect(find.text('Child'), findsOneWidget); + }); + + testWidgets('renders two colored box', (tester) async { + when(() => animation.value).thenReturn(.6); + await tester.pumpWidget( + MaterialApp( + theme: flutterNesTheme(), + home: NesVerticalCloseTransition( + animation: animation, + child: Text('Child'), + ), + ), + ); + + expect(find.byType(ColoredBox), findsNWidgets(2)); + }); + }); +}