diff --git a/integration_test/e2e_test.dart b/integration_test/e2e_test.dart index ed9de80..419db8e 100644 --- a/integration_test/e2e_test.dart +++ b/integration_test/e2e_test.dart @@ -7,7 +7,7 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; +import 'package:lightmeter/screens/metering/components/bottom_controls/widget_bottom_controls.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart'; @@ -329,7 +329,7 @@ Future _expectMeteringStateAndMeasure( void expectMeasureButton(double ev) { find.descendant( - of: find.byType(MeteringMeasureButton), + of: find.byType(MeteringBottomControls), matching: find.text('${ev.toStringAsFixed(1)}\n${S.current.ev}'), ); } diff --git a/integration_test/purchases_test.dart b/integration_test/purchases_test.dart index 234a147..f001a5c 100644 --- a/integration_test/purchases_test.dart +++ b/integration_test/purchases_test.dart @@ -6,7 +6,6 @@ import 'package:lightmeter/data/models/ev_source_type.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart'; @@ -21,6 +20,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../integration_test/utils/widget_tester_actions.dart'; import 'mocks/iap_products_mock.dart'; +import 'utils/finder_actions.dart'; @isTest void testPurchases(String description) { @@ -84,7 +84,7 @@ void _expectProMeteringScreen({required bool enabled}) { expect(find.byType(NdValuePicker), findsOneWidget); expect( find.descendant( - of: find.byType(MeteringMeasureButton), + of: find.measureButton(), matching: find.byWidgetPredicate((widget) => widget is Text && widget.data!.contains('\u2081\u2080\u2080')), ), enabled ? findsOneWidget : findsNothing, diff --git a/integration_test/utils/finder_actions.dart b/integration_test/utils/finder_actions.dart new file mode 100644 index 0000000..507876d --- /dev/null +++ b/integration_test/utils/finder_actions.dart @@ -0,0 +1,10 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/screens/metering/components/bottom_controls/widget_bottom_controls.dart'; +import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart'; + +extension CommonFindersExtension on CommonFinders { + Finder measureButton() => find.descendant( + of: find.byType(MeteringBottomControls), + matching: find.byType(AnimatedCircluarButton), + ); +} diff --git a/integration_test/utils/widget_tester_actions.dart b/integration_test/utils/widget_tester_actions.dart index 7732d1a..8515c1d 100644 --- a/integration_test/utils/widget_tester_actions.dart +++ b/integration_test/utils/widget_tester_actions.dart @@ -5,7 +5,6 @@ import 'package:lightmeter/application_wrapper.dart'; import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart'; import 'package:lightmeter/screens/metering/screen_metering.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; @@ -13,6 +12,7 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import '../mocks/iap_products_mock.dart'; import '../mocks/paid_features_mock.dart'; +import 'finder_actions.dart'; import 'platform_channel_mock.dart'; const mockPhotoEv100 = 8.3; @@ -46,16 +46,16 @@ extension WidgetTesterCommonActions on WidgetTester { } Future takePhoto() async { - await tap(find.byType(MeteringMeasureButton)); + await tap(find.measureButton()); await pump(const Duration(seconds: 2)); // wait for circular progress indicator await pump(const Duration(seconds: 1)); // wait for circular progress indicator await pumpAndSettle(); } Future toggleIncidentMetering(double ev) async { - await tap(find.byType(MeteringMeasureButton)); + await tap(find.measureButton()); await sendMockIncidentEv(ev); - await tap(find.byType(MeteringMeasureButton)); + await tap(find.measureButton()); await pumpAndSettle(); } diff --git a/lib/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart b/lib/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart deleted file mode 100644 index 7104d5b..0000000 --- a/lib/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/user_preferences_provider.dart'; -import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart'; -import 'package:lightmeter/utils/context_utils.dart'; - -const String _subscript100 = '\u2081\u2080\u2080'; - -class MeteringMeasureButton extends StatefulWidget { - final double? ev; - final double? ev100; - final bool isMetering; - final VoidCallback onTap; - - const MeteringMeasureButton({ - required this.ev, - required this.ev100, - required this.isMetering, - required this.onTap, - super.key, - }); - - @override - State createState() => _MeteringMeasureButtonState(); -} - -class _MeteringMeasureButtonState extends State { - bool _isPressed = false; - - @override - void didUpdateWidget(covariant MeteringMeasureButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.isMetering != widget.isMetering) { - _isPressed = widget.isMetering; - } - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: widget.onTap, - onTapDown: (_) { - setState(() { - _isPressed = true; - }); - }, - onTapUp: (_) { - setState(() { - _isPressed = false; - }); - }, - onTapCancel: () { - setState(() { - _isPressed = false; - }); - }, - child: Stack( - children: [ - Center( - child: AnimatedScale( - duration: Dimens.durationS, - scale: _isPressed ? 0.9 : 1.0, - child: FilledCircle( - color: Theme.of(context).colorScheme.onSurface, - size: Dimens.grid72 - Dimens.grid8, - child: Center( - child: widget.ev != null ? _EvValueText(ev: widget.ev!, ev100: widget.ev100!) : null, - ), - ), - ), - ), - Positioned.fill( - child: CircularProgressIndicator( - /// This key is needed to make indicator start from the same point every time - key: ValueKey(widget.isMetering), - color: Theme.of(context).colorScheme.onSurface, - value: widget.isMetering ? null : 1, - ), - ), - ], - ), - ); - } -} - -class _EvValueText extends StatelessWidget { - final double ev; - final double ev100; - - const _EvValueText({ - required this.ev, - required this.ev100, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Text( - _text(context), - style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.surface), - textAlign: TextAlign.center, - ); - } - - String _text(BuildContext context) { - final bool showEv100 = context.isPro && UserPreferencesProvider.showEv100Of(context); - final StringBuffer buffer = StringBuffer() - ..writeAll([ - (showEv100 ? ev100 : ev).toStringAsFixed(1), - '\n', - S.of(context).ev, - if (showEv100) _subscript100, - ]); - return buffer.toString(); - } -} diff --git a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart index 5882f8d..957d0b7 100644 --- a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart +++ b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/ev_source_type.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; -import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; +import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart'; import 'package:lightmeter/screens/shared/bottom_controls_bar/widget_bottom_controls_bar.dart'; +import 'package:lightmeter/utils/context_utils.dart'; class MeteringBottomControls extends StatelessWidget { final double? ev; @@ -39,11 +40,11 @@ class MeteringBottomControls extends StatelessWidget { : S.of(context).tooltipUseLightSensor, ) : null, - center: MeteringMeasureButton( - ev: ev, - ev100: ev100, - isMetering: isMetering, - onTap: onMeasure, + center: AnimatedCircluarButton( + progress: isMetering ? null : 1.0, + isPressed: isMetering, + onPressed: onMeasure, + child: ev != null ? _EvValueText(ev: ev!, ev100: ev100!) : null, ), right: IconButton( onPressed: onSettings, @@ -53,3 +54,36 @@ class MeteringBottomControls extends StatelessWidget { ); } } + +class _EvValueText extends StatelessWidget { + static const String _subscript100 = '\u2081\u2080\u2080'; + final double ev; + final double ev100; + + const _EvValueText({ + required this.ev, + required this.ev100, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Text( + _text(context), + style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.surface), + textAlign: TextAlign.center, + ); + } + + String _text(BuildContext context) { + final bool showEv100 = context.isPro && UserPreferencesProvider.showEv100Of(context); + final StringBuffer buffer = StringBuffer() + ..writeAll([ + (showEv100 ? ev100 : ev).toStringAsFixed(1), + '\n', + S.of(context).ev, + if (showEv100) _subscript100, + ]); + return buffer.toString(); + } +} diff --git a/lib/screens/shared/animated_circular_button/widget_button_circular_animated.dart b/lib/screens/shared/animated_circular_button/widget_button_circular_animated.dart new file mode 100644 index 0000000..5057ca4 --- /dev/null +++ b/lib/screens/shared/animated_circular_button/widget_button_circular_animated.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart'; + +class AnimatedCircluarButton extends StatefulWidget { + final double? progress; + final bool isPressed; + final VoidCallback onPressed; + final Widget? child; + + const AnimatedCircluarButton({ + this.progress = 1.0, + required this.isPressed, + required this.onPressed, + this.child, + super.key, + }); + + @override + State createState() => _AnimatedCircluarButtonState(); +} + +class _AnimatedCircluarButtonState extends State { + bool _isPressed = false; + + @override + void didUpdateWidget(covariant AnimatedCircluarButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.isPressed != widget.isPressed) { + _isPressed = widget.isPressed; + } + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.onPressed, + onTapDown: (_) { + setState(() { + _isPressed = true; + }); + }, + onTapUp: (_) { + setState(() { + _isPressed = false; + }); + }, + onTapCancel: () { + setState(() { + _isPressed = false; + }); + }, + child: Stack( + children: [ + Center( + child: AnimatedScale( + duration: Dimens.durationS, + scale: _isPressed ? 0.9 : 1.0, + child: FilledCircle( + color: Theme.of(context).colorScheme.onSurface, + size: Dimens.grid72 - Dimens.grid8, + child: Center( + child: widget.child, + ), + ), + ), + ), + Positioned.fill( + child: CircularProgressIndicator( + /// This key is needed to make indicator start from the same point every time + key: ValueKey(widget.progress), + color: Theme.of(context).colorScheme.onSurface, + value: widget.progress, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/timer/screen_timer.dart b/lib/screens/timer/screen_timer.dart index 741bc79..cfeecdd 100644 --- a/lib/screens/timer/screen_timer.dart +++ b/lib/screens/timer/screen_timer.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart'; import 'package:lightmeter/screens/shared/bottom_controls_bar/widget_bottom_controls_bar.dart'; import 'package:lightmeter/screens/timer/bloc_timer.dart'; import 'package:lightmeter/screens/timer/components/metering_config/widget_metering_config_timer.dart'; @@ -96,8 +97,8 @@ class _TimerScreenState extends State with TickerProviderStateMixin icon: const Icon(Icons.restore), ), center: BlocBuilder( - builder: (_, state) => FloatingActionButton( - shape: state is TimerResumedState ? null : const CircleBorder(), + builder: (_, state) => AnimatedCircluarButton( + isPressed: state is TimerResumedState, onPressed: () { if (timelineAnimation.value == 0) { return; @@ -108,6 +109,7 @@ class _TimerScreenState extends State with TickerProviderStateMixin child: AnimatedIcon( icon: AnimatedIcons.play_pause, progress: startStopIconAnimation, + color: Theme.of(context).colorScheme.surface, ), ), ), diff --git a/test/screens/metering/screen_metering_golden_test.dart b/test/screens/metering/screen_metering_golden_test.dart index d17cb43..8c7e15f 100644 --- a/test/screens/metering/screen_metering_golden_test.dart +++ b/test/screens/metering/screen_metering_golden_test.dart @@ -8,11 +8,11 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; -import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; import 'package:lightmeter/screens/metering/flow_metering.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../../../integration_test/utils/finder_actions.dart'; import '../../../integration_test/utils/platform_channel_mock.dart'; import '../../application_mock.dart'; @@ -67,7 +67,7 @@ void main() { Future takePhoto(WidgetTester tester, Key scenarioWidgetKey) async { final button = find.descendant( of: find.byKey(scenarioWidgetKey), - matching: find.byType(MeteringMeasureButton), + matching: find.measureButton(), ); await tester.tap(button); await tester.pump(const Duration(seconds: 2)); // wait for circular progress indicator @@ -78,7 +78,7 @@ void main() { Future toggleIncidentMetering(WidgetTester tester, Key scenarioWidgetKey, double ev) async { final button = find.descendant( of: find.byKey(scenarioWidgetKey), - matching: find.byType(MeteringMeasureButton), + matching: find.measureButton(), ); await tester.tap(button); await sendMockIncidentEv(ev);