ML-44 Save zoom level for equipment profile (#164)

* made zoom slider vertical & added more ticks to ruler

* show sliders values

* increased slider tappable area

* more accurate sliders values

* added zoom slider to equipment profiles settings

* split `EquipmentListTiles` widget

* set zoom on equipment profile change

* clamp zoom to the nearest value

* added missing translations

* added zoom checks to e2e test

* removed unused import
This commit is contained in:
Vadim 2024-04-07 10:54:57 +02:00 committed by GitHub
parent bfd0bfe531
commit 2117df2f11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 548 additions and 270 deletions

13
.vscode/launch.json vendored
View file

@ -61,5 +61,18 @@
],
"program": "${workspaceFolder}/lib/main_dev.dart",
},
{
"name": "integration-test",
"request": "launch",
"type": "dart",
"flutterMode": "debug",
"args": [
"--flavor",
"dev",
"--dart-define",
"cameraStubImage=assets/camera_stub_image.jpg"
],
"program": "${workspaceFolder}/integration_test/run_all_tests.dart",
},
],
}

View file

@ -16,6 +16,7 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container
import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart';
import 'package:lightmeter/screens/settings/screen_settings.dart';
import 'package:lightmeter/utils/double_to_zoom.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
import 'package:meta/meta.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -56,6 +57,8 @@ void testE2E(String description) {
await tester.setNdValues(0, mockEquipmentProfiles[0].ndValues);
await tester.setApertureValues(0, mockEquipmentProfiles[0].apertureValues);
await tester.setShutterSpeedValues(0, mockEquipmentProfiles[0].shutterSpeedValues);
await tester.setZoomValue(0, mockEquipmentProfiles[0].lensZoom);
expect(find.text('x1.91'), findsOneWidget);
expect(find.text('f/1.7 - f/16'), findsOneWidget);
expect(find.text('1/1000 - 16"'), findsOneWidget);
@ -65,6 +68,8 @@ void testE2E(String description) {
await tester.setProfileName(mockEquipmentProfiles[1].name);
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name);
await tester.setApertureValues(1, mockEquipmentProfiles[1].apertureValues);
await tester.setZoomValue(1, mockEquipmentProfiles[1].lensZoom);
expect(find.text('x5.02'), findsOneWidget);
expect(find.text('f/3.5 - f/22'), findsOneWidget);
expect(find.text('1/1000 - 16"'), findsNWidgets(2));
await tester.navigatorPop();
@ -171,6 +176,9 @@ extension EquipmentProfileActions on WidgetTester {
Future<void> setShutterSpeedValues(int profileIndex, List<ShutterSpeedValue> values) =>
_setDialogRangePickerValues<ShutterSpeedValue>(profileIndex, S.current.shutterSpeedValues, values);
Future<void> setZoomValue(int profileIndex, double value) =>
_setDialogSliderPickerValue(profileIndex, S.current.lensZoom, value);
}
extension on WidgetTester {
@ -235,6 +243,30 @@ extension on WidgetTester {
await tapSaveButton();
}
Future<void> _setDialogSliderPickerValue(
int profileIndex,
String listTileTitle,
double value,
) async {
await tap(find.text(listTileTitle).at(profileIndex));
await pumpAndSettle();
final sliderFinder = find.byType(Slider);
final trackWidth = getSize(sliderFinder).width - (2 * Dimens.paddingL);
final trackStep = trackWidth / (widget<Slider>(sliderFinder).max - widget<Slider>(sliderFinder).min);
final oldValue = widget<Slider>(sliderFinder).value;
final oldStart = (oldValue - 1) * trackStep;
final newStart = (value - 1) * trackStep;
await dragFrom(
getTopLeft(sliderFinder) + Offset(Dimens.paddingL + oldStart, getSize(sliderFinder).height / 2),
Offset(newStart - oldStart, 0),
);
await pump();
await tapSaveButton();
}
}
Future<void> _expectMeteringState(
@ -257,6 +289,7 @@ Future<void> _expectMeteringState(
await tester.scrollToTheLastExposurePair(equipmentProfile: equipmentProfile);
expectExposurePairsListItem(tester, slowest.split(' - ')[0], slowest.split(' - ')[1]);
expectMeasureButton(ev);
expect(find.text(equipmentProfile.lensZoom.toZoom()), findsOneWidget);
}
Future<void> _expectMeteringStateAndMeasure(

View file

@ -91,6 +91,7 @@ final mockEquipmentProfiles = [
IsoValue(1600, StopType.full),
IsoValue(3200, StopType.full),
],
lensZoom: 1.91,
),
EquipmentProfile(
id: '2',
@ -120,6 +121,7 @@ final mockEquipmentProfiles = [
IsoValue(1600, StopType.full),
IsoValue(3200, StopType.full),
],
lensZoom: 5.02,
),
];

