mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-22 07:20:39 +00:00
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:
parent
0a45a2719b
commit
8381e5b753
8 changed files with 186 additions and 78 deletions
|
@ -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":
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,13 +32,32 @@ 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: [
|
||||||
|
ColoredBox(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
@ -39,19 +66,33 @@ class _MeteringScreenDialogPickerState<T> extends State<MeteringScreenDialogPick
|
||||||
Dimens.paddingL,
|
Dimens.paddingL,
|
||||||
Dimens.paddingM,
|
Dimens.paddingM,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
widget.title,
|
widget.title,
|
||||||
style: Theme.of(context).textTheme.headlineSmall!,
|
style: Theme.of(context).textTheme.headlineSmall!,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: Dimens.grid16),
|
||||||
|
Text(
|
||||||
|
widget.subtitle,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium!,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Divider(
|
Divider(
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
height: 0,
|
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,6 +113,10 @@ class _MeteringScreenDialogPickerState<T> extends State<MeteringScreenDialogPick
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ColoredBox(
|
||||||
|
color: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
Divider(
|
Divider(
|
||||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
height: 0,
|
height: 0,
|
||||||
|
@ -93,6 +141,9 @@ class _MeteringScreenDialogPickerState<T> extends State<MeteringScreenDialogPick
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedDialog(
|
||||||
|
key: _key,
|
||||||
closedChild: ReadingContainer.singleValue(
|
closedChild: ReadingContainer.singleValue(
|
||||||
value: ReadingValue(
|
value: ReadingValue(
|
||||||
label: title,
|
label: title,
|
||||||
value: selectedValue.value.toString(),
|
value: selectedValue.value.toString(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
openedChild: MeteringScreenDialogPicker(
|
openedChild: MeteringScreenDialogPicker<T>(
|
||||||
title: title,
|
title: title,
|
||||||
|
subtitle: subtitle,
|
||||||
initialValue: selectedValue,
|
initialValue: selectedValue,
|
||||||
values: values,
|
values: values,
|
||||||
itemTitleBuilder: itemTitleBuilder,
|
itemTitleBuilder: itemTitleBuilder,
|
||||||
|
evDifferenceBuilder: evDifferenceBuilder,
|
||||||
onCancel: () {
|
onCancel: () {
|
||||||
key.currentState?.close();
|
_key.currentState?.close();
|
||||||
},
|
},
|
||||||
onSelect: (value) {
|
onSelect: (value) {
|
||||||
key.currentState?.close().then((_) => onChanged(value));
|
_key.currentState?.close().then((_) => onChanged(value));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue