Flutter中Stack嵌套扫码组件与CameraPreview导致预览冻结问题
问题:切换二维码扫描与拍照组件后CameraPreview预览冻结
我一开始想找个能同时支持二维码扫描和普通拍照的单一相机组件,还发现了一篇没得到解答的Stack Overflow帖子,最后决定用Stack布局放两个不同组件,通过控制可见性实现切换。
用mobile_scanner库实现二维码扫描功能,参考Flutter官方文档做的拍照功能。
当前逻辑:初始状态显示CameraPreview组件,打开开关切换为MobileScanner组件。问题是MobileScanner切换可见性时能正常处理相机的暂停与恢复,但CameraPreview做不到——从二维码模式切回拍照模式后,相机预览会冻结。不确定是否需要手动调用_controller.resumePreview(),以及具体的调用时机。
界面代码
class CameraComponent extends StatefulWidget { const CameraComponent({Key? key}) : super(key: key); @override State<CameraComponent> createState() => _CameraComponentState(); } class _CameraComponentState extends State<CameraComponent> { CameraCubit get _cubit => context.read<CameraCubit>(); bool qrModeEnabled = false; CameraController? _controller; late Future<void> _initializeControllerFuture; Photo? _artwork; final ArtworkRepository _artworkRepository = getIt<ArtworkRepository>(); @override void initState() { super.initState(); _cubit.init(); } @override void dispose() { // Dispose of the controller when the widget is disposed. _controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: BlocConsumer<CameraCubit, CameraState>( listener: (BuildContext context, CameraState state) { state.maybeWhen( initCamera: (List<CameraDescription> cameras) { _controller = CameraController( cameras.firstWhere( (CameraDescription camera) => camera.lensDirection == CameraLensDirection.back, orElse: () => cameras.first, ), ResolutionPreset.max, ); _initializeControllerFuture = _controller!.initialize(); }, orElse: () {}, ); }, builder: (BuildContext context, CameraState state) { return state.maybeWhen( initCamera: (_) { return FutureBuilder<void>( future: _initializeControllerFuture, builder: (BuildContext context, AsyncSnapshot<void> snapshot) { if (snapshot.connectionState == ConnectionState.done) { return Stack( children: <Widget>[ Visibility( visible: qrModeEnabled, child: MobileScanner( onDetect: (Barcode barcode, MobileScannerArguments? args) async { if (barcode.rawValue == null) { debugPrint('Failed to scan qr code'); } else { try { final int artworkId = int.parse( barcode.rawValue!.split('_')[1]); debugPrint( 'QR code found! ${barcode.rawValue!}'); _artwork = _artworkRepository.savedArtworks .firstWhereOrNull( (Photo element) => element.id == artworkId, ); _artwork ??= await _artworkRepository .getArtworkDetails(id: artworkId); NavigatorUtils.goToArtworkViewScreen( context, artwork: _artwork!, ); } catch (e) { rethrow; } } }, ), ), Visibility( visible: !qrModeEnabled, child: CameraPreview(_controller!), ), Align( child: Image.asset('assets/icons/b_logo_grey.png'), ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 144), child: Text( 'Scan a QR code or take a photo of the artwork', style: montserratRegular15.copyWith( color: AppColors.white, ), ), ), ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 80), child: GestureDetector( onTap: () async { final XFile image = await _controller!.takePicture(); final Uint8List bytes = await image.readAsBytes(); final String base64Image = base64Encode(bytes); _cubit.addBase64Image(base64Image: base64Image); }, child: Image.asset( 'assets/icons/shutter_icon.png', height: 48, ), ), ), ), Align( alignment: Alignment.topRight, child: Padding( padding: const EdgeInsets.only(top: 10, right: 10), child: Switch( value: qrModeEnabled, onChanged: (bool value) { setState(() { qrModeEnabled = !qrModeEnabled; }); }, activeTrackColor: AppColors.red, inactiveTrackColor: AppColors.white, ), ), ), ], ); } else { return const Center(child: CircularProgressIndicator()); } }, ); }, orElse: () => const SizedBox.shrink(), ); }, ), ); } }
CameraCubit.dart代码
class CameraCubit extends Cubit<CameraState> { CameraCubit({ required ClarifaiRepository clarifaiRepository, }) : _clarifaiRepository = clarifaiRepository, super(const CameraState.initial()); final ClarifaiRepository _clarifaiRepository; Future<void> init() async { try { final List<CameraDescription> cameras = await availableCameras(); if (cameras.isNotEmpty) { emit(CameraState.initCamera(cameras: cameras)); } } catch (e) { emit(CameraState.error(e)); } } Future<void> addBase64Image({required String base64Image}) async { emit(const CameraState.loading()); try { await _clarifaiRepository.addBase64Image(base64Image: base64Image); emit(const CameraState.success()); } catch (e) { emit(CameraState.error(e)); } } }
解决方法
核心问题是两个组件会抢占相机资源:当MobileScanner显示时,它会接管相机硬件,导致CameraController被后台暂停,切回拍照模式时需要手动恢复预览。
1. 切换时手动恢复CameraPreview
修改开关的onChanged回调,在切回拍照模式时调用resumePreview():
Switch( value: qrModeEnabled, onChanged: (bool value) async { setState(() { qrModeEnabled = value; }); // 切回拍照模式时恢复相机预览 if (!value && _controller != null) { await _controller!.resumePreview(); } }, activeTrackColor: AppColors.red, inactiveTrackColor: AppColors.white, ),
2. 避免重复初始化CameraController
当前BlocListener的initCamera状态每次触发都会重新创建CameraController,容易引发资源问题。添加判断,仅在_controller为null时初始化:
listener: (BuildContext context, CameraState state) { state.maybeWhen( initCamera: (List<CameraDescription> cameras) { if (_controller == null) { // 新增判断,避免重复初始化 _controller = CameraController( cameras.firstWhere( (CameraDescription camera) => camera.lensDirection == CameraLensDirection.back, orElse: () => cameras.first, ), ResolutionPreset.max, ); _initializeControllerFuture = _controller!.initialize(); } }, orElse: () {}, ); },
3. 额外优化:处理MobileScanner的相机释放
虽然mobile_scanner通常会在不可见时自动释放相机,但如果仍有冲突,可以尝试通过Key手动控制它的启停,不过一般情况下上面两个步骤就能解决问题。
内容的提问来源于stack exchange,提问作者Stelios Papamichail
相关产品推荐
相关产品推荐