View file

@ -60,6 +60,8 @@
"shutterSpeedValuesFilterDescription": "Select the range of shutter speed values to display. This is usually determined by the camera body you are using.",
"isoValues": "ISO values",
"isoValuesFilterDescription": "Select the ISO values to display. These may be your most commonly used values or those supported by your camera.",
"lensZoom": "Lens zoom",
"lensZoomDescription": "Set the zoom level relative to the phone's camera to match your camera's viewfinder.",
"equipmentProfile": "Equipment profile",
"equipmentProfiles": "Equipment profiles",
"tapToAdd": "Tap to add",

View file

@ -60,6 +60,8 @@
"shutterSpeedValuesFilterDescription": "Sélectionnez la plage de valeurs de vitesse d'obturation à afficher. Cela est généralement déterminé par le corps de l'appareil que vous utilisez.",
"isoValues": "Valeurs ISO",
"isoValuesFilterDescription": "Sélectionnez les valeurs ISO à afficher. Ce sont peut-être vos valeurs les plus couramment utilisées ou celles prises en charge par votre caméra.",
"lensZoom": "Zoom sur l'objectif",
"lensZoomDescription": "Réglez le niveau de zoom par rapport à l'appareil photo du téléphone pour qu'il corresponde au viseur de votre appareil photo.",
"equipmentProfile": "Profil de l'équipement",
"equipmentProfiles": "Profils de l'équipement",
"tapToAdd": "Appuie pour ajouter",

View file

@ -60,6 +60,8 @@
"shutterSpeedValuesFilterDescription": "Выберите диапазон значений выдержки. Обычно ограничивается возможностями вашей камеры.",
"isoValues": "Значения ISO",
"isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.",
"lensZoom": "Зум объектива",
"lensZoomDescription": "Установите уровень зума относительно камеры телефона, чтобы он соответствовал видоискателю вашей камеры.",
"equipmentProfile": "Оборудование",
"equipmentProfiles": "Профили оборудования",
"tapToAdd": "Нажмите, чтобы добавить",

View file

@ -60,6 +60,8 @@
"shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。",
"isoValues": "ISO",
"isoValuesFilterDescription": "选择要显示的 ISO 。这些可能是您常用的ISO值也可以是相机支持的ISO范围。",
"lensZoom": "镜头变焦",
"lensZoomDescription": "设置相对于手机摄像头的变焦级别,使其与相机取景器相匹配。",
"equipmentProfile": "设备配置",
"equipmentProfiles": "设备配置",
"tapToAdd": "点击添加",
@ -113,4 +115,4 @@
"tooltipUseLightSensor": "使用光线传感器",
"tooltipUseCamera": "使用摄像头",
"tooltipOpenSettings": "打开设置"
}
}

View file

