From 9edb22e4f48eaba528451f971b13efa75996ecd0 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 13 Feb 2025 23:50:09 +0100 Subject: [PATCH] starting with #30 --- lib/src/components/zoom_selector.dart | 194 ++++++++++++++++++ .../camera_to_share/camera_preview_view.dart | 17 +- 2 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 lib/src/components/zoom_selector.dart diff --git a/lib/src/components/zoom_selector.dart b/lib/src/components/zoom_selector.dart new file mode 100644 index 0000000..43da9f4 --- /dev/null +++ b/lib/src/components/zoom_selector.dart @@ -0,0 +1,194 @@ +import 'package:camerawesome/camerawesome_plugin.dart'; +import 'package:flutter/material.dart'; + +class ZoomSelector extends StatefulWidget { + final CameraState state; + + const ZoomSelector({ + super.key, + required this.state, + }); + + @override + State createState() => _ZoomSelectorState(); +} + +class _ZoomSelectorState extends State { + double? minZoom; + double? maxZoom; + + @override + void initState() { + super.initState(); + initAsync(); + } + + initAsync() async { + minZoom = await CamerawesomePlugin.getMinZoom(); + maxZoom = await CamerawesomePlugin.getMaxZoom(); + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: widget.state.sensorConfig$, + builder: (context, sensorConfigSnapshot) { + initAsync(); + if (sensorConfigSnapshot.data == null || + minZoom == null || + maxZoom == null) { + return const SizedBox.shrink(); + } + + return StreamBuilder( + stream: sensorConfigSnapshot.requireData.zoom$, + builder: (context, snapshot) { + if (snapshot.hasData) { + return _ZoomIndicatorLayout( + zoom: snapshot.requireData, + min: minZoom!, + max: maxZoom!, + sensorConfig: widget.state.sensorConfig, + ); + } else { + return const SizedBox.shrink(); + } + }, + ); + }, + ); + } +} + +class _ZoomIndicatorLayout extends StatelessWidget { + final double zoom; + final double min; + final double max; + final SensorConfig sensorConfig; + + const _ZoomIndicatorLayout({ + required this.zoom, + required this.min, + required this.max, + required this.sensorConfig, + }); + + @override + Widget build(BuildContext context) { + final displayZoom = (max - min) * zoom + min; + if (min == 1.0) { + return Container(); + } + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Show 3 dots for zooming: min, 1.0X and max zoom. The closer one shows + // text, the other ones a dot. + _ZoomIndicator( + normalValue: 0.0, + zoom: zoom, + selected: displayZoom < 1.0, + min: min, + max: max, + sensorConfig: sensorConfig, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: _ZoomIndicator( + normalValue: 0.5, + zoom: zoom, + selected: !(displayZoom < 1.0 || displayZoom == max), + min: min, + max: max, + sensorConfig: sensorConfig, + ), + ), + _ZoomIndicator( + normalValue: 1.0, + zoom: zoom, + selected: displayZoom == max, + min: min, + max: max, + sensorConfig: sensorConfig, + ), + ], + ); + } +} + +class _ZoomIndicator extends StatelessWidget { + final double zoom; + final double min; + final double max; + final double normalValue; + final SensorConfig sensorConfig; + final bool selected; + + const _ZoomIndicator({ + required this.zoom, + required this.min, + required this.max, + required this.normalValue, + required this.sensorConfig, + required this.selected, + }); + + @override + Widget build(BuildContext context) { + final baseTheme = AwesomeThemeProvider.of(context).theme; + final baseButtonTheme = baseTheme.buttonTheme; + final displayZoom = (max - min) * zoom + min; + Widget content = AnimatedSwitcher( + duration: const Duration(milliseconds: 100), + transitionBuilder: (child, anim) { + return ScaleTransition(scale: anim, child: child); + }, + child: selected + ? AwesomeBouncingWidget( + key: ValueKey("zoomIndicator_${normalValue}_selected"), + onTap: () { + sensorConfig.setZoom(normalValue); + }, + child: Container( + color: Colors.transparent, + padding: const EdgeInsets.all(0.0), + child: AwesomeCircleWidget( + theme: baseTheme, + child: Text( + "${displayZoom.toStringAsFixed(1)}X", + style: const TextStyle(color: Colors.white, fontSize: 12), + ), + ), + ), + ) + : AwesomeBouncingWidget( + key: ValueKey("zoomIndicator_${normalValue}_unselected"), + onTap: () { + sensorConfig.setZoom(normalValue); + }, + child: Container( + color: Colors.transparent, + padding: const EdgeInsets.all(16.0), + child: AwesomeCircleWidget( + theme: baseTheme.copyWith( + buttonTheme: baseButtonTheme.copyWith( + backgroundColor: baseButtonTheme.foregroundColor, + padding: EdgeInsets.zero, + ), + ), + child: const SizedBox(width: 6, height: 6), + ), + ), + ), + ); + + // Same width for each dot to keep them in their position + return SizedBox( + width: 56, + child: Center( + child: content, + ), + ); + } +} diff --git a/lib/src/views/camera_to_share/camera_preview_view.dart b/lib/src/views/camera_to_share/camera_preview_view.dart index 8d01bea..a372fa2 100644 --- a/lib/src/views/camera_to_share/camera_preview_view.dart +++ b/lib/src/views/camera_to_share/camera_preview_view.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:camerawesome/camerawesome_plugin.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:twonly/src/components/zoom_selector.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/components/image_editor/action_button.dart'; import 'package:twonly/src/components/media_view_sizing.dart'; @@ -65,7 +66,7 @@ class _CameraPreviewViewState extends State { previewAlignment: Alignment.topLeft, sensorConfig: SensorConfig.single( aspectRatio: CameraAspectRatios.ratio_16_9, - zoom: 0.07, + zoom: 0.5, ), previewFit: CameraPreviewFit.contain, progressIndicator: Container(), @@ -138,11 +139,19 @@ class _CameraPreviewViewState extends State { Positioned.fill( child: GestureDetector( onPanStart: (details) async { + if (cameraState.sensorConfig.sensors.first.position == + SensorPosition.front) { + return; + } setState(() { _basePanY = details.localPosition.dy; }); }, onPanUpdate: (details) async { + if (cameraState.sensorConfig.sensors.first.position == + SensorPosition.front) { + return; + } var diff = _basePanY - details.localPosition.dy; if (diff > 200) diff = 200; if (diff < 0) diff = 0; @@ -156,9 +165,13 @@ class _CameraPreviewViewState extends State { } }, onDoubleTap: () async { + bool isFront = + cameraState.sensorConfig.sensors.first.position == + SensorPosition.front; cameraState.switchCameraSensor( aspectRatio: CameraAspectRatios.ratio_16_9, flash: isFlashOn ? FlashMode.on : FlashMode.none, + zoom: isFront ? 0.5 : 0, ); }, ), @@ -222,7 +235,7 @@ class _CameraPreviewViewState extends State { alignment: Alignment.bottomCenter, child: Column( children: [ - AwesomeZoomSelector(state: cameraState), + ZoomSelector(state: cameraState), const SizedBox(height: 30), GestureDetector( onTap: () async {