From ce04dcf25714de1e328f1c6269de833b6e549b52 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sat, 26 Nov 2022 22:29:00 +0300 Subject: [PATCH] connected animation with navigation --- lib/main.dart | 81 +++++++++++++++++-- lib/res/dimens.dart | 1 + lib/screens/metering/metering_screen.dart | 34 ++------ .../settings/settings_page_route_builder.dart | 37 +++++++++ 4 files changed, 122 insertions(+), 31 deletions(-) create mode 100644 lib/screens/settings/settings_page_route_builder.dart diff --git a/lib/main.dart b/lib/main.dart index ff0f870..7f1f928 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,20 +1,51 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:lightmeter/screens/settings/settings_page_route_builder.dart'; import 'generated/l10n.dart'; import 'models/photography_value.dart'; +import 'res/dimens.dart'; import 'res/theme.dart'; import 'screens/metering/metering_bloc.dart'; import 'screens/metering/metering_screen.dart'; import 'utils/stop_type_provider.dart'; void main() { - runApp(const MyApp()); + runApp(const Application()); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +final RouteObserver routeObserver = RouteObserver(); + +class Application extends StatefulWidget { + const Application({super.key}); + + @override + State createState() => _ApplicationState(); +} + +class _ApplicationState extends State with TickerProviderStateMixin { + late final AnimationController _animationController; + late final _settingsRouteObserver = _SettingsRouteObserver(onPush: _onSettingsPush, onPop: _onSettingsPop); + + @override + void initState() { + super.initState(); + // 0 - collapsed + // 1 - expanded + _animationController = AnimationController( + value: 0, + duration: Dimens.durationM, + reverseDuration: Dimens.durationSM, + vsync: this, + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -22,11 +53,11 @@ class MyApp extends StatelessWidget { child: BlocProvider( create: (context) => MeteringBloc(context.read()), child: MaterialApp( - title: 'Flutter Demo', theme: ThemeData( useMaterial3: true, colorScheme: lightColorScheme, ), + navigatorObservers: [_settingsRouteObserver], localizationsDelegates: const [ S.delegate, GlobalMaterialLocalizations.delegate, @@ -34,9 +65,49 @@ class MyApp extends StatelessWidget { GlobalCupertinoLocalizations.delegate, ], supportedLocales: S.delegate.supportedLocales, - home: const MeteringScreen(), + home: MeteringScreen(animationController: _animationController), ), ), ); } + + void _onSettingsPush() { + if (!_animationController.isAnimating && _animationController.status != AnimationStatus.completed) { + _animationController.forward(); + } + } + + void _onSettingsPop() { + Future.delayed(Dimens.durationSM).then((_) { + if (!_animationController.isAnimating && _animationController.status != AnimationStatus.dismissed) { + _animationController.reverse(); + } + }); + } +} + +class _SettingsRouteObserver extends RouteObserver { + final VoidCallback onPush; + final VoidCallback onPop; + + _SettingsRouteObserver({ + required this.onPush, + required this.onPop, + }); + + @override + void didPush(Route route, Route? previousRoute) { + super.didPush(route, previousRoute); + if (route is SettingsPageRouteBuilder) { + onPush(); + } + } + + @override + void didPop(Route route, Route? previousRoute) { + super.didPop(route, previousRoute); + if (previousRoute is PageRoute && route is SettingsPageRouteBuilder) { + onPop(); + } + } } diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index a9ce096..95f4396 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -12,6 +12,7 @@ class Dimens { static const double paddingM = 16; static const Duration durationS = Duration(milliseconds: 100); + static const Duration durationSM = Duration(milliseconds: 150); static const Duration durationM = Duration(milliseconds: 200); static const Duration durationL = Duration(milliseconds: 300); } diff --git a/lib/screens/metering/metering_screen.dart b/lib/screens/metering/metering_screen.dart index efe898c..e51d647 100644 --- a/lib/screens/metering/metering_screen.dart +++ b/lib/screens/metering/metering_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/settings/settings_page_route_builder.dart'; import 'components/animated_surface/animated_surface.dart'; import 'components/bottom_controls/bottom_controls.dart'; @@ -11,19 +12,19 @@ import 'metering_event.dart'; import 'metering_state.dart'; class MeteringScreen extends StatefulWidget { - const MeteringScreen({super.key}); + final AnimationController animationController; + + const MeteringScreen({required this.animationController, super.key}); @override State createState() => _MeteringScreenState(); } -class _MeteringScreenState extends State with TickerProviderStateMixin { +class _MeteringScreenState extends State { final _topBarKey = GlobalKey(debugLabel: 'TopBarKey'); final _middleAreaKey = GlobalKey(debugLabel: 'MiddleAreaKey'); final _bottomBarKey = GlobalKey(debugLabel: 'BottomBarKey'); - late final AnimationController _animationController; - bool _secondBuild = false; late double _topBarHeight; late double _middleAreaHeight; @@ -32,13 +33,6 @@ class _MeteringScreenState extends State with TickerProviderStat @override void initState() { super.initState(); - // 0 - collapsed - // 1 - expanded - _animationController = AnimationController( - value: 0, - duration: Dimens.durationL, - vsync: this, - ); WidgetsBinding.instance.addPostFrameCallback((_) { _topBarHeight = _getHeight(_topBarKey); _middleAreaHeight = _getHeight(_middleAreaKey); @@ -85,7 +79,7 @@ class _MeteringScreenState extends State with TickerProviderStat top: 0, right: 0, child: MeteringScreenAnimatedSurface.top( - controller: _animationController, + controller: widget.animationController, areaHeight: _topBarHeight, overflowSize: _middleAreaHeight / 2, child: _topBar(state), @@ -97,7 +91,7 @@ class _MeteringScreenState extends State with TickerProviderStat right: 0, bottom: 0, child: MeteringScreenAnimatedSurface.bottom( - controller: _animationController, + controller: widget.animationController, areaHeight: _bottomBarHeight, overflowSize: _middleAreaHeight / 2, child: _bottomBar(), @@ -127,22 +121,10 @@ class _MeteringScreenState extends State with TickerProviderStat onSourceChanged: () {}, onMeasure: () => context.read().add(const MeasureEvent()), onSettings: () { - // Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())); - _toggleAnimation(); + Navigator.push(context, SettingsPageRouteBuilder()); }, ); } double _getHeight(GlobalKey key) => key.currentContext!.size!.height; - - void _toggleAnimation() { - if (_animationController.isAnimating) { - return; - } - if (_animationController.status != AnimationStatus.dismissed) { - _animationController.reverse(); - } else if (_animationController.status != AnimationStatus.completed) { - _animationController.forward(); - } - } } diff --git a/lib/screens/settings/settings_page_route_builder.dart b/lib/screens/settings/settings_page_route_builder.dart new file mode 100644 index 0000000..8a22316 --- /dev/null +++ b/lib/screens/settings/settings_page_route_builder.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/settings/settings_screen.dart'; + +class SettingsPageRouteBuilder extends PageRouteBuilder { + SettingsPageRouteBuilder() + : super( + transitionDuration: + Dimens.durationM + Dimens.durationS, // wait for `MeteringScreenAnimatedSurface`s to expand + reverseTransitionDuration: Dimens.durationS, + pageBuilder: (context, animation, secondaryAnimation) => const SettingsScreen(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + final didPop = !(animation.value != 0.0 && secondaryAnimation.value == 0.0); + final tween = Tween(begin: 0.0, end: 1.0); + late Interval interval; + if (didPop) { + interval = const Interval( + 0, + 1.0, + curve: Curves.linear, + ); + } else { + interval = Interval( + Dimens.durationM.inMilliseconds / (Dimens.durationM + Dimens.durationS).inMilliseconds, + 1.0, + curve: Curves.linear, + ); + } + + final animatable = tween.chain(CurveTween(curve: interval)); + return Opacity( + opacity: (didPop ? secondaryAnimation : animation).drive(animatable).value, + child: child, + ); + }, + ); +}