From 12c7d5935dec32ce39b4599de366d71729595508 Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Mon, 26 Oct 2026 19:04:08 +0000 Subject: [PATCH] Fade in the camera preview when startup completes Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Signed-off-by: kenny lopez --- .../pairing_qr_scanner/scanner_camera.dart | 107 ++++++++++++------ .../pairing/pairing_qr_scanner_test.dart | 19 ++++ 2 files changed, 89 insertions(+), 37 deletions(-) diff --git a/mobile/lib/features/pairing/pairing_qr_scanner/scanner_camera.dart b/mobile/lib/features/pairing/pairing_qr_scanner/scanner_camera.dart index a5f85a1f15..a1ecbd4170 100644 --- a/mobile/lib/features/pairing/pairing_qr_scanner/scanner_camera.dart +++ b/mobile/lib/features/pairing/pairing_qr_scanner/scanner_camera.dart @@ -1,6 +1,6 @@ part of '../pairing_qr_scanner.dart'; -class _QrScannerCamera extends StatelessWidget { +class _QrScannerCamera extends HookWidget { const _QrScannerCamera({required this.controller, required this.onDetect}); final MobileScannerController controller; @@ -8,46 +8,79 @@ class _QrScannerCamera extends StatelessWidget { @override Widget build(BuildContext context) { - return MobileScanner( - controller: controller, - fit: BoxFit.cover, - errorBuilder: (context, error) { - final message = switch (error.errorCode) { - MobileScannerErrorCode.permissionDenied => - 'Camera permission is required to scan QR codes.\n\n' - 'Please grant camera access in your device settings.', - _ => - 'Could not start camera: ' - "${error.errorDetails?.message ?? 'unknown error'}", - }; - return ColoredBox( - color: Colors.black, - child: Center( - child: Padding( - padding: const EdgeInsets.all(Grid.sm), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - BuzzIcons.cameraOff, - size: 48, - color: Colors.white.withValues(alpha: 0.72), - ), - const SizedBox(height: Grid.xs), - Text( - message, - textAlign: TextAlign.center, - style: context.textTheme.bodyMedium?.copyWith( + final cameraState = useState(controller.value); + useEffect(() { + var active = true; + void publish() { + if (active) cameraState.value = controller.value; + } + + void changed() { + // The scanner can publish initialization state while its child builds. + if (WidgetsBinding.instance.schedulerPhase == + SchedulerPhase.persistentCallbacks) { + WidgetsBinding.instance.addPostFrameCallback((_) => publish()); + } else { + publish(); + } + } + + controller.addListener(changed); + return () { + active = false; + controller.removeListener(changed); + }; + }, [controller]); + final state = cameraState.value; + final ready = state.isInitialized && state.isRunning; + return AnimatedOpacity( + key: const ValueKey('qr-camera-ready-fade'), + opacity: ready || state.error != null ? 1 : 0, + duration: MediaQuery.disableAnimationsOf(context) || state.error != null + ? Duration.zero + : const Duration(milliseconds: 120), + curve: Curves.easeOut, + child: MobileScanner( + controller: controller, + fit: BoxFit.cover, + errorBuilder: (context, error) { + final message = switch (error.errorCode) { + MobileScannerErrorCode.permissionDenied => + 'Camera permission is required to scan QR codes.\n\n' + 'Please grant camera access in your device settings.', + _ => + 'Could not start camera: ' + "${error.errorDetails?.message ?? 'unknown error'}", + }; + return ColoredBox( + color: Colors.black, + child: Center( + child: Padding( + padding: const EdgeInsets.all(Grid.sm), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + BuzzIcons.cameraOff, + size: 48, color: Colors.white.withValues(alpha: 0.72), ), - ), - ], + const SizedBox(height: Grid.xs), + Text( + message, + textAlign: TextAlign.center, + style: context.textTheme.bodyMedium?.copyWith( + color: Colors.white.withValues(alpha: 0.72), + ), + ), + ], + ), ), ), - ), - ); - }, - onDetect: onDetect, + ); + }, + onDetect: onDetect, + ), ); } } diff --git a/mobile/test/features/pairing/pairing_qr_scanner_test.dart b/mobile/test/features/pairing/pairing_qr_scanner_test.dart index e3dd5a7cd2..2aa20649a5 100644 --- a/mobile/test/features/pairing/pairing_qr_scanner_test.dart +++ b/mobile/test/features/pairing/pairing_qr_scanner_test.dart @@ -138,6 +138,8 @@ void main() { await tester.binding.setSurfaceSize(const Size(393, 852)); addTearDown(() => tester.binding.setSurfaceSize(null)); + final cameraStart = Completer(); + fakeScannerPlatform.startGate = cameraStart.future; var scannerClosed = false; await tester.pumpWidget( MaterialApp( @@ -212,6 +214,21 @@ void main() { expect(openingRadius, inExclusiveRange(18, 40)); await tester.pumpAndSettle(); + final readyFade = find.byKey(const ValueKey('qr-camera-ready-fade')); + final previewFade = find + .descendant(of: readyFade, matching: find.byType(FadeTransition)) + .first; + expect(tester.widget(previewFade).opacity.value, 0); + cameraStart.complete(); + await tester.pump(); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 60)); + expect( + tester.widget(previewFade).opacity.value, + inExclusiveRange(0, 1), + ); + await tester.pumpAndSettle(); + expect(tester.widget(previewFade).opacity.value, 1); expect( tester.widget(portal).borderRadius, BorderRadius.circular(40), @@ -466,6 +483,7 @@ class _FakeMobileScannerPlatform extends MobileScannerPlatform { var _isDisposed = false; var stopCalls = 0; var startCalls = 0; + Future? startGate; @override Stream get barcodesStream => _barcodes.stream; @@ -480,6 +498,7 @@ class _FakeMobileScannerPlatform extends MobileScannerPlatform { @override Future start(StartOptions startOptions) async { startCalls++; + await startGate; return const MobileScannerViewAttributes( cameraDirection: CameraFacing.back, currentTorchMode: TorchState.unavailable,