2-column layout

2-column layout

removed settings screen route animation
This commit is contained in:
Vadim 2022-12-11 17:09:10 +03:00
parent 10764086ad
commit 9abad012e3
6 changed files with 91 additions and 356 deletions

View file

@ -3,13 +3,11 @@ import 'package:flutter/services.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/data/permissions_service.dart'; import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/screens/settings/settings_page_route_builder.dart';
import 'package:lightmeter/screens/settings/settings_screen.dart'; import 'package:lightmeter/screens/settings/settings_screen.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.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';
@ -28,10 +26,7 @@ class Application extends StatefulWidget {
State<Application> createState() => _ApplicationState(); State<Application> createState() => _ApplicationState();
} }
class _ApplicationState extends State<Application> with TickerProviderStateMixin { class _ApplicationState extends State<Application> {
late final AnimationController _animationController;
late final _settingsRouteObserver = _SettingsRouteObserver(onPush: _onSettingsPush, onPop: _onSettingsPop);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -43,20 +38,6 @@ class _ApplicationState extends State<Application> with TickerProviderStateMixin
systemNavigationBarIconBrightness: Brightness.dark, systemNavigationBarIconBrightness: Brightness.dark,
); );
SystemChrome.setSystemUIOverlayStyle(mySystemTheme); SystemChrome.setSystemUIOverlayStyle(mySystemTheme);
// 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
@ -71,7 +52,6 @@ class _ApplicationState extends State<Application> with TickerProviderStateMixin
useMaterial3: true, useMaterial3: true,
colorScheme: lightColorScheme, colorScheme: lightColorScheme,
), ),
navigatorObservers: [_settingsRouteObserver],
localizationsDelegates: const [ localizationsDelegates: const [
S.delegate, S.delegate,
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
@ -79,9 +59,13 @@ class _ApplicationState extends State<Application> with TickerProviderStateMixin
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
supportedLocales: S.delegate.supportedLocales, supportedLocales: S.delegate.supportedLocales,
home: MeteringScreen(animationController: _animationController), builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child!,
),
home: const MeteringScreen(),
routes: { routes: {
"metering": (context) => MeteringScreen(animationController: _animationController), "metering": (context) => const MeteringScreen(),
"settings": (context) => const SettingsScreen(), "settings": (context) => const SettingsScreen(),
}, },
), ),
@ -89,44 +73,4 @@ class _ApplicationState extends State<Application> with TickerProviderStateMixin
), ),
); );
} }
void _onSettingsPush() {
if (!_animationController.isAnimating && _animationController.status != AnimationStatus.completed) {
_animationController.forward();
}
}
void _onSettingsPop() {
Future.delayed(Dimens.durationM).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

@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
class MeteringScreenAnimatedSurface extends _AnimatedSurface {
MeteringScreenAnimatedSurface.top({
required super.controller,
required super.areaHeight,
required super.overflowSize,
required super.child,
}) : super(
alignment: Alignment.topCenter,
borderRadiusBegin: const BorderRadius.only(
bottomLeft: Radius.circular(Dimens.borderRadiusL),
bottomRight: Radius.circular(Dimens.borderRadiusL),
),
);
MeteringScreenAnimatedSurface.bottom({
required super.controller,
required super.areaHeight,
required super.overflowSize,
required super.child,
}) : super(
alignment: Alignment.bottomCenter,
borderRadiusBegin: const BorderRadius.only(
topLeft: Radius.circular(Dimens.borderRadiusL),
topRight: Radius.circular(Dimens.borderRadiusL),
),
);
}
class _AnimatedSurface extends StatelessWidget {
final AnimationController controller;
final Alignment alignment;
final double areaHeight;
final double overflowSize;
final Widget child;
final Animation<BorderRadius?> _borderRadiusAnimation;
final Animation<double> _childOpacityAnimation;
final Animation<double> _overflowHeightAnimation;
_AnimatedSurface({
required this.controller,
required this.alignment,
required BorderRadius borderRadiusBegin,
required this.areaHeight,
required this.overflowSize,
required this.child,
}) : _borderRadiusAnimation = BorderRadiusTween(
begin: borderRadiusBegin,
end: BorderRadius.zero,
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.4, 1.0, curve: Curves.linear),
),
),
_childOpacityAnimation = Tween<double>(
begin: 1,
end: 0,
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 0.4, curve: Curves.linear),
),
),
_overflowHeightAnimation = Tween<double>(
begin: 0,
end: overflowSize,
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 1.0, curve: Curves.linear),
),
);
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
child: child,
builder: (context, child) => SizedBox(
height: areaHeight + _overflowHeightAnimation.value,
child: ClipRRect(
borderRadius: _borderRadiusAnimation.value,
child: ColoredBox(
color: Theme.of(context).colorScheme.surface,
child: Align(
alignment: alignment,
child: Opacity(
opacity: _childOpacityAnimation.value,
child: child,
),
),
),
),
),
);
}
}