@ -38,7 +38,8 @@ class Dimens {
// `CenteredSlider`
static const double cameraSliderTrackHeight = grid4;
static const double cameraSliderTrackRadius = cameraSliderTrackHeight / 2;
static const double cameraSliderHandleSize = 32;
static const double cameraSliderHandleArea = 32;
static const double cameraSliderHandleSize = 24;
static const double cameraSliderHandleIconSize = cameraSliderHandleSize * 2 / 3;
// Dialog

View file

@ -169,9 +169,10 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
}
Future<void> _onZoomChanged(ZoomChangedEvent event, Emitter emit) async {
if (_cameraController != null && event.value >= _zoomRange!.start && event.value <= _zoomRange!.end) {
_cameraController!.setZoomLevel(event.value);
_currentZoom = event.value;
if (_cameraController != null) {
final double zoom = event.value.clamp(_zoomRange!.start, _zoomRange!.end);
_cameraController!.setZoomLevel(zoom);
_currentZoom = zoom;
_emitActiveState(emit);
}
}

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart';
import 'package:lightmeter/screens/shared/ruler_slider/widget_slider_ruler.dart';
import 'package:lightmeter/utils/to_string_signed.dart';
class ExposureOffsetSlider extends StatelessWidget {
@ -18,76 +17,14 @@ class ExposureOffsetSlider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
IconButton(
icon: const Icon(Icons.sync),
onPressed: value != 0.0 ? () => onChanged(0.0) : null,
tooltip: S.of(context).tooltipResetToZero,
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.grid8),
child: _Ruler(
range.start,
range.end,
),
),
CenteredSlider(
isVertical: true,
icon: const Icon(Icons.light_mode),
value: value,
min: range.start,
max: range.end,
onChanged: onChanged,
),
],
),
),
],
);
}
}
class _Ruler extends StatelessWidget {
final double min;
final double max;
const _Ruler(this.min, this.max);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
(max - min + 1).toInt(),
(index) {
final bool showValue = index % 2 == 0.0 || index == 0.0;
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (showValue)
Text(
(index + min).toStringSignedAsFixed(0),
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(width: Dimens.grid8),
ColoredBox(
color: Theme.of(context).colorScheme.onBackground,
child: SizedBox(
height: 1,
width: showValue ? Dimens.grid16 : Dimens.grid8,
),
),
const SizedBox(width: Dimens.grid8),
],
);
},
).reversed.toList(),
return RulerSlider(
range: range,
value: value,
onChanged: onChanged,
icon: Icons.light_mode,
defaultValue: 0,
rulerValueAdapter: (value) => value.toStringSignedAsFixed(0),
valueAdapter: (value) => S.of(context).evValue(value.toStringSignedAsFixed(1)),
);
}
}

View file

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/screens/shared/ruler_slider/widget_slider_ruler.dart';
import 'package:lightmeter/utils/double_to_zoom.dart';
class ZoomSlider extends StatelessWidget {
class ZoomSlider extends StatefulWidget {
final RangeValues range;
final double value;
final ValueChanged<double> onChanged;
@ -13,14 +15,27 @@ class ZoomSlider extends StatelessWidget {
super.key,
});
@override
State<ZoomSlider> createState() => _ZoomSliderState();
}
class _ZoomSliderState extends State<ZoomSlider> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
widget.onChanged(EquipmentProfiles.selectedOf(context).lensZoom);
}
@override
Widget build(BuildContext context) {
return CenteredSlider(
icon: const Icon(Icons.search),
value: value,
min: range.start,
max: range.end,
onChanged: onChanged,
return RulerSlider(
range: widget.range,
value: widget.value,
onChanged: widget.onChanged,
icon: Icons.search,
defaultValue: EquipmentProfiles.selectedOf(context).lensZoom,
rulerValueAdapter: (value) => value.toStringAsFixed(0),
valueAdapter: (value) => value.toZoom(),
);
}
}

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/components/zoom_slider/widget_slider_zoom.dart';
@ -25,16 +24,14 @@ class CameraControls extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: ExposureOffsetSlider(
range: exposureOffsetRange,
value: exposureOffsetValue,
onChanged: onExposureOffsetChanged,
),
ExposureOffsetSlider(
range: exposureOffsetRange,
value: exposureOffsetValue,
onChanged: onExposureOffsetChanged,
),
const SizedBox(height: Dimens.grid24),
ZoomSlider(
range: zoomRange,
value: zoomValue,

View file

@ -157,7 +157,12 @@ class _CameraControlsBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
padding: const EdgeInsets.fromLTRB(
Dimens.paddingL,
Dimens.paddingL,
0,
Dimens.paddingM,
),
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
builder: (context, state) {
late final Widget child;

View file

@ -1,135 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentListTiles extends StatelessWidget {
final List<ApertureValue> selectedApertureValues;
final List<IsoValue> selectedIsoValues;
final List<NdValue> selectedNdValues;
final List<ShutterSpeedValue> selectedShutterSpeedValues;
final ValueChanged<List<ApertureValue>> onApertureValuesSelected;
final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
final ValueChanged<List<NdValue>> onNdValuesSelected;
final ValueChanged<List<ShutterSpeedValue>> onShutterSpeedValuesSelected;
const EquipmentListTiles({
required this.selectedApertureValues,
required this.selectedIsoValues,
required this.selectedNdValues,
required this.selectedShutterSpeedValues,
required this.onApertureValuesSelected,
required this.onIsoValuesSelecred,
required this.onNdValuesSelected,
required this.onShutterSpeedValuesSelected,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_EquipmentListTile<IsoValue>(
icon: Icons.iso,
title: S.of(context).isoValues,
description: S.of(context).isoValuesFilterDescription,
values: IsoValue.values,
selectedValues: selectedIsoValues,
rangeSelect: false,
onChanged: onIsoValuesSelecred,
),
_EquipmentListTile<NdValue>(
icon: Icons.filter_b_and_w,
title: S.of(context).ndFilters,
description: S.of(context).ndFiltersFilterDescription,
values: NdValue.values,
selectedValues: selectedNdValues,
rangeSelect: false,
onChanged: onNdValuesSelected,
),
_EquipmentListTile<ApertureValue>(
icon: Icons.camera,
title: S.of(context).apertureValues,
description: S.of(context).apertureValuesFilterDescription,
values: ApertureValue.values,
selectedValues: selectedApertureValues,
rangeSelect: true,
onChanged: onApertureValuesSelected,
),
_EquipmentListTile<ShutterSpeedValue>(
icon: Icons.shutter_speed,
title: S.of(context).shutterSpeedValues,
description: S.of(context).shutterSpeedValuesFilterDescription,
values: ShutterSpeedValue.values,
selectedValues: selectedShutterSpeedValues,
rangeSelect: true,
onChanged: onShutterSpeedValuesSelected,
),
],
);
}
}
class _EquipmentListTile<T extends PhotographyValue> extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final List<T> selectedValues;
final List<T> values;
final ValueChanged<List<T>> onChanged;
final bool rangeSelect;
const _EquipmentListTile({
required this.icon,
required this.title,
required this.description,
required this.selectedValues,
required this.values,
required this.onChanged,
required this.rangeSelect,
super.key,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon),
title: Text(title),
trailing: rangeSelect
? Text("${selectedValues.first} - ${selectedValues.last}")
: Text(
values.length == selectedValues.length
? S.of(context).equipmentProfileAllValues
: selectedValues.length.toString(),
),
onTap: () {
showDialog<List<T>>(
context: context,
builder: (_) => rangeSelect
? DialogRangePicker<T>(
icon: Icon(icon),
title: title,
description: description,
values: values,
selectedValues: selectedValues,
titleAdapter: (_, value) => value.toString(),
)
: DialogFilter<T>(
icon: Icon(icon),
title: title,
description: description,
values: values,
selectedValues: selectedValues,
titleAdapter: (_, value) => value.toString(),
),
).then((values) {
if (values != null) {
onChanged(values);
}
});
},
);
}
}

