From 32f3f447cf7252833a051c297d3f971c0604c4be Mon Sep 17 00:00:00 2001 From: M123-dev Date: Thu, 20 Jan 2022 19:51:51 +0100 Subject: [PATCH 1/4] fix: Camera state management --- .../cards/product_cards/question_card.dart | 1 - .../lib/pages/scan/ml_kit_scan_page.dart | 107 +++++++++++------- 2 files changed, 66 insertions(+), 42 deletions(-) diff --git a/packages/smooth_app/lib/cards/product_cards/question_card.dart b/packages/smooth_app/lib/cards/product_cards/question_card.dart index 610f50bc85b..bdb1fd3828d 100644 --- a/packages/smooth_app/lib/cards/product_cards/question_card.dart +++ b/packages/smooth_app/lib/cards/product_cards/question_card.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; diff --git a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart index b1658e84364..29b581ceafa 100644 --- a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart @@ -19,20 +19,23 @@ class MLKitScannerPage extends StatefulWidget { class MLKitScannerPageState extends State { BarcodeScanner? barcodeScanner = GoogleMlKit.vision.barcodeScanner(); + CameraLensDirection cameraLensDirection = CameraLensDirection.back; late ContinuousScanModel _model; CameraController? _controller; int _cameraIndex = 0; - CameraLensDirection cameraLensDirection = CameraLensDirection.back; bool isBusy = false; - bool imageStreamActive = false; + //Used when rebuilding to stop the camera + bool stoppingCamera = false; @override void initState() { super.initState(); - //Find the most relevant camera to use if none of these criteria are met, - //the default value of [_cameraIndex] will be used to select the first - //camera in the global cameras list. + // Find the most relevant camera to use if none of these criteria are met, + // the default value of [_cameraIndex] will be used to select the first + // camera in the global cameras list. + // if non matching is found we fall back to the first in the list + // initValue of [_cameraIndex] if (cameras.any( (CameraDescription element) => element.lensDirection == cameraLensDirection && @@ -53,22 +56,36 @@ class MLKitScannerPageState extends State { ); } - _startLiveFeed(); + startLiveFeed(); } @override void dispose() { - _stopImageStream().then( - (_) => _controller?.dispose(), - ); + _controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { _model = context.watch(); - if (_controller == null || _controller!.value.isInitialized == false) { - return const Center(child: CircularProgressIndicator()); + + return Scaffold( + body: ScannerOverlay( + restartCamera: startLiveFeed, + stopCamera: _stopImageStream, + model: _model, + scannerWidget: _buildScannerWidget(), + ), + ); + } + + Widget _buildScannerWidget() { + // Showing the black scanner background + the icon when the scanner is + // loading or stopped + if (_controller == null || + _controller!.value.isInitialized == false || + stoppingCamera) { + return Container(); } final Size size = MediaQuery.of(context).size; @@ -84,55 +101,63 @@ class MLKitScannerPageState extends State { scale = 1 / scale; } - return Scaffold( - body: ScannerOverlay( - restartCamera: _resumeImageStream, - stopCamera: _stopImageStream, - model: _model, - scannerWidget: Transform.scale( - scale: scale, - child: Center( - child: CameraPreview( - _controller!, - ), - ), + return Transform.scale( + scale: scale, + child: Center( + key: ValueKey(stoppingCamera), + child: CameraPreview( + _controller!, ), ), ); } - Future _startLiveFeed() async { + Future startLiveFeed() async { + stoppingCamera = false; final CameraDescription camera = cameras[_cameraIndex]; - _controller = CameraController( + + final CameraController cameraController = CameraController( camera, ResolutionPreset.high, enableAudio: false, ); - _controller!.setFocusMode(FocusMode.auto); - _controller!.lockCaptureOrientation(DeviceOrientation.portraitUp); + cameraController.setFocusMode(FocusMode.auto); + cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp); + + _controller = cameraController; - _controller!.initialize().then((_) { - if (!mounted) { - return; + // If the controller is updated then update the UI. + cameraController.addListener(() { + if (mounted) { + setState(() {}); + } + if (cameraController.value.hasError) { + if (kDebugMode) { + print(cameraController.value.errorDescription); + } } - _controller!.startImageStream(_processCameraImage); - imageStreamActive = true; - setState(() {}); }); - } - void _resumeImageStream() { - if (_controller != null && !imageStreamActive) { - _controller!.startImageStream(_processCameraImage); - imageStreamActive = true; + try { + await cameraController.initialize(); + _controller?.startImageStream(_processCameraImage); + } on CameraException catch (e) { + if (kDebugMode) { + print(e); + } + } + + if (mounted) { + setState(() {}); } } Future _stopImageStream() async { - if (_controller != null) { - await _controller!.stopImageStream(); - imageStreamActive = false; + stoppingCamera = true; + if (mounted) { + setState(() {}); } + await _controller?.dispose(); } //Convert the [CameraImage] to a [InputImage] From 04b49c27839297ad57334fc4dc3e1f36b8091d6b Mon Sep 17 00:00:00 2001 From: M123-dev Date: Thu, 20 Jan 2022 20:52:45 +0100 Subject: [PATCH 2/4] Extracted lifecycle to own ScannerStateManager --- .../lib/pages/scan/continuous_scan_page.dart | 27 +++---- .../lib/pages/scan/ml_kit_scan_page.dart | 13 ++- .../smooth_app/lib/pages/scan/scan_page.dart | 14 +++- .../lib/pages/scan/scanner_overlay.dart | 81 +++---------------- .../lib/pages/scan/scanner_state_manager.dart | 64 +++++++++++++++ 5 files changed, 102 insertions(+), 97 deletions(-) create mode 100644 packages/smooth_app/lib/pages/scan/scanner_state_manager.dart diff --git a/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart b/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart index 2b9f3b1eafe..3f057e74771 100644 --- a/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:smooth_app/data_models/continuous_scan_model.dart'; -import 'package:smooth_app/pages/scan/scanner_overlay.dart'; +import 'package:smooth_app/pages/scan/scanner_state_manager.dart'; class ContinuousScanPage extends StatefulWidget { const ContinuousScanPage(); @@ -25,21 +25,18 @@ class _ContinuousScanPageState extends State { constraints.maxHeight / 1.81; // roughly 55% of the available height final double viewFinderBottomOffset = carouselHeight / 2.0; - return Scaffold( - body: ScannerOverlay( - model: _model, - restartCamera: _resumeLiveFeed, - stopCamera: _stopLiveFeed, - scannerWidget: QRView( - overlay: QrScannerOverlayShape( - // We use [SmoothViewFinder] instead of the overlay. - overlayColor: Colors.transparent, - // This offset adjusts the scanning area on iOS. - cutOutBottomOffset: viewFinderBottomOffset, - ), - key: _scannerViewKey, - onQRViewCreated: setupScanner, + return ScannerStateManager( + restartCamera: _resumeLiveFeed, + stopCamera: _stopLiveFeed, + child: QRView( + overlay: QrScannerOverlayShape( + // We use [SmoothViewFinder] instead of the overlay. + overlayColor: Colors.transparent, + // This offset adjusts the scanning area on iOS. + cutOutBottomOffset: viewFinderBottomOffset, ), + key: _scannerViewKey, + onQRViewCreated: setupScanner, ), ); }, diff --git a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart index 29b581ceafa..bccab074fc2 100644 --- a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart @@ -8,7 +8,7 @@ import 'package:google_ml_barcode_scanner/google_ml_barcode_scanner.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/continuous_scan_model.dart'; import 'package:smooth_app/main.dart'; -import 'package:smooth_app/pages/scan/scanner_overlay.dart'; +import 'package:smooth_app/pages/scan/scanner_state_manager.dart'; class MLKitScannerPage extends StatefulWidget { const MLKitScannerPage({Key? key}) : super(key: key); @@ -69,13 +69,10 @@ class MLKitScannerPageState extends State { Widget build(BuildContext context) { _model = context.watch(); - return Scaffold( - body: ScannerOverlay( - restartCamera: startLiveFeed, - stopCamera: _stopImageStream, - model: _model, - scannerWidget: _buildScannerWidget(), - ), + return ScannerStateManager( + restartCamera: startLiveFeed, + stopCamera: _stopImageStream, + child: _buildScannerWidget(), ); } diff --git a/packages/smooth_app/lib/pages/scan/scan_page.dart b/packages/smooth_app/lib/pages/scan/scan_page.dart index 2fad1bc8999..1a465c11244 100644 --- a/packages/smooth_app/lib/pages/scan/scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/scan_page.dart @@ -6,6 +6,7 @@ import 'package:smooth_app/data_models/user_preferences.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/pages/scan/continuous_scan_page.dart'; import 'package:smooth_app/pages/scan/ml_kit_scan_page.dart'; +import 'package:smooth_app/pages/scan/scanner_overlay.dart'; import 'package:smooth_app/pages/user_preferences_dev_mode.dart'; class ScanPage extends StatefulWidget { @@ -35,10 +36,12 @@ class _ScanPageState extends State { } Future _permissionCheck( - UserPreferences userPreferences) async { + UserPreferences userPreferences, + ) async { final PermissionStatus status = await Permission.camera.status; - //If is denied, is not restricted by for example parental control and is not already declined once + // If is denied, is not restricted by for example parental control and is + // not already declined once if (status.isDenied && !status.isRestricted && !userPreferences.cameraDeclinedOnce) { @@ -93,7 +96,12 @@ class _ScanPageState extends State { return ChangeNotifierProvider( create: (BuildContext context) => _model!, - child: child, + child: Scaffold( + body: ScannerOverlay( + child: child, + model: _model!, + ), + ), ); }, ); diff --git a/packages/smooth_app/lib/pages/scan/scanner_overlay.dart b/packages/smooth_app/lib/pages/scan/scanner_overlay.dart index 70f7ad1bff4..133d978c71a 100644 --- a/packages/smooth_app/lib/pages/scan/scanner_overlay.dart +++ b/packages/smooth_app/lib/pages/scan/scanner_overlay.dart @@ -1,67 +1,27 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:smooth_app/data_models/continuous_scan_model.dart'; import 'package:smooth_app/pages/scan/scan_page_helper.dart'; import 'package:smooth_app/widgets/smooth_product_carousel.dart'; import 'package:smooth_ui_library/animations/smooth_reveal_animation.dart'; import 'package:smooth_ui_library/widgets/smooth_view_finder.dart'; -import 'package:visibility_detector/visibility_detector.dart'; /// This builds all the essential widgets which are displayed above the camera /// preview, like the [SmoothProductCarousel], the [SmoothViewFinder] and the -/// clear and compare buttons row. It takes the camera preview widget to display -/// and functions to stop and restart the camera, to only activate the camera -/// when the screen is currently visible. -class ScannerOverlay extends StatefulWidget { +/// clear and compare buttons row. +class ScannerOverlay extends StatelessWidget { const ScannerOverlay({ - required this.scannerWidget, + required this.child, required this.model, - required this.restartCamera, - required this.stopCamera, }); - final Widget scannerWidget; + final Widget child; final ContinuousScanModel model; - final Function() restartCamera; - final Function() stopCamera; static const double carouselHeightPct = 0.55; static const double scannerWidthPct = 0.6; static const double scannerHeightPct = 0.33; static const double buttonRowHeightPx = 48; - @override - State createState() => _ScannerOverlayState(); -} - -class _ScannerOverlayState extends State - with WidgetsBindingObserver { - @override - void initState() { - super.initState(); - WidgetsBinding.instance!.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance!.removeObserver(this); - super.dispose(); - } - - // Lifecycle changes are not handled by either of the used plugin. This means - // we are responsible to control camera resources when the lifecycle state is - // updated. Failure to do so might lead to unexpected behavior - // didChangeAppLifecycleState is called when the system puts the app in the - // background or returns the app to the foreground. - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.inactive) { - widget.stopCamera.call(); - } else if (state == AppLifecycleState.resumed) { - widget.restartCamera.call(); - } - } - @override Widget build(BuildContext context) { return LayoutBuilder( @@ -76,9 +36,8 @@ class _ScannerOverlayState extends State ); final double carouselHeight = constraints.maxHeight * ScannerOverlay.carouselHeightPct; - final double buttonRowHeight = areButtonsRendered(widget.model) - ? ScannerOverlay.buttonRowHeightPx - : 0; + final double buttonRowHeight = + areButtonsRendered(model) ? ScannerOverlay.buttonRowHeightPx : 0; final double availableScanHeight = constraints.maxHeight - carouselHeight - buttonRowHeight; @@ -87,35 +46,15 @@ class _ScannerOverlayState extends State top: (availableScanHeight - scannerSize.height) / 2 + buttonRowHeight); - return VisibilityDetector( - key: const ValueKey('VisibilityDetector'), - onVisibilityChanged: (VisibilityInfo info) { - if (info.visibleFraction == 0.0) { - widget.stopCamera.call(); - } else { - widget.restartCamera.call(); - } - }, + return Container( + color: Colors.black, child: Stack( children: [ - Container( - alignment: Alignment.center, - color: Colors.black, - child: Padding( - padding: qrScannerPadding, - child: SvgPicture.asset( - 'assets/actions/scanner_alt_2.svg', - width: scannerSize.width * 0.8, - height: scannerSize.height * 0.8, - color: Colors.white, - ), - ), - ), SmoothRevealAnimation( delay: 400, startOffset: Offset.zero, animationCurve: Curves.easeInOutBack, - child: widget.scannerWidget, + child: child, ), SmoothRevealAnimation( delay: 400, @@ -141,7 +80,7 @@ class _ScannerOverlayState extends State child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - buildButtonsRow(context, widget.model), + buildButtonsRow(context, model), const Spacer(), SmoothProductCarousel( showSearchCard: true, diff --git a/packages/smooth_app/lib/pages/scan/scanner_state_manager.dart b/packages/smooth_app/lib/pages/scan/scanner_state_manager.dart new file mode 100644 index 00000000000..efc89fadcd8 --- /dev/null +++ b/packages/smooth_app/lib/pages/scan/scanner_state_manager.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:visibility_detector/visibility_detector.dart'; + +/// This Widgets tracks if the scanner is currently visible and if the app +/// is currently open/idle/closed and controls the camera depending +class ScannerStateManager extends StatefulWidget { + const ScannerStateManager({ + required this.restartCamera, + required this.stopCamera, + required this.child, + Key? key, + }) : super(key: key); + + final Function() restartCamera; + final Function() stopCamera; + final Widget child; + + @override + ScannerStateManagerState createState() => ScannerStateManagerState(); +} + +class ScannerStateManagerState extends State + with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance!.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance!.removeObserver(this); + super.dispose(); + } + + // Lifecycle changes are not handled by either of the used plugin. This means + // we are responsible to control camera resources when the lifecycle state is + // updated. Failure to do so might lead to unexpected behavior + // didChangeAppLifecycleState is called when the system puts the app in the + // background or returns the app to the foreground. + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.inactive) { + widget.stopCamera.call(); + } else if (state == AppLifecycleState.resumed) { + widget.restartCamera.call(); + } + } + + @override + Widget build(BuildContext context) { + return VisibilityDetector( + key: const ValueKey('VisibilityDetector'), + onVisibilityChanged: (VisibilityInfo info) { + if (info.visibleFraction == 0.0) { + widget.stopCamera.call(); + } else { + widget.restartCamera.call(); + } + }, + child: widget.child, + ); + } +} From 059fd1240aadd625bbbb7b294396f86bc42d84b3 Mon Sep 17 00:00:00 2001 From: M123-dev Date: Thu, 20 Jan 2022 21:38:11 +0100 Subject: [PATCH 3/4] Update ml_kit_scan_page.dart --- packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart index bccab074fc2..0e32c1a7ab0 100644 --- a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart @@ -129,9 +129,7 @@ class MLKitScannerPageState extends State { setState(() {}); } if (cameraController.value.hasError) { - if (kDebugMode) { - print(cameraController.value.errorDescription); - } + debugPrint(cameraController.value.errorDescription); } }); @@ -140,7 +138,7 @@ class MLKitScannerPageState extends State { _controller?.startImageStream(_processCameraImage); } on CameraException catch (e) { if (kDebugMode) { - print(e); + debugPrint(e.toString()); } } From 740a4f1421ff547bbe0ab0e192591ace54a2a049 Mon Sep 17 00:00:00 2001 From: M123-dev Date: Fri, 21 Jan 2022 12:34:50 +0100 Subject: [PATCH 4/4] Review --- .../lib/pages/scan/continuous_scan_page.dart | 6 ++--- .../lib/pages/scan/ml_kit_scan_page.dart | 11 +++++---- .../lib/pages/scan/scanner_state_manager.dart | 24 +++++++++---------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart b/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart index 3f057e74771..b1370dc9673 100644 --- a/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/continuous_scan_page.dart @@ -25,9 +25,9 @@ class _ContinuousScanPageState extends State { constraints.maxHeight / 1.81; // roughly 55% of the available height final double viewFinderBottomOffset = carouselHeight / 2.0; - return ScannerStateManager( - restartCamera: _resumeLiveFeed, - stopCamera: _stopLiveFeed, + return LifeCycleManager( + onResume: _resumeLiveFeed, + onStop: _stopLiveFeed, child: QRView( overlay: QrScannerOverlayShape( // We use [SmoothViewFinder] instead of the overlay. diff --git a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart index 0e32c1a7ab0..6b9a3a3fc3a 100644 --- a/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart +++ b/packages/smooth_app/lib/pages/scan/ml_kit_scan_page.dart @@ -56,7 +56,7 @@ class MLKitScannerPageState extends State { ); } - startLiveFeed(); + _startLiveFeed(); } @override @@ -69,9 +69,9 @@ class MLKitScannerPageState extends State { Widget build(BuildContext context) { _model = context.watch(); - return ScannerStateManager( - restartCamera: startLiveFeed, - stopCamera: _stopImageStream, + return LifeCycleManager( + onResume: _startLiveFeed, + onStop: _stopImageStream, child: _buildScannerWidget(), ); } @@ -109,7 +109,7 @@ class MLKitScannerPageState extends State { ); } - Future startLiveFeed() async { + Future _startLiveFeed() async { stoppingCamera = false; final CameraDescription camera = cameras[_cameraIndex]; @@ -138,6 +138,7 @@ class MLKitScannerPageState extends State { _controller?.startImageStream(_processCameraImage); } on CameraException catch (e) { if (kDebugMode) { + // TODO(M123): Show error message debugPrint(e.toString()); } } diff --git a/packages/smooth_app/lib/pages/scan/scanner_state_manager.dart b/packages/smooth_app/lib/pages/scan/scanner_state_manager.dart index efc89fadcd8..6c6c1ed40df 100644 --- a/packages/smooth_app/lib/pages/scan/scanner_state_manager.dart +++ b/packages/smooth_app/lib/pages/scan/scanner_state_manager.dart @@ -3,23 +3,23 @@ import 'package:visibility_detector/visibility_detector.dart'; /// This Widgets tracks if the scanner is currently visible and if the app /// is currently open/idle/closed and controls the camera depending -class ScannerStateManager extends StatefulWidget { - const ScannerStateManager({ - required this.restartCamera, - required this.stopCamera, +class LifeCycleManager extends StatefulWidget { + const LifeCycleManager({ + required this.onResume, + required this.onStop, required this.child, Key? key, }) : super(key: key); - final Function() restartCamera; - final Function() stopCamera; + final Function() onResume; + final Function() onStop; final Widget child; @override - ScannerStateManagerState createState() => ScannerStateManagerState(); + LifeCycleManagerState createState() => LifeCycleManagerState(); } -class ScannerStateManagerState extends State +class LifeCycleManagerState extends State with WidgetsBindingObserver { @override void initState() { @@ -41,9 +41,9 @@ class ScannerStateManagerState extends State @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.inactive) { - widget.stopCamera.call(); + widget.onStop.call(); } else if (state == AppLifecycleState.resumed) { - widget.restartCamera.call(); + widget.onResume.call(); } } @@ -53,9 +53,9 @@ class ScannerStateManagerState extends State key: const ValueKey('VisibilityDetector'), onVisibilityChanged: (VisibilityInfo info) { if (info.visibleFraction == 0.0) { - widget.stopCamera.call(); + widget.onStop.call(); } else { - widget.restartCamera.call(); + widget.onResume.call(); } }, child: widget.child,