Fade in the camera preview when startup completes

Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-10-26 19:04:08 +00:00
parent fe860a646f
commit 12c7d5935d
2 changed files with 89 additions and 37 deletions

View File

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

View File

@@ -138,6 +138,8 @@ void main() {
await tester.binding.setSurfaceSize(const Size(393, 852));
addTearDown(() => tester.binding.setSurfaceSize(null));
final cameraStart = Completer<void>();
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<FadeTransition>(previewFade).opacity.value, 0);
cameraStart.complete();
await tester.pump();
await tester.pump();
await tester.pump(const Duration(milliseconds: 60));
expect(
tester.widget<FadeTransition>(previewFade).opacity.value,
inExclusiveRange(0, 1),
);
await tester.pumpAndSettle();
expect(tester.widget<FadeTransition>(previewFade).opacity.value, 1);
expect(
tester.widget<ClipRSuperellipse>(portal).borderRadius,
BorderRadius.circular(40),
@@ -466,6 +483,7 @@ class _FakeMobileScannerPlatform extends MobileScannerPlatform {
var _isDisposed = false;
var stopCalls = 0;
var startCalls = 0;
Future<void>? startGate;
@override
Stream<BarcodeCapture?> get barcodesStream => _barcodes.stream;
@@ -480,6 +498,7 @@ class _FakeMobileScannerPlatform extends MobileScannerPlatform {
@override
Future<MobileScannerViewAttributes> start(StartOptions startOptions) async {
startCalls++;
await startGate;
return const MobileScannerViewAttributes(
cameraDirection: CameraFacing.back,
currentTorchMode: TorchState.unavailable,