From 8381e5b7534fbb7a0977aa6116f60fde5b9aca51 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sun, 4 Dec 2022 23:10:04 +0300 Subject: [PATCH] Enhanced picker dialogs added picker dialogs subtitle show EV difference (wip) jump to selected value added EV difference to pickers hide inkwell overflow thirds iso normal style fixed picker ev difference order more picker typdefs --- lib/generated/intl/messages_en.dart | 3 + lib/generated/l10n.dart | 20 +++ lib/l10n/intl_en.arb | 2 + lib/models/photography_value.dart | 21 ++- lib/res/dimens.dart | 1 + .../topbar/components/dialog_picker.dart | 121 +++++++++++++----- .../metering/components/topbar/topbar.dart | 94 ++++++++------ lib/screens/metering/metering_bloc.dart | 2 +- 8 files changed, 186 insertions(+), 78 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 1c1cacd..a06fefc 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -25,11 +25,14 @@ class MessageLookup extends MessageLookupByLibrary { "caffeine": MessageLookupByLibrary.simpleMessage("Caffeine"), "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "fastestExposurePair": MessageLookupByLibrary.simpleMessage("Fastest"), + "filmSpeed": MessageLookupByLibrary.simpleMessage("Film speed"), "haptics": MessageLookupByLibrary.simpleMessage("Haptics"), "iso": MessageLookupByLibrary.simpleMessage("ISO"), "keepsScreenOn": MessageLookupByLibrary.simpleMessage("Keeps screen on"), "nd": MessageLookupByLibrary.simpleMessage("ND"), + "ndFilterFactor": MessageLookupByLibrary.simpleMessage( + "Neutral density filter factor"), "none": MessageLookupByLibrary.simpleMessage("None"), "openSettings": MessageLookupByLibrary.simpleMessage("Open settings"), "permissionNeeded": diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index c7d7453..84b188e 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -110,6 +110,16 @@ class S { ); } + /// `Film speed` + String get filmSpeed { + return Intl.message( + 'Film speed', + name: 'filmSpeed', + desc: '', + args: [], + ); + } + /// `ND` String get nd { return Intl.message( @@ -120,6 +130,16 @@ class S { ); } + /// `Neutral density filter factor` + String get ndFilterFactor { + return Intl.message( + 'Neutral density filter factor', + name: 'ndFilterFactor', + desc: '', + args: [], + ); + } + /// `None` String get none { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index adbf419..dcacbe8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -6,7 +6,9 @@ "fastestExposurePair": "Fastest", "slowestExposurePair": "Slowest", "iso": "ISO", + "filmSpeed": "Film speed", "nd": "ND", + "ndFilterFactor": "Neutral density filter factor", "none": "None", "cancel": "Cancel", "select": "Select", diff --git a/lib/models/photography_value.dart b/lib/models/photography_value.dart index 59c8a07..45da5f9 100644 --- a/lib/models/photography_value.dart +++ b/lib/models/photography_value.dart @@ -1,14 +1,31 @@ +import 'dart:math'; + +import 'package:lightmeter/utils/log_2.dart'; + enum StopType { full, half, third } -abstract class PhotographyValue { +abstract class PhotographyValue { final T rawValue; const PhotographyValue(this.rawValue); T get value => rawValue; + + /// EV difference between `this` and `other` + double evDifference(PhotographyValue other) => log2(max(1, other.value) / max(1, value)); + + String toStringDifference(PhotographyValue other) { + final ev = log2(max(1, other.value) / max(1, value)); + final buffer = StringBuffer(); + if (ev > 0) { + buffer.write('+'); + } + buffer.write(ev.toStringAsFixed(1)); + return buffer.toString(); + } } -abstract class PhotographyStopValue extends PhotographyValue { +abstract class PhotographyStopValue extends PhotographyValue { final StopType stopType; const PhotographyStopValue(super.rawValue, this.stopType); diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index 3607a86..9d0ceed 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -9,6 +9,7 @@ class Dimens { static const double grid8 = 8; static const double grid16 = 16; static const double grid24 = 24; + static const double grid56 = 56; static const double grid168 = 168; static const double paddingM = 16; diff --git a/lib/screens/metering/components/topbar/components/dialog_picker.dart b/lib/screens/metering/components/topbar/components/dialog_picker.dart index 3401966..7b41e0a 100644 --- a/lib/screens/metering/components/topbar/components/dialog_picker.dart +++ b/lib/screens/metering/components/topbar/components/dialog_picker.dart @@ -1,20 +1,28 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/models/photography_value.dart'; import 'package:lightmeter/res/dimens.dart'; -class MeteringScreenDialogPicker extends StatefulWidget { +typedef DialogPickerItemBuilder = Widget Function(BuildContext, T); +typedef DialogPickerEvDifferenceBuilder = String Function(T selected, T other); + +class MeteringScreenDialogPicker extends StatefulWidget { final String title; + final String subtitle; final T initialValue; final List values; - final Widget Function(BuildContext context, T value) itemTitleBuilder; + final DialogPickerItemBuilder itemTitleBuilder; + final DialogPickerEvDifferenceBuilder evDifferenceBuilder; final VoidCallback onCancel; final ValueChanged onSelect; const MeteringScreenDialogPicker({ required this.title, + required this.subtitle, required this.initialValue, required this.values, required this.itemTitleBuilder, + required this.evDifferenceBuilder, required this.onCancel, required this.onSelect, super.key, @@ -24,34 +32,67 @@ class MeteringScreenDialogPicker extends StatefulWidget { State> createState() => _MeteringScreenDialogPickerState(); } -class _MeteringScreenDialogPickerState extends State> { +class _MeteringScreenDialogPickerState extends State> { late T _selectedValue = widget.initialValue; + final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollController.jumpTo(Dimens.grid56 * widget.values.indexOf(_selectedValue)); + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.fromLTRB( - Dimens.paddingL, - Dimens.paddingL, - Dimens.paddingL, - Dimens.paddingM, + ColoredBox( + color: Theme.of(context).colorScheme.primaryContainer, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + Dimens.paddingL, + Dimens.paddingL, + Dimens.paddingL, + Dimens.paddingM, + ), + child: Column( + children: [ + Text( + widget.title, + style: Theme.of(context).textTheme.headlineSmall!, + ), + const SizedBox(height: Dimens.grid16), + Text( + widget.subtitle, + style: Theme.of(context).textTheme.bodyMedium!, + ), + ], + ), + ), + Divider( + color: Theme.of(context).colorScheme.onPrimaryContainer, + height: 0, + ), + ], ), - child: Text( - widget.title, - style: Theme.of(context).textTheme.headlineSmall!, - ), - ), - Divider( - color: Theme.of(context).colorScheme.onPrimaryContainer, - height: 0, ), Expanded( child: ListView.builder( + controller: _scrollController, padding: EdgeInsets.zero, itemCount: widget.values.length, + itemExtent: Dimens.grid56, itemBuilder: (context, index) => RadioListTile( value: widget.values[index], groupValue: _selectedValue, @@ -59,6 +100,9 @@ class _MeteringScreenDialogPickerState extends State extends State widget.onSelect(_selectedValue), - child: Text(S.of(context).select), + Padding( + padding: const EdgeInsets.all(Dimens.paddingL), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + children: [ + const Spacer(), + TextButton( + onPressed: widget.onCancel, + child: Text(S.of(context).cancel), + ), + const SizedBox(width: Dimens.grid16), + TextButton( + onPressed: () => widget.onSelect(_selectedValue), + child: Text(S.of(context).select), + ), + ], + ), ), ], ), diff --git a/lib/screens/metering/components/topbar/topbar.dart b/lib/screens/metering/components/topbar/topbar.dart index 3457c76..c342412 100644 --- a/lib/screens/metering/components/topbar/topbar.dart +++ b/lib/screens/metering/components/topbar/topbar.dart @@ -13,8 +13,6 @@ import 'models/reading_value.dart'; class MeteringTopBar extends StatelessWidget { static const _columnsCount = 3; - final _isoDialogKey = GlobalKey(); - final _ndDialogKey = GlobalKey(); final ExposurePair? fastest; final ExposurePair? slowest; @@ -24,7 +22,7 @@ class MeteringTopBar extends StatelessWidget { final ValueChanged onIsoChanged; final ValueChanged onNdChanged; - MeteringTopBar({ + const MeteringTopBar({ required this.fastest, required this.slowest, required this.ev, @@ -94,16 +92,13 @@ class MeteringTopBar extends StatelessWidget { SizedBox( width: columnWidth, child: _AnimatedDialogPicker( - key: _isoDialogKey, title: S.of(context).iso, + subtitle: S.of(context).filmSpeed, selectedValue: iso, values: isoValues, - itemTitleBuilder: (context, value) => Text( - value.value.toString(), - style: value.stopType == StopType.full - ? null // use default - : Theme.of(context).textTheme.bodySmall, - ), + itemTitleBuilder: (_, value) => Text(value.value.toString()), + // using ascending order, because increase in film speed rises EV + evDifferenceBuilder: (selected, other) => selected.toStringDifference(other), onChanged: onIsoChanged, ), ), @@ -130,13 +125,15 @@ class MeteringTopBar extends StatelessWidget { ), const _InnerPadding(), _AnimatedDialogPicker( - key: _ndDialogKey, title: S.of(context).nd, + subtitle: S.of(context).ndFilterFactor, selectedValue: nd, values: ndValues, - itemTitleBuilder: (context, value) => Text( + 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, ), ], @@ -156,33 +153,50 @@ class _InnerPadding extends SizedBox { const _InnerPadding() : super(height: Dimens.grid16, width: Dimens.grid16); } -class _AnimatedDialogPicker extends AnimatedDialog { +class _AnimatedDialogPicker extends StatelessWidget { + final _key = GlobalKey(); + final String title; + final String subtitle; + final T selectedValue; + final List values; + final DialogPickerItemBuilder itemTitleBuilder; + final DialogPickerEvDifferenceBuilder evDifferenceBuilder; + final ValueChanged onChanged; + _AnimatedDialogPicker({ - required GlobalKey key, - required String title, - required T selectedValue, - required List values, - required Widget Function(BuildContext, T) itemTitleBuilder, - required ValueChanged onChanged, - }) : super( - key: key, - closedChild: ReadingContainer.singleValue( - value: ReadingValue( - label: title, - value: selectedValue.value.toString(), - ), - ), - openedChild: MeteringScreenDialogPicker( - title: title, - initialValue: selectedValue, - values: values, - itemTitleBuilder: itemTitleBuilder, - onCancel: () { - key.currentState?.close(); - }, - onSelect: (value) { - key.currentState?.close().then((_) => onChanged(value)); - }, - ), - ); + required this.title, + required this.subtitle, + required this.selectedValue, + required this.values, + required this.itemTitleBuilder, + required this.evDifferenceBuilder, + required this.onChanged, + }) : super(); + + @override + Widget build(BuildContext context) { + return AnimatedDialog( + key: _key, + closedChild: ReadingContainer.singleValue( + value: ReadingValue( + label: title, + value: selectedValue.value.toString(), + ), + ), + openedChild: MeteringScreenDialogPicker( + title: title, + subtitle: subtitle, + initialValue: selectedValue, + values: values, + itemTitleBuilder: itemTitleBuilder, + evDifferenceBuilder: evDifferenceBuilder, + onCancel: () { + _key.currentState?.close(); + }, + onSelect: (value) { + _key.currentState?.close().then((_) => onChanged(value)); + }, + ), + ); + } } diff --git a/lib/screens/metering/metering_bloc.dart b/lib/screens/metering/metering_bloc.dart index 1ce65bb..49fa8c2 100644 --- a/lib/screens/metering/metering_bloc.dart +++ b/lib/screens/metering/metering_bloc.dart @@ -64,7 +64,7 @@ class MeteringBloc extends Bloc { final shutterSpeed = _shutterSpeedValues[_random.nextInt(_shutterSpeedValues.thirdStops().length)]; final iso = _isoValues[_random.nextInt(_isoValues.thirdStops().length)]; - final evAtSystemIso = log2(pow(aperture.value, 2).toDouble()) - log2(shutterSpeed.value); + final evAtSystemIso = log2(pow(aperture.value, 2).toDouble() / shutterSpeed.value); final ev = evAtSystemIso - log2(iso.value / state.iso.value); emit(MeteringState(