mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-21 23:10:40 +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"),
|
||||
"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":
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,13 +32,32 @@ 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: [
|
||||
ColoredBox(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
|
@ -39,19 +66,33 @@ class _MeteringScreenDialogPickerState<T> extends State<MeteringScreenDialogPick
|
|||
Dimens.paddingL,
|
||||
Dimens.paddingM,
|
||||
),
|
||||
child: Text(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,6 +113,10 @@ class _MeteringScreenDialogPickerState<T> extends State<MeteringScreenDialogPick
|
|||
),
|
||||
),
|
||||
),
|
||||
ColoredBox(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Column(
|
||||
children: [
|
||||
Divider(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
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 {
|
||||
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,
|
||||
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(
|
||||
openedChild: MeteringScreenDialogPicker<T>(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
initialValue: selectedValue,
|
||||
values: values,
|
||||
itemTitleBuilder: itemTitleBuilder,
|
||||
evDifferenceBuilder: evDifferenceBuilder,
|
||||
onCancel: () {
|
||||
key.currentState?.close();
|
||||
_key.currentState?.close();
|
||||
},
|
||||
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 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(
|
||||
|
|
Loading…
Reference in a new issue