You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 19:15:50