View file

@ -51,7 +51,7 @@ class _ReadingValueBuilder extends StatelessWidget {
reading.label, reading.label,
style: textTheme.labelMedium?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer), style: textTheme.labelMedium?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer),
), ),
const SizedBox(height: Dimens.grid4), const SizedBox(height: Dimens.grid8),
Text( Text(
reading.value, reading.value,
style: textTheme.bodyLarge?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer), style: textTheme.bodyLarge?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer),

View file

@ -12,8 +12,6 @@ import 'components/reading_container.dart';
import 'models/reading_value.dart'; import 'models/reading_value.dart';
class MeteringTopBar extends StatelessWidget { class MeteringTopBar extends StatelessWidget {
static const _columnsCount = 3;
final ExposurePair? fastest; final ExposurePair? fastest;
final ExposurePair? slowest; final ExposurePair? slowest;
final double ev; final double ev;
@ -35,8 +33,6 @@ class MeteringTopBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final columnWidth =
(MediaQuery.of(context).size.width - Dimens.paddingM * 2 - Dimens.grid16 * (_columnsCount - 1)) / 3;
return ClipRRect( return ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(Dimens.borderRadiusL), bottomLeft: Radius.circular(Dimens.borderRadiusL),
@ -50,96 +46,88 @@ class MeteringTopBar extends StatelessWidget {
bottom: false, bottom: false,
child: MediaQuery( child: MediaQuery(
data: MediaQuery.of(context), data: MediaQuery.of(context),
child: Row( child: IntrinsicHeight(
mainAxisSize: MainAxisSize.min, child: Row(
children: [ crossAxisAlignment: CrossAxisAlignment.stretch,
Expanded( mainAxisSize: MainAxisSize.min,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.stretch, Expanded(
children: [ child: Column(
SizedBox( crossAxisAlignment: CrossAxisAlignment.stretch,
height: columnWidth / 3 * 4, children: [
child: ReadingContainer( Expanded(
values: [ child: ReadingContainer(
ReadingValue( values: [
label: S.of(context).fastestExposurePair, ReadingValue(
value: fastest != null label: S.of(context).fastestExposurePair,
? '${fastest!.aperture.toString()} - ${fastest!.shutterSpeed.toString()}' value: fastest != null
: 'N/A', ? '${fastest!.aperture.toString()} - ${fastest!.shutterSpeed.toString()}'
), : 'N/A',
ReadingValue( ),
label: S.of(context).slowestExposurePair, ReadingValue(
value: fastest != null label: S.of(context).slowestExposurePair,
? '${slowest!.aperture.toString()} - ${slowest!.shutterSpeed.toString()}' value: fastest != null
: 'N/A', ? '${slowest!.aperture.toString()} - ${slowest!.shutterSpeed.toString()}'
), : 'N/A',
], ),
],
),
), ),
), const _InnerPadding(),
const _InnerPadding(), Row(
Row( children: [
children: [ Expanded(
SizedBox( child: _AnimatedDialogPicker(
width: columnWidth, title: S.of(context).iso,
child: ReadingContainer.singleValue( subtitle: S.of(context).filmSpeed,
value: ReadingValue( selectedValue: iso,
label: 'EV', values: isoValues,
value: ev.toStringAsFixed(1), itemTitleBuilder: (_, value) => Text(value.value.toString()),
// using ascending order, because increase in film speed rises EV
evDifferenceBuilder: (selected, other) => selected.toStringDifference(other),
onChanged: onIsoChanged,
), ),
), ),
), const _InnerPadding(),
const _InnerPadding(), Expanded(
SizedBox( child: _AnimatedDialogPicker(
width: columnWidth, title: S.of(context).nd,
child: _AnimatedDialogPicker( subtitle: S.of(context).ndFilterFactor,
title: S.of(context).iso, selectedValue: nd,
subtitle: S.of(context).filmSpeed, values: ndValues,
selectedValue: iso, itemTitleBuilder: (_, value) => Text(
values: isoValues, value.value == 0 ? S.of(context).none : value.value.toString(),
itemTitleBuilder: (_, value) => Text(value.value.toString()), ),
// using ascending order, because increase in film speed rises EV // using descending order, because ND filter darkens image & lowers EV
evDifferenceBuilder: (selected, other) => selected.toStringDifference(other), evDifferenceBuilder: (selected, other) => other.toStringDifference(selected),
onChanged: onIsoChanged, onChanged: onNdChanged,
),
), ),
],
)
],
),
),
const _InnerPadding(),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AnimatedDialog(
openedSize: Size(
MediaQuery.of(context).size.width - Dimens.paddingM * 2,
(MediaQuery.of(context).size.width - Dimens.paddingM * 2) / 3 * 4,
), ),
], child: const AspectRatio(
) aspectRatio: 3 / 4,
], child: ColoredBox(color: Colors.black),
),
),
],
),
), ),
), ],
const _InnerPadding(), ),
SizedBox(
width: columnWidth,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AnimatedDialog(
openedSize: Size(
MediaQuery.of(context).size.width - Dimens.paddingM * 2,
(MediaQuery.of(context).size.width - Dimens.paddingM * 2) / 3 * 4,
),
child: const AspectRatio(
aspectRatio: 3 / 4,
child: ColoredBox(color: Colors.black),
),
),
const _InnerPadding(),
_AnimatedDialogPicker(
title: S.of(context).nd,
subtitle: S.of(context).ndFilterFactor,
selectedValue: nd,
values: ndValues,
itemTitleBuilder: (_, value) => Text(
value.value == 0 ? S.of(context).none : value.value.toString(),
),
// using descending order, because ND filter darkens image & lowers EV
evDifferenceBuilder: (selected, other) => other.toStringDifference(selected),
onChanged: onNdChanged,
),
],
),
),
],
), ),
), ),
), ),
@ -150,7 +138,7 @@ class MeteringTopBar extends StatelessWidget {
} }
class _InnerPadding extends SizedBox { class _InnerPadding extends SizedBox {
const _InnerPadding() : super(height: Dimens.grid16, width: Dimens.grid16); const _InnerPadding() : super(height: Dimens.grid8, width: Dimens.grid8);
} }
class _AnimatedDialogPicker<T extends PhotographyValue> extends StatelessWidget { class _AnimatedDialogPicker<T extends PhotographyValue> extends StatelessWidget {

View file

@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/models/photography_value.dart'; import 'package:lightmeter/models/photography_value.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/settings_page_route_builder.dart'; import 'package:lightmeter/screens/settings/settings_screen.dart';
import 'components/animated_surface/animated_surface.dart';
import 'components/bottom_controls/bottom_controls.dart'; import 'components/bottom_controls/bottom_controls.dart';
import 'components/exposure_pairs_list/exposure_pairs_list.dart'; import 'components/exposure_pairs_list/exposure_pairs_list.dart';
import 'components/topbar/topbar.dart'; import 'components/topbar/topbar.dart';
@ -13,37 +12,13 @@ import 'metering_event.dart';
import 'metering_state.dart'; import 'metering_state.dart';
class MeteringScreen extends StatefulWidget { class MeteringScreen extends StatefulWidget {
final AnimationController animationController; const MeteringScreen({super.key});
const MeteringScreen({required this.animationController, super.key});
@override @override
State<MeteringScreen> createState() => _MeteringScreenState(); State<MeteringScreen> createState() => _MeteringScreenState();
} }
class _MeteringScreenState extends State<MeteringScreen> { class _MeteringScreenState extends State<MeteringScreen> {
final _topBarKey = GlobalKey(debugLabel: 'TopBarKey');
final _middleAreaKey = GlobalKey(debugLabel: 'MiddleAreaKey');
final _bottomBarKey = GlobalKey(debugLabel: 'BottomBarKey');
bool _secondBuild = false;
late double _topBarHeight;
late double _middleAreaHeight;
late double _bottomBarHeight;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_topBarHeight = _getHeight(_topBarKey);
_middleAreaHeight = _getHeight(_middleAreaKey);
_bottomBarHeight = _getHeight(_bottomBarKey);
setState(() {
_secondBuild = true;
});
});
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@ -60,50 +35,20 @@ class _MeteringScreenState extends State<MeteringScreen> {
children: [ children: [
Column( Column(
children: [ children: [
if (_secondBuild) SizedBox(height: _topBarHeight) else _topBar(state), _topBar(state),
Expanded( Expanded(
key: _middleAreaKey,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(child: ExposurePairsList(state.exposurePairs)),
flex: 2,
child: ExposurePairsList(state.exposurePairs),
),
const SizedBox(width: Dimens.grid16),
const Spacer()
], ],
), ),
), ),
), ),
if (_secondBuild) SizedBox(height: _bottomBarHeight) else _bottomBar(), _bottomBar(),
], ],
), ),
if (_secondBuild)
Positioned(
left: 0,
top: 0,
right: 0,
child: MeteringScreenAnimatedSurface.top(
controller: widget.animationController,
areaHeight: _topBarHeight,
overflowSize: _middleAreaHeight / 2,
child: _topBar(state),
),
),
if (_secondBuild)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: MeteringScreenAnimatedSurface.bottom(
controller: widget.animationController,
areaHeight: _bottomBarHeight,
overflowSize: _middleAreaHeight / 2,
child: _bottomBar(),
),
),
], ],
); );
}, },
@ -113,7 +58,6 @@ class _MeteringScreenState extends State<MeteringScreen> {
Widget _topBar(MeteringState state) { Widget _topBar(MeteringState state) {
return MeteringTopBar( return MeteringTopBar(
key: _topBarKey,
fastest: state.fastest, fastest: state.fastest,
slowest: state.slowest, slowest: state.slowest,
ev: state.ev, ev: state.ev,
@ -126,14 +70,11 @@ class _MeteringScreenState extends State<MeteringScreen> {
Widget _bottomBar() { Widget _bottomBar() {
return MeteringBottomControls( return MeteringBottomControls(
key: _bottomBarKey,
onSourceChanged: () {}, onSourceChanged: () {},
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()), onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
onSettings: () { onSettings: () {
Navigator.push(context, SettingsPageRouteBuilder()); Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen()));
}, },
); );
} }
double _getHeight(GlobalKey key) => key.currentContext!.size!.height;
} }

View file

@ -1,37 +0,0 @@
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.durationM, // wait for `MeteringScreenAnimatedSurface`s to expand
reverseTransitionDuration: Dimens.durationM,
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.durationM).inMilliseconds,
1.0,
curve: Curves.linear,
);
}
final animatable = tween.chain(CurveTween(curve: interval));
return Opacity(
opacity: (didPop ? secondaryAnimation : animation).drive(animatable).value,
child: child,
);
},
);
}