View file

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class FilterListTile<T extends PhotographyValue> extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final List<T> selectedValues;
final List<T> values;
final ValueChanged<List<T>> onChanged;
const FilterListTile({
required this.icon,
required this.title,
required this.description,
required this.selectedValues,
required this.values,
required this.onChanged,
super.key,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon),
title: Text(title),
trailing: Text(
values.length == selectedValues.length
? S.of(context).equipmentProfileAllValues
: selectedValues.length.toString(),
),
onTap: () {
showDialog<List<T>>(
context: context,
builder: (_) => DialogFilter<T>(
icon: Icon(icon),
title: title,
description: description,
values: values,
selectedValues: selectedValues,
titleAdapter: (_, value) => value.toString(),
),
).then((values) {
if (values != null) {
onChanged(values);
}
});
},
);
}
}

View file

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class RangePickerListTile<T extends PhotographyValue> extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final List<T> selectedValues;
final List<T> values;
final ValueChanged<List<T>> onChanged;
const RangePickerListTile({
required this.icon,
required this.title,
required this.description,
required this.selectedValues,
required this.values,
required this.onChanged,
super.key,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon),
title: Text(title),
trailing: Text("${selectedValues.first} - ${selectedValues.last}"),
onTap: () {
showDialog<List<T>>(
context: context,
builder: (_) => DialogRangePicker<T>(
icon: Icon(icon),
title: title,
description: description,
values: values,
selectedValues: selectedValues,
titleAdapter: (_, value) => value.toString(),
),
).then((values) {
if (values != null) {
onChanged(values);
}
});
},
);
}
}

