connected animation with navigation

This commit is contained in:
Vadim 2022-11-26 22:29:00 +03:00
parent 8ef53c344e
commit ce04dcf257
4 changed files with 122 additions and 31 deletions

View file

@ -1,20 +1,51 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:lightmeter/screens/settings/settings_page_route_builder.dart';
import 'generated/l10n.dart'; import 'generated/l10n.dart';
import 'models/photography_value.dart'; import 'models/photography_value.dart';
import 'res/dimens.dart';
import 'res/theme.dart'; import 'res/theme.dart';
import 'screens/metering/metering_bloc.dart'; import 'screens/metering/metering_bloc.dart';
import 'screens/metering/metering_screen.dart'; import 'screens/metering/metering_screen.dart';
import 'utils/stop_type_provider.dart'; import 'utils/stop_type_provider.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const Application());
} }
class MyApp extends StatelessWidget { final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
const MyApp({super.key});
class Application extends StatefulWidget {
const Application({super.key});
@override
State<Application> createState() => _ApplicationState();
}
class _ApplicationState extends State<Application> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -22,11 +53,11 @@ class MyApp extends StatelessWidget {
child: BlocProvider( child: BlocProvider(
create: (context) => MeteringBloc(context.read<StopType>()), create: (context) => MeteringBloc(context.read<StopType>()),
child: MaterialApp( child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
colorScheme: lightColorScheme, colorScheme: lightColorScheme,
), ),
navigatorObservers: [_settingsRouteObserver],
localizationsDelegates: const [ localizationsDelegates: const [
S.delegate, S.delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
@ -34,9 +65,49 @@ class MyApp extends StatelessWidget {
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
supportedLocales: S.delegate.supportedLocales, 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<SettingsPageRouteBuilder> {
final VoidCallback onPush;
final VoidCallback onPop;
_SettingsRouteObserver({
required this.onPush,
required this.onPop,
});
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);
if (route is SettingsPageRouteBuilder) {
onPush();
}
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPop(route, previousRoute);
if (previousRoute is PageRoute && route is SettingsPageRouteBuilder) {
onPop();
}
}
} }

View file

@ -12,6 +12,7 @@ class Dimens {
static const double paddingM = 16; static const double paddingM = 16;
static const Duration durationS = Duration(milliseconds: 100); static const Duration durationS = Duration(milliseconds: 100);
static const Duration durationSM = Duration(milliseconds: 150);
static const Duration durationM = Duration(milliseconds: 200); static const Duration durationM = Duration(milliseconds: 200);
static const Duration durationL = Duration(milliseconds: 300); static const Duration durationL = Duration(milliseconds: 300);
} }

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/res/dimens.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/animated_surface/animated_surface.dart';
import 'components/bottom_controls/bottom_controls.dart'; import 'components/bottom_controls/bottom_controls.dart';
@ -11,19 +12,19 @@ import 'metering_event.dart';
import 'metering_state.dart'; import 'metering_state.dart';
class MeteringScreen extends StatefulWidget { class MeteringScreen extends StatefulWidget {
const MeteringScreen({super.key}); final AnimationController animationController;
const MeteringScreen({required this.animationController, super.key});
@override @override
State<MeteringScreen> createState() => _MeteringScreenState(); State<MeteringScreen> createState() => _MeteringScreenState();
} }
class _MeteringScreenState extends State<MeteringScreen> with TickerProviderStateMixin { class _MeteringScreenState extends State<MeteringScreen> {
final _topBarKey = GlobalKey(debugLabel: 'TopBarKey'); final _topBarKey = GlobalKey(debugLabel: 'TopBarKey');
final _middleAreaKey = GlobalKey(debugLabel: 'MiddleAreaKey'); final _middleAreaKey = GlobalKey(debugLabel: 'MiddleAreaKey');
final _bottomBarKey = GlobalKey(debugLabel: 'BottomBarKey'); final _bottomBarKey = GlobalKey(debugLabel: 'BottomBarKey');
late final AnimationController _animationController;
bool _secondBuild = false; bool _secondBuild = false;
late double _topBarHeight; late double _topBarHeight;
late double _middleAreaHeight; late double _middleAreaHeight;
@ -32,13 +33,6 @@ class _MeteringScreenState extends State<MeteringScreen> with TickerProviderStat
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// 0 - collapsed
// 1 - expanded
_animationController = AnimationController(
value: 0,
duration: Dimens.durationL,
vsync: this,
);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_topBarHeight = _getHeight(_topBarKey); _topBarHeight = _getHeight(_topBarKey);
_middleAreaHeight = _getHeight(_middleAreaKey); _middleAreaHeight = _getHeight(_middleAreaKey);
@ -85,7 +79,7 @@ class _MeteringScreenState extends State<MeteringScreen> with TickerProviderStat
top: 0, top: 0,
right: 0, right: 0,
child: MeteringScreenAnimatedSurface.top( child: MeteringScreenAnimatedSurface.top(
controller: _animationController, controller: widget.animationController,
areaHeight: _topBarHeight, areaHeight: _topBarHeight,
overflowSize: _middleAreaHeight / 2, overflowSize: _middleAreaHeight / 2,
child: _topBar(state), child: _topBar(state),
@ -97,7 +91,7 @@ class _MeteringScreenState extends State<MeteringScreen> with TickerProviderStat
right: 0, right: 0,
bottom: 0, bottom: 0,
child: MeteringScreenAnimatedSurface.bottom( child: MeteringScreenAnimatedSurface.bottom(
controller: _animationController, controller: widget.animationController,
areaHeight: _bottomBarHeight, areaHeight: _bottomBarHeight,
overflowSize: _middleAreaHeight / 2, overflowSize: _middleAreaHeight / 2,
child: _bottomBar(), child: _bottomBar(),
@ -127,22 +121,10 @@ class _MeteringScreenState extends State<MeteringScreen> with TickerProviderStat
onSourceChanged: () {}, onSourceChanged: () {},
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()), onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
onSettings: () { onSettings: () {
// Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen())); Navigator.push(context, SettingsPageRouteBuilder());
_toggleAnimation();
}, },
); );
} }
double _getHeight(GlobalKey key) => key.currentContext!.size!.height; 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();
}
}
} }

View file

@ -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<void> {
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,
);
},
);
}