separated reusable AnimatedCircluarButton

This commit is contained in:
Vadim 2024-05-03 12:24:36 +02:00
parent 43ab97f87f
commit 8765998680
9 changed files with 145 additions and 136 deletions

View file

@ -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<void> _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}'),
);
}

View file

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

View file

@ -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),
);
}

View file

@ -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<void> 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<void> 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();
}

View file

@ -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<MeteringMeasureButton> createState() => _MeteringMeasureButtonState();
}
class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
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();
}
}

View file

@ -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();
}
}

View file

@ -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<AnimatedCircluarButton> createState() => _AnimatedCircluarButtonState();
}
class _AnimatedCircluarButtonState extends State<AnimatedCircluarButton> {
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,
),
),
],
),
);
}
}

View file

@ -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<TimerScreen> with TickerProviderStateMixin
icon: const Icon(Icons.restore),
),
center: BlocBuilder<TimerBloc, TimerState>(
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<TimerScreen> with TickerProviderStateMixin
child: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: startStopIconAnimation,
color: Theme.of(context).colorScheme.surface,
),
),
),

View file

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