View file

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_slider_picker/widget_dialog_slider_picker.dart';
class SliderPickerListTile extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final double value;
final RangeValues range;
final ValueChanged<double> onChanged;
final String Function(BuildContext, double) valueAdapter;
const SliderPickerListTile({
required this.icon,
required this.title,
required this.description,
required this.value,
required this.range,
required this.onChanged,
required this.valueAdapter,
super.key,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon),
title: Text(title),
trailing: Text(valueAdapter(context, value)),
onTap: () {
showDialog<double>(
context: context,
builder: (_) => DialogSliderPicker(
icon: Icon(icon),
title: title,
description: description,
value: value,
range: range,
valueAdapter: valueAdapter,
),
).then((value) {
if (value != null) {
onChanged(value);
}
});
},
);
}
}

View file

@ -4,8 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/equipment_list_tiles/widget_list_tiles_equipments.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/filter_list_tile/widget_list_tile_filter.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/range_picker_list_tile/widget_list_tile_range_picker.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/slider_picker_list_tile/widget_list_tile_slider_picker.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart';
import 'package:lightmeter/utils/double_to_zoom.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileContainer extends StatefulWidget {
@ -28,8 +31,7 @@ class EquipmentProfileContainer extends StatefulWidget {
State<EquipmentProfileContainer> createState() => EquipmentProfileContainerState();
}
class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
with TickerProviderStateMixin {
class EquipmentProfileContainerState extends State<EquipmentProfileContainer> with TickerProviderStateMixin {
late EquipmentProfile _equipmentData = EquipmentProfile(
id: widget.data.id,
name: widget.data.name,
@ -37,6 +39,7 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
ndValues: widget.data.ndValues,
shutterSpeedValues: widget.data.shutterSpeedValues,
isoValues: widget.data.isoValues,
lensZoom: widget.data.lensZoom,
);
late final AnimationController _controller = AnimationController(
@ -55,6 +58,7 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
ndValues: widget.data.ndValues,
shutterSpeedValues: widget.data.shutterSpeedValues,
isoValues: widget.data.isoValues,
lensZoom: widget.data.lensZoom,
);
}
@ -113,6 +117,10 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
_equipmentData = _equipmentData.copyWith(shutterSpeedValues: value);
widget.onUpdate(_equipmentData);
},
onLensZoomChanged: (value) {
_equipmentData = _equipmentData.copyWith(lensZoom: value);
widget.onUpdate(_equipmentData);
},
onCopy: widget.onCopy,
onDelete: widget.onDelete,
),
@ -154,8 +162,7 @@ class EquipmentProfileContainerState extends State<EquipmentProfileContainer>
}
class _AnimatedNameLeading extends AnimatedWidget {
const _AnimatedNameLeading({required AnimationController controller})
: super(listenable: controller);
const _AnimatedNameLeading({required AnimationController controller}) : super(listenable: controller);
Animation<double> get _progress => listenable as Animation<double>;
@ -200,6 +207,7 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget {
final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
final ValueChanged<List<NdValue>> onNdValuesSelected;
final ValueChanged<List<ShutterSpeedValue>> onShutterSpeedValuesSelected;
final ValueChanged<double> onLensZoomChanged;
final VoidCallback onCopy;
final VoidCallback onDelete;
@ -210,6 +218,7 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget {
required this.onIsoValuesSelecred,
required this.onNdValuesSelected,
required this.onShutterSpeedValuesSelected,
required this.onLensZoomChanged,
required this.onCopy,
required this.onDelete,
}) : super(listenable: controller);
@ -222,22 +231,53 @@ class _AnimatedEquipmentListTiles extends AnimatedWidget {
alignment: Alignment.topCenter,
size: Size(
double.maxFinite,
_progress.value * Dimens.grid56 * 5,
_progress.value * Dimens.grid56 * 6,
),
// https://github.com/gskinnerTeam/flutter-folio/pull/62
child: Opacity(
opacity: _progress.value,
child: Column(
children: [
EquipmentListTiles(
selectedApertureValues: equipmentData.apertureValues,
selectedIsoValues: equipmentData.isoValues,
selectedNdValues: equipmentData.ndValues,
selectedShutterSpeedValues: equipmentData.shutterSpeedValues,
onApertureValuesSelected: onApertureValuesSelected,
onIsoValuesSelecred: onIsoValuesSelecred,
onNdValuesSelected: onNdValuesSelected,
onShutterSpeedValuesSelected: onShutterSpeedValuesSelected,
FilterListTile<IsoValue>(
icon: Icons.iso,
title: S.of(context).isoValues,
description: S.of(context).isoValuesFilterDescription,
values: IsoValue.values,
selectedValues: equipmentData.isoValues,
onChanged: onIsoValuesSelecred,
),
FilterListTile<NdValue>(
icon: Icons.filter_b_and_w,
title: S.of(context).ndFilters,
description: S.of(context).ndFiltersFilterDescription,
values: NdValue.values,
selectedValues: equipmentData.ndValues,
onChanged: onNdValuesSelected,
),
RangePickerListTile<ApertureValue>(
icon: Icons.camera,
title: S.of(context).apertureValues,
description: S.of(context).apertureValuesFilterDescription,
values: ApertureValue.values,
selectedValues: equipmentData.apertureValues,
onChanged: onApertureValuesSelected,
),
RangePickerListTile<ShutterSpeedValue>(
icon: Icons.shutter_speed,
title: S.of(context).shutterSpeedValues,
description: S.of(context).shutterSpeedValuesFilterDescription,
values: ShutterSpeedValue.values,
selectedValues: equipmentData.shutterSpeedValues,
onChanged: onShutterSpeedValuesSelected,
),
SliderPickerListTile(
icon: Icons.zoom_in,
title: S.of(context).lensZoom,
description: S.of(context).lensZoomDescription,
value: equipmentData.lensZoom,
range: const RangeValues(1, 7),
onChanged: onLensZoomChanged,
valueAdapter: (_, value) => value.toZoom(),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),

View file

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
class DialogSliderPicker extends StatefulWidget {
final Icon icon;
final String title;
final String description;
final double value;
final RangeValues range;
final String Function(BuildContext context, double value) valueAdapter;
const DialogSliderPicker({
required this.icon,
required this.title,
required this.description,
required this.value,
required this.range,
required this.valueAdapter,
super.key,
});
@override
State<DialogSliderPicker> createState() => _DialogSliderPickerState();
}
class _DialogSliderPickerState extends State<DialogSliderPicker> {
late double value = widget.value;
@override
Widget build(BuildContext context) {
return AlertDialog(
icon: widget.icon,
titlePadding: Dimens.dialogIconTitlePadding,
title: Text(widget.title),
contentPadding: EdgeInsets.zero,
content: SizedBox(
width: double.maxFinite,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: Dimens.dialogIconTitlePadding,
child: Text(widget.description),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
child: Text(
widget.valueAdapter(context, value),
style: Theme.of(context).textTheme.bodyLarge,
),
),
Row(
children: [
Expanded(
child: Slider(
value: value,
min: widget.range.start,
max: widget.range.end,
onChanged: (value) {
setState(() {
this.value = value;
});
},
),
),
],
),
],
),
),
actionsPadding: Dimens.dialogActionsPadding,
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text(S.of(context).cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(value),
child: Text(S.of(context).save),
),
],
);
}
}

View file

@ -41,12 +41,12 @@ class _CenteredSliderState extends State<CenteredSlider> {
@override
Widget build(BuildContext context) {
return SizedBox(
height: widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleSize,
width: !widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleSize,
height: widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleArea,
width: !widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleArea,
child: LayoutBuilder(
builder: (context, constraints) {
final biggestSize = widget.isVertical ? constraints.maxHeight : constraints.maxWidth;
final handleDistance = biggestSize - Dimens.cameraSliderHandleSize;
final handleDistance = biggestSize - Dimens.cameraSliderHandleArea;
return RotatedBox(
quarterTurns: widget.isVertical ? -1 : 0,
child: GestureDetector(
@ -60,11 +60,11 @@ class _CenteredSliderState extends State<CenteredSlider> {
handleDistance,
),
child: SizedBox(
height: Dimens.cameraSliderHandleSize,
height: Dimens.cameraSliderHandleArea,
width: biggestSize,
child: _Slider(
handleDistance: handleDistance,
handleSize: Dimens.cameraSliderHandleSize,
handleSize: Dimens.cameraSliderHandleArea,
trackThickness: Dimens.cameraSliderTrackHeight,
value: relativeValue,
icon: RotatedBox(
@ -81,12 +81,12 @@ class _CenteredSliderState extends State<CenteredSlider> {
}
void _updateHandlePosition(double offset, double handleDistance) {
if (offset <= Dimens.cameraSliderHandleSize / 2) {
if (offset <= Dimens.cameraSliderHandleArea / 2) {
relativeValue = 0;
} else if (offset >= handleDistance + Dimens.cameraSliderHandleSize / 2) {
} else if (offset >= handleDistance + Dimens.cameraSliderHandleArea / 2) {
relativeValue = 1;
} else {
relativeValue = (offset - Dimens.cameraSliderHandleSize / 2) / handleDistance;
relativeValue = (offset - Dimens.cameraSliderHandleArea / 2) / handleDistance;
}
setState(() {});
widget.onChanged(relativeValue * (widget.max - widget.min) + widget.min);
@ -124,7 +124,7 @@ class _Slider extends StatelessWidget {
),
),
AnimatedPositioned.fromRect(
duration: Dimens.durationM,
duration: Dimens.durationS,
rect: Rect.fromCenter(
center: Offset(
handleSize / 2 + handleDistance * value,
@ -133,15 +133,17 @@ class _Slider extends StatelessWidget {
width: handleSize,
height: handleSize,
),
child: _Handle(
color: Theme.of(context).colorScheme.primary,
size: handleSize,
child: IconTheme(
data: Theme.of(context).iconTheme.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
size: Dimens.cameraSliderHandleIconSize,
),
child: icon,
child: Center(
child: _Handle(
color: Theme.of(context).colorScheme.primary,
size: Dimens.cameraSliderHandleSize,
child: IconTheme(
data: Theme.of(context).iconTheme.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
size: Dimens.cameraSliderHandleIconSize,
),
child: icon,
),
),
),
),
@ -163,15 +165,15 @@ class _Handle extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(size / 2),
child: SizedBox(
height: size,
width: size,
child: ColoredBox(
return SizedBox(
height: size,
width: size,
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
child: child,
shape: BoxShape.circle,
),
child: child,
),
);
}

View file

@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart';
class RulerSlider extends StatelessWidget {
final IconData icon;
final RangeValues range;
final double value;
final double defaultValue;
final ValueChanged<double> onChanged;
final String Function(double value) rulerValueAdapter;
final String Function(double value) valueAdapter;
const RulerSlider({
required this.icon,
required this.range,
required this.value,
required this.defaultValue,
required this.onChanged,
required this.rulerValueAdapter,
required this.valueAdapter,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
valueAdapter(value),
style: Theme.of(context).textTheme.labelLarge,
),
const SizedBox(height: Dimens.grid4),
Expanded(
child: Row(
children: [
_Ruler(
range.start,
range.end,
rulerValueAdapter,
),
CenteredSlider(
isVertical: true,
icon: Icon(icon),
value: value,
min: range.start,
max: range.end,
onChanged: onChanged,
),
],
),
),
IconButton(
icon: const Icon(Icons.sync),
onPressed: value != defaultValue ? () => onChanged(defaultValue) : null,
tooltip: S.of(context).tooltipResetToZero,
),
],
);
}
}
class _Ruler extends StatelessWidget {
final double min;
final double max;
final String Function(double value) rulerValueAdapter;
late final int mainTicksCount = (max - min + 1).toInt();
late final int itemsCount = mainTicksCount * 2 - 1;
_Ruler(
this.min,
this.max,
this.rulerValueAdapter,
);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final bool showAllMainTicks = Dimens.cameraSliderHandleArea * mainTicksCount <= constraints.maxHeight;
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
itemsCount,
(index) {
final bool isMainTick = index % 2 == 0.0;
if (!showAllMainTicks && !isMainTick) {
return const SizedBox();
}
final bool showValue = (index % (showAllMainTicks ? 2 : 4) == 0.0);
return SizedBox(
height: index == itemsCount - 1 || showValue ? Dimens.cameraSliderHandleArea : 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (showValue)
Text(
rulerValueAdapter(index / 2 + min),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(width: Dimens.grid4),
ColoredBox(
color: Theme.of(context).colorScheme.onBackground,
child: SizedBox(
height: 1,
width: isMainTick ? Dimens.grid8 : Dimens.grid4,
),
),
],
),
);
},
).reversed.toList(),
);
},
);
}
}

View file

@ -0,0 +1,3 @@
extension DoubleToZoom on double {
String toZoom() => 'x${toStringAsFixed(2)}';
}