mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-25 08:50:40 +00:00
2-column layout
2-column layout removed settings screen route animation
This commit is contained in:
parent
10764086ad
commit
9abad012e3
6 changed files with 91 additions and 356 deletions
|
@ -3,13 +3,11 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.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:provider/provider.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';
|
||||
|
@ -28,10 +26,7 @@ class Application extends StatefulWidget {
|
|||
State<Application> createState() => _ApplicationState();
|
||||
}
|
||||
|
||||
class _ApplicationState extends State<Application> with TickerProviderStateMixin {
|
||||
late final AnimationController _animationController;
|
||||
late final _settingsRouteObserver = _SettingsRouteObserver(onPush: _onSettingsPush, onPop: _onSettingsPop);
|
||||
|
||||
class _ApplicationState extends State<Application> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -43,20 +38,6 @@ class _ApplicationState extends State<Application> with TickerProviderStateMixin
|
|||
systemNavigationBarIconBrightness: Brightness.dark,
|
||||
);
|
||||
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
|
||||
|
@ -71,7 +52,6 @@ class _ApplicationState extends State<Application> with TickerProviderStateMixin
|
|||
useMaterial3: true,
|
||||
colorScheme: lightColorScheme,
|
||||
),
|
||||
navigatorObservers: [_settingsRouteObserver],
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
|
@ -79,9 +59,13 @@ class _ApplicationState extends State<Application> with TickerProviderStateMixin
|
|||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
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: {
|
||||
"metering": (context) => MeteringScreen(animationController: _animationController),
|
||||
"metering": (context) => const MeteringScreen(),
|
||||
"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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -51,7 +51,7 @@ class _ReadingValueBuilder extends StatelessWidget {
|
|||
reading.label,
|
||||
style: textTheme.labelMedium?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer),
|
||||
),
|
||||
const SizedBox(height: Dimens.grid4),
|
||||
const SizedBox(height: Dimens.grid8),
|
||||
Text(
|
||||
reading.value,
|
||||
style: textTheme.bodyLarge?.copyWith(color: Theme.of(context).colorScheme.onPrimaryContainer),
|
||||
|
|
|
@ -12,8 +12,6 @@ import 'components/reading_container.dart';
|
|||
import 'models/reading_value.dart';
|
||||
|
||||
class MeteringTopBar extends StatelessWidget {
|
||||
static const _columnsCount = 3;
|
||||
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
final double ev;
|
||||
|
@ -35,8 +33,6 @@ class MeteringTopBar extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final columnWidth =
|
||||
(MediaQuery.of(context).size.width - Dimens.paddingM * 2 - Dimens.grid16 * (_columnsCount - 1)) / 3;
|
||||
return ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(Dimens.borderRadiusL),
|
||||
|
@ -50,15 +46,16 @@ class MeteringTopBar extends StatelessWidget {
|
|||
bottom: false,
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: columnWidth / 3 * 4,
|
||||
Expanded(
|
||||
child: ReadingContainer(
|
||||
values: [
|
||||
ReadingValue(
|
||||
|
@ -79,18 +76,7 @@ class MeteringTopBar extends StatelessWidget {
|
|||
const _InnerPadding(),
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: columnWidth,
|
||||
child: ReadingContainer.singleValue(
|
||||
value: ReadingValue(
|
||||
label: 'EV',
|
||||
value: ev.toStringAsFixed(1),
|
||||
),
|
||||
),
|
||||
),
|
||||
const _InnerPadding(),
|
||||
SizedBox(
|
||||
width: columnWidth,
|
||||
Expanded(
|
||||
child: _AnimatedDialogPicker(
|
||||
title: S.of(context).iso,
|
||||
subtitle: S.of(context).filmSpeed,
|
||||
|
@ -102,14 +88,28 @@ class MeteringTopBar extends StatelessWidget {
|
|||
onChanged: onIsoChanged,
|
||||
),
|
||||
),
|
||||
const _InnerPadding(),
|
||||
Expanded(
|
||||
child: _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,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const _InnerPadding(),
|
||||
SizedBox(
|
||||
width: columnWidth,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
|
@ -123,19 +123,6 @@ class MeteringTopBar extends StatelessWidget {
|
|||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -145,12 +132,13 @@ class MeteringTopBar extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/models/photography_value.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/exposure_pairs_list/exposure_pairs_list.dart';
|
||||
import 'components/topbar/topbar.dart';
|
||||
|
@ -13,37 +12,13 @@ import 'metering_event.dart';
|
|||
import 'metering_state.dart';
|
||||
|
||||
class MeteringScreen extends StatefulWidget {
|
||||
final AnimationController animationController;
|
||||
|
||||
const MeteringScreen({required this.animationController, super.key});
|
||||
const MeteringScreen({super.key});
|
||||
|
||||
@override
|
||||
State<MeteringScreen> createState() => _MeteringScreenState();
|
||||
}
|
||||
|
||||
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
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
@ -60,50 +35,20 @@ class _MeteringScreenState extends State<MeteringScreen> {
|
|||
children: [
|
||||
Column(
|
||||
children: [
|
||||
if (_secondBuild) SizedBox(height: _topBarHeight) else _topBar(state),
|
||||
_topBar(state),
|
||||
Expanded(
|
||||
key: _middleAreaKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ExposurePairsList(state.exposurePairs),
|
||||
),
|
||||
const SizedBox(width: Dimens.grid16),
|
||||
const Spacer()
|
||||
Expanded(child: ExposurePairsList(state.exposurePairs)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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) {
|
||||
return MeteringTopBar(
|
||||
key: _topBarKey,
|
||||
fastest: state.fastest,
|
||||
slowest: state.slowest,
|
||||
ev: state.ev,
|
||||
|
@ -126,14 +70,11 @@ class _MeteringScreenState extends State<MeteringScreen> {
|
|||
|
||||
Widget _bottomBar() {
|
||||
return MeteringBottomControls(
|
||||
key: _bottomBarKey,
|
||||
onSourceChanged: () {},
|
||||
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
|
||||
onSettings: () {
|
||||
Navigator.push(context, SettingsPageRouteBuilder());
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen()));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double _getHeight(GlobalKey key) => key.currentContext!.size!.height;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue