diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index 1d7d4b5..ad932bd 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -11,10 +11,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/platform_config.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; -import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' - as communication_event; -import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' - as communication_states; +import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_event; +import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart'; @@ -57,6 +55,7 @@ class CameraContainerBloc extends EvSourceBlocBase(_onZoomChanged); on(_onExposureOffsetChanged); on(_onExposureOffsetResetEvent); + on(_onExposureSpotChangedEvent); } @override @@ -166,9 +165,7 @@ class CameraContainerBloc extends EvSourceBlocBase _onZoomChanged(ZoomChangedEvent event, Emitter emit) async { - if (_cameraController != null && - event.value >= _zoomRange!.start && - event.value <= _zoomRange!.end) { + if (_cameraController != null && event.value >= _zoomRange!.start && event.value <= _zoomRange!.end) { _cameraController!.setZoomLevel(event.value); _currentZoom = event.value; _emitActiveState(emit); @@ -188,6 +185,14 @@ class CameraContainerBloc extends EvSourceBlocBase _onExposureSpotChangedEvent(ExposureSpotChangedEvent event, Emitter emit) async { + if (_cameraController != null) { + _cameraController!.setExposurePoint(event.offset); + _cameraController!.setFocusPoint(event.offset); + _emitActiveState(emit); + } + } + void _emitActiveState(Emitter emit) { emit( CameraActiveState( diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_spot_detector/widget_camera_spot_detector.dart b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_spot_detector/widget_camera_spot_detector.dart new file mode 100644 index 0000000..44ded02 --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_spot_detector/widget_camera_spot_detector.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; + +class CameraSpotDetector extends StatefulWidget { + final ValueChanged onSpotTap; + + const CameraSpotDetector({ + required this.onSpotTap, + super.key, + }); + + @override + State createState() => _CameraSpotDetectorState(); +} + +class _CameraSpotDetectorState extends State { + Offset? spot; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (_, constraints) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: (TapDownDetails details) => onViewFinderTap(details, constraints), + child: Stack( + children: [ + if (spot != null) + AnimatedPositioned( + duration: Dimens.durationS, + left: spot!.dx - Dimens.grid16 / 2, + top: spot!.dy - Dimens.grid16 / 2, + height: Dimens.grid16, + width: Dimens.grid16, + child: const _Spot(), + ), + ], + ), + ), + ); + } + + void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { + setState(() { + spot = details.localPosition; + }); + + widget.onSpotTap( + Offset( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ), + ); + } +} + +class _Spot extends StatelessWidget { + const _Spot(); + + @override + Widget build(BuildContext context) { + return const DecoratedBox( + decoration: BoxDecoration( + color: Colors.white70, + shape: BoxShape.circle, + ), + ); + } +} diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart index 7c06062..c054f3d 100644 --- a/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart +++ b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart @@ -12,14 +12,17 @@ class CameraView extends StatelessWidget { final value = controller.value; return ValueListenableBuilder( valueListenable: controller, - builder: (_, __, ___) => AspectRatio( + builder: (_, __, Widget? child) => AspectRatio( aspectRatio: _isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio), - child: value.isInitialized - ? RotatedBox( - quarterTurns: _getQuarterTurns(value), - child: controller.buildPreview(), - ) - : const SizedBox.shrink(), + child: Stack( + children: [ + RotatedBox( + quarterTurns: _getQuarterTurns(value), + child: controller.buildPreview(), + ), + child ?? const SizedBox(), + ], + ), ), ); } @@ -42,8 +45,6 @@ class CameraView extends StatelessWidget { DeviceOrientation _getApplicableOrientation(CameraValue value) { return value.isRecordingVideo ? value.recordingOrientation! - : (value.previewPauseOrientation ?? - value.lockedCaptureOrientation ?? - value.deviceOrientation); + : (value.previewPauseOrientation ?? value.lockedCaptureOrientation ?? value.deviceOrientation); } } diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart index 3e9538f..c38673c 100644 --- a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart +++ b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart @@ -4,6 +4,7 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/platform_config.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_spot_detector/widget_camera_spot_detector.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart'; @@ -12,8 +13,14 @@ import 'package:lightmeter/screens/metering/components/camera_container/models/c class CameraPreview extends StatefulWidget { final CameraController? controller; final CameraErrorType? error; + final ValueChanged onSpotTap; - const CameraPreview({this.controller, this.error, super.key}); + const CameraPreview({ + this.controller, + this.error, + required this.onSpotTap, + super.key, + }); @override State createState() => _CameraPreviewState(); @@ -31,7 +38,10 @@ class _CameraPreviewState extends State { AnimatedSwitcher( duration: Dimens.switchDuration, child: widget.controller != null - ? _CameraPreviewBuilder(controller: widget.controller!) + ? _CameraPreviewBuilder( + controller: widget.controller!, + onSpotTap: widget.onSpotTap, + ) : CameraViewPlaceholder(error: widget.error), ), ], @@ -43,16 +53,19 @@ class _CameraPreviewState extends State { class _CameraPreviewBuilder extends StatefulWidget { final CameraController controller; + final ValueChanged onSpotTap; - const _CameraPreviewBuilder({required this.controller}); + const _CameraPreviewBuilder({ + required this.controller, + required this.onSpotTap, + }); @override State<_CameraPreviewBuilder> createState() => _CameraPreviewBuilderState(); } class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> { - late final ValueNotifier _initializedNotifier = - ValueNotifier(widget.controller.value.isInitialized); + late final ValueNotifier _initializedNotifier = ValueNotifier(widget.controller.value.isInitialized); @override void initState() { @@ -89,6 +102,7 @@ class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> { bottom: Dimens.grid16, child: CameraHistogram(controller: widget.controller), ), + CameraSpotDetector(onSpotTap: widget.onSpotTap) ], ) : const SizedBox.shrink(), diff --git a/lib/screens/metering/components/camera_container/event_container_camera.dart b/lib/screens/metering/components/camera_container/event_container_camera.dart index d3e5995..6c02e27 100644 --- a/lib/screens/metering/components/camera_container/event_container_camera.dart +++ b/lib/screens/metering/components/camera_container/event_container_camera.dart @@ -1,3 +1,5 @@ +import 'package:flutter/gestures.dart'; + abstract class CameraContainerEvent { const CameraContainerEvent(); } @@ -53,3 +55,19 @@ class ExposureOffsetChangedEvent extends CameraContainerEvent { class ExposureOffsetResetEvent extends CameraContainerEvent { const ExposureOffsetResetEvent(); } + +class ExposureSpotChangedEvent extends CameraContainerEvent { + final Offset offset; + + const ExposureSpotChangedEvent(this.offset); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is ExposureSpotChangedEvent && other.offset == offset; + } + + @override + int get hashCode => Object.hash(offset, runtimeType); +} diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index 944aa34..a23545e 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -143,6 +143,9 @@ class _CameraViewBuilder extends StatelessWidget { builder: (context, state) => CameraPreview( controller: state is CameraInitializedState ? state.controller : null, error: state is CameraErrorState ? state.error : null, + onSpotTap: (value) { + context.read().add(ExposureSpotChangedEvent(value)); + }, ), ); } diff --git a/test/screens/metering/components/camera/event_container_camera_test.dart b/test/screens/metering/components/camera/event_container_camera_test.dart index f136b5f..3751f10 100644 --- a/test/screens/metering/components/camera/event_container_camera_test.dart +++ b/test/screens/metering/components/camera/event_container_camera_test.dart @@ -1,5 +1,7 @@ // ignore_for_file: prefer_const_constructors +import 'dart:ui'; + import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:test/test.dart'; @@ -41,4 +43,23 @@ void main() { }); }, ); + + group( + '`ExposureSpotChangedEvent`', + () { + final a = ExposureSpotChangedEvent(Offset(0.0, 0.0)); + final b = ExposureSpotChangedEvent(Offset(0.0, 0.0)); + final c = ExposureSpotChangedEvent(Offset(2.0, 2.0)); + test('==', () { + expect(a == b && b == a, true); + expect(a != c && c != a, true); + expect(b != c && c != b, true); + }); + test('hashCode', () { + expect(a.hashCode == b.hashCode, true); + expect(a.hashCode != c.hashCode, true); + expect(b.hashCode != c.hashCode, true); + }); + }, + ); }