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
This commit is contained in:
Vadim 2022-12-04 23:10:04 +03:00
parent 0a45a2719b
commit 8381e5b753
8 changed files with 186 additions and 78 deletions

View file

@ -25,11 +25,14 @@ class MessageLookup extends MessageLookupByLibrary {
"caffeine": MessageLookupByLibrary.simpleMessage("Caffeine"), "caffeine": MessageLookupByLibrary.simpleMessage("Caffeine"),
"cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "cancel": MessageLookupByLibrary.simpleMessage("Cancel"),
"fastestExposurePair": MessageLookupByLibrary.simpleMessage("Fastest"), "fastestExposurePair": MessageLookupByLibrary.simpleMessage("Fastest"),
"filmSpeed": MessageLookupByLibrary.simpleMessage("Film speed"),
"haptics": MessageLookupByLibrary.simpleMessage("Haptics"), "haptics": MessageLookupByLibrary.simpleMessage("Haptics"),
"iso": MessageLookupByLibrary.simpleMessage("ISO"), "iso": MessageLookupByLibrary.simpleMessage("ISO"),
"keepsScreenOn": "keepsScreenOn":
MessageLookupByLibrary.simpleMessage("Keeps screen on"), MessageLookupByLibrary.simpleMessage("Keeps screen on"),
"nd": MessageLookupByLibrary.simpleMessage("ND"), "nd": MessageLookupByLibrary.simpleMessage("ND"),
"ndFilterFactor": MessageLookupByLibrary.simpleMessage(
"Neutral density filter factor"),
"none": MessageLookupByLibrary.simpleMessage("None"), "none": MessageLookupByLibrary.simpleMessage("None"),
"openSettings": MessageLookupByLibrary.simpleMessage("Open settings"), "openSettings": MessageLookupByLibrary.simpleMessage("Open settings"),
"permissionNeeded": "permissionNeeded":

View file

@ -110,6 +110,16 @@ class S {
); );
} }
/// `Film speed`
String get filmSpeed {
return Intl.message(
'Film speed',
name: 'filmSpeed',
desc: '',
args: [],
);
}
/// `ND` /// `ND`
String get nd { String get nd {
return Intl.message( 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` /// `None`
String get none { String get none {
return Intl.message( return Intl.message(

View file

@ -6,7 +6,9 @@
"fastestExposurePair": "Fastest", "fastestExposurePair": "Fastest",
"slowestExposurePair": "Slowest", "slowestExposurePair": "Slowest",
"iso": "ISO", "iso": "ISO",
"filmSpeed": "Film speed",
"nd": "ND", "nd": "ND",
"ndFilterFactor": "Neutral density filter factor",
"none": "None", "none": "None",
"cancel": "Cancel", "cancel": "Cancel",
"select": "Select", "select": "Select",

View file

@ -1,14 +1,31 @@
import 'dart:math';
import 'package:lightmeter/utils/log_2.dart';
enum StopType { full, half, third } enum StopType { full, half, third }
abstract class PhotographyValue<T> { abstract class PhotographyValue<T extends num> {
final T rawValue; final T rawValue;
const PhotographyValue(this.rawValue); const PhotographyValue(this.rawValue);
T get value => 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<T> extends PhotographyValue<T> { abstract class PhotographyStopValue<T extends num> extends PhotographyValue<T> {
final StopType stopType; final StopType stopType;
const PhotographyStopValue(super.rawValue, this.stopType); const PhotographyStopValue(super.rawValue, this.stopType);

View file

@ -9,6 +9,7 @@ class Dimens {
static const double grid8 = 8; static const double grid8 = 8;
static const double grid16 = 16; static const double grid16 = 16;
static const double grid24 = 24; static const double grid24 = 24;
static const double grid56 = 56;
static const double grid168 = 168; static const double grid168 = 168;
static const double paddingM = 16; static const double paddingM = 16;

View file

@ -1,20 +1,28 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/models/photography_value.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
class MeteringScreenDialogPicker<T> extends StatefulWidget { typedef DialogPickerItemBuilder<T extends PhotographyValue> = Widget Function(BuildContext, T);
typedef DialogPickerEvDifferenceBuilder<T extends PhotographyValue> = String Function(T selected, T other);
class MeteringScreenDialogPicker<T extends PhotographyValue> extends StatefulWidget {
final String title; final String title;
final String subtitle;
final T initialValue; final T initialValue;
final List<T> values; final List<T> values;
final Widget Function(BuildContext context, T value) itemTitleBuilder; final DialogPickerItemBuilder<T> itemTitleBuilder;
final DialogPickerEvDifferenceBuilder<T> evDifferenceBuilder;
final VoidCallback onCancel; final VoidCallback onCancel;
final ValueChanged onSelect; final ValueChanged onSelect;
const MeteringScreenDialogPicker({ const MeteringScreenDialogPicker({
required this.title, required this.title,
required this.subtitle,
required this.initialValue, required this.initialValue,
required this.values, required this.values,
required this.itemTitleBuilder, required this.itemTitleBuilder,
required this.evDifferenceBuilder,
required this.onCancel, required this.onCancel,
required this.onSelect, required this.onSelect,
super.key, super.key,
@ -24,34 +32,67 @@ class MeteringScreenDialogPicker<T> extends StatefulWidget {
State<MeteringScreenDialogPicker<T>> createState() => _MeteringScreenDialogPickerState<T>(); State<MeteringScreenDialogPicker<T>> createState() => _MeteringScreenDialogPickerState<T>();
} }
class _MeteringScreenDialogPickerState<T> extends State<MeteringScreenDialogPicker<T>> { class _MeteringScreenDialogPickerState<T extends PhotographyValue> extends State<MeteringScreenDialogPicker<T>> {
late T _selectedValue = widget.initialValue; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Padding( ColoredBox(
padding: const EdgeInsets.fromLTRB( color: Theme.of(context).colorScheme.primaryContainer,
Dimens.paddingL, child: Column(
Dimens.paddingL, children: [
Dimens.paddingL, Padding(
Dimens.paddingM, 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( Expanded(
child: ListView.builder( child: ListView.builder(
controller: _scrollController,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
itemCount: widget.values.length, itemCount: widget.values.length,
itemExtent: Dimens.grid56,
itemBuilder: (context, index) => RadioListTile( itemBuilder: (context, index) => RadioListTile(
value: widget.values[index], value: widget.values[index],
groupValue: _selectedValue, groupValue: _selectedValue,
@ -59,6 +100,9 @@ class _MeteringScreenDialogPickerState<T> extends State<MeteringScreenDialogPick
style: Theme.of(context).textTheme.bodyLarge!, style: Theme.of(context).textTheme.bodyLarge!,
child: widget.itemTitleBuilder(context, widget.values[index]), child: widget.itemTitleBuilder(context, widget.values[index]),
), ),
secondary: widget.values[index].value != _selectedValue.value
? Text('${widget.evDifferenceBuilder.call(_selectedValue, widget.values[index])} EV')
: null,
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
setState(() { setState(() {
@ -69,25 +113,32 @@ class _MeteringScreenDialogPickerState<T> extends State<MeteringScreenDialogPick
), ),
), ),
), ),
Divider( ColoredBox(
color: Theme.of(context).colorScheme.onPrimaryContainer, color: Theme.of(context).colorScheme.primaryContainer,
height: 0, child: Column(
),
Padding(
padding: const EdgeInsets.all(Dimens.paddingL),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: [ children: [
const Spacer(), Divider(
TextButton( color: Theme.of(context).colorScheme.onPrimaryContainer,
onPressed: widget.onCancel, height: 0,
child: Text(S.of(context).cancel),
), ),
const SizedBox(width: Dimens.grid16), Padding(
TextButton( padding: const EdgeInsets.all(Dimens.paddingL),
onPressed: () => widget.onSelect(_selectedValue), child: Row(
child: Text(S.of(context).select), 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),
),
],
),
), ),
], ],
), ),

View file

@ -13,8 +13,6 @@ import 'models/reading_value.dart';
class MeteringTopBar extends StatelessWidget { class MeteringTopBar extends StatelessWidget {
static const _columnsCount = 3; static const _columnsCount = 3;
final _isoDialogKey = GlobalKey<AnimatedDialogState>();
final _ndDialogKey = GlobalKey<AnimatedDialogState>();
final ExposurePair? fastest; final ExposurePair? fastest;
final ExposurePair? slowest; final ExposurePair? slowest;
@ -24,7 +22,7 @@ class MeteringTopBar extends StatelessWidget {
final ValueChanged<IsoValue> onIsoChanged; final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged; final ValueChanged<NdValue> onNdChanged;
MeteringTopBar({ const MeteringTopBar({
required this.fastest, required this.fastest,
required this.slowest, required this.slowest,
required this.ev, required this.ev,
@ -94,16 +92,13 @@ class MeteringTopBar extends StatelessWidget {
SizedBox( SizedBox(
width: columnWidth, width: columnWidth,
child: _AnimatedDialogPicker( child: _AnimatedDialogPicker(
key: _isoDialogKey,
title: S.of(context).iso, title: S.of(context).iso,
subtitle: S.of(context).filmSpeed,
selectedValue: iso, selectedValue: iso,
values: isoValues, values: isoValues,
itemTitleBuilder: (context, value) => Text( itemTitleBuilder: (_, value) => Text(value.value.toString()),
value.value.toString(), // using ascending order, because increase in film speed rises EV
style: value.stopType == StopType.full evDifferenceBuilder: (selected, other) => selected.toStringDifference(other),
? null // use default
: Theme.of(context).textTheme.bodySmall,
),
onChanged: onIsoChanged, onChanged: onIsoChanged,
), ),
), ),
@ -130,13 +125,15 @@ class MeteringTopBar extends StatelessWidget {
), ),
const _InnerPadding(), const _InnerPadding(),
_AnimatedDialogPicker( _AnimatedDialogPicker(
key: _ndDialogKey,
title: S.of(context).nd, title: S.of(context).nd,
subtitle: S.of(context).ndFilterFactor,
selectedValue: nd, selectedValue: nd,
values: ndValues, values: ndValues,
itemTitleBuilder: (context, value) => Text( itemTitleBuilder: (_, value) => Text(
value.value == 0 ? S.of(context).none : value.value.toString(), 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, onChanged: onNdChanged,
), ),
], ],
@ -156,33 +153,50 @@ class _InnerPadding extends SizedBox {
const _InnerPadding() : super(height: Dimens.grid16, width: Dimens.grid16); const _InnerPadding() : super(height: Dimens.grid16, width: Dimens.grid16);
} }
class _AnimatedDialogPicker<T extends PhotographyValue> extends AnimatedDialog { class _AnimatedDialogPicker<T extends PhotographyValue> extends StatelessWidget {
final _key = GlobalKey<AnimatedDialogState>();
final String title;
final String subtitle;
final T selectedValue;
final List<T> values;
final DialogPickerItemBuilder<T> itemTitleBuilder;
final DialogPickerEvDifferenceBuilder<T> evDifferenceBuilder;
final ValueChanged<T> onChanged;
_AnimatedDialogPicker({ _AnimatedDialogPicker({
required GlobalKey<AnimatedDialogState> key, required this.title,
required String title, required this.subtitle,
required T selectedValue, required this.selectedValue,
required List<T> values, required this.values,
required Widget Function(BuildContext, T) itemTitleBuilder, required this.itemTitleBuilder,
required ValueChanged<T> onChanged, required this.evDifferenceBuilder,
}) : super( required this.onChanged,
key: key, }) : super();
closedChild: ReadingContainer.singleValue(
value: ReadingValue( @override
label: title, Widget build(BuildContext context) {
value: selectedValue.value.toString(), return AnimatedDialog(
), key: _key,
), closedChild: ReadingContainer.singleValue(
openedChild: MeteringScreenDialogPicker( value: ReadingValue(
title: title, label: title,
initialValue: selectedValue, value: selectedValue.value.toString(),
values: values, ),
itemTitleBuilder: itemTitleBuilder, ),
onCancel: () { openedChild: MeteringScreenDialogPicker<T>(
key.currentState?.close(); title: title,
}, subtitle: subtitle,
onSelect: (value) { initialValue: selectedValue,
key.currentState?.close().then((_) => onChanged(value)); values: values,
}, itemTitleBuilder: itemTitleBuilder,
), evDifferenceBuilder: evDifferenceBuilder,
); onCancel: () {
_key.currentState?.close();
},
onSelect: (value) {
_key.currentState?.close().then((_) => onChanged(value));
},
),
);
}
} }

View file

@ -64,7 +64,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
final shutterSpeed = _shutterSpeedValues[_random.nextInt(_shutterSpeedValues.thirdStops().length)]; final shutterSpeed = _shutterSpeedValues[_random.nextInt(_shutterSpeedValues.thirdStops().length)];
final iso = _isoValues[_random.nextInt(_isoValues.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); final ev = evAtSystemIso - log2(iso.value / state.iso.value);
emit(MeteringState( emit(MeteringState(