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"),
"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":

View file

@ -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(

View file

@ -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",

View file

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

View file

@ -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;

View file

@ -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<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 subtitle;
final T initialValue;
final List<T> values;
final Widget Function(BuildContext context, T value) itemTitleBuilder;
final DialogPickerItemBuilder<T> itemTitleBuilder;
final DialogPickerEvDifferenceBuilder<T> 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<T> extends StatefulWidget {
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;
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<T> extends State<MeteringScreenDialogPick
style: Theme.of(context).textTheme.bodyLarge!,
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) {
if (value != null) {
setState(() {
@ -69,25 +113,32 @@ class _MeteringScreenDialogPickerState<T> extends State<MeteringScreenDialogPick
),
),
),
Divider(
color: Theme.of(context).colorScheme.onPrimaryContainer,
height: 0,
),
Padding(
padding: const EdgeInsets.all(Dimens.paddingL),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
ColoredBox(
color: Theme.of(context).colorScheme.primaryContainer,
child: Column(
children: [
const Spacer(),
TextButton(
onPressed: widget.onCancel,
child: Text(S.of(context).cancel),
Divider(
color: Theme.of(context).colorScheme.onPrimaryContainer,
height: 0,
),
const SizedBox(width: Dimens.grid16),
TextButton(
onPressed: () => 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),
),
],
),
),
],
),

View file

@ -13,8 +13,6 @@ import 'models/reading_value.dart';
class MeteringTopBar extends StatelessWidget {
static const _columnsCount = 3;
final _isoDialogKey = GlobalKey<AnimatedDialogState>();
final _ndDialogKey = GlobalKey<AnimatedDialogState>();
final ExposurePair? fastest;
final ExposurePair? slowest;
@ -24,7 +22,7 @@ class MeteringTopBar extends StatelessWidget {
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> 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<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({
required GlobalKey<AnimatedDialogState> key,
required String title,
required T selectedValue,
required List<T> values,
required Widget Function(BuildContext, T) itemTitleBuilder,
required ValueChanged<T> 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<T>(
title: title,
subtitle: subtitle,
initialValue: selectedValue,
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 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(