Flutter - 原生交互 - 相机Camera - 曝光,缩放,录制视频

曝光

Flutter上CupertinoSlider组件的样式是iOS上的Slider,使用该组件控制曝光量, Camera插件提供的API是CameraController

dart 复制代码
Future<double> setExposureOffset(double offset) async {
...
}

最后调用iOS端的系统方法控制曝光值

objc 复制代码
- (void)setExposureTargetBias:(float)bias completionHandler:(nullable void (^)(CMTime syncTime))handler API_AVAILABLE(ios(8.0), macCatalyst(14.0), tvos(17.0)) API_UNAVAILABLE(macos, visionos);
dart 复制代码
class TakePictureScreenState extends State<TakePictureScreen> {
    /// 设置默认值
    double currentExposure = 0.0;
    /// 是否显示曝光Slider组件
    bool _showedExposure = false;
    ...
    /// 使用CupertinoSlider
    
    Widget showExposure() {
        if (_showedExposure) {
            return SizedBox(
                height: 44,
                width: MediaQuery.of(context).size.width,
                child: CupertinoSlider(
                    /// 滑动Slider时触发的事件
                    onChanged: (value) {
                        setState(() {
                            /// 调整相机的曝光值
                            _controller.setExposureOffset(value);
                            currentExposure = value;
                        });
                    },
                    min: -3, /// 设置作用范围
                    max: 3,
                    value: currentExposure, /// 当前Slider显示的值
                ),
            );
        }
    return SizedBox.shrink();
  }
}

两指手势缩放

系统的相机可以双指进行缩放操作,在Flutter中可以在GestureDetector来实现

dart 复制代码
/// 最小缩放比例
double _minAvailableZoom = 1.0;
/// 最大缩放比例
double _maxAvailableZoom = 1.0;
/// 记录当前的缩放比例
double _currentScale = 1.0;
/// 当前的基础值
double _baseScale = 1.0;

Listener(
    onPointerDown: (_) => _pointers++,
    onPointerUp: (_) => _pointers--, // 用来判断是否双指
    child: CameraPreview(
            _controller,
            // Creates a widget that defers its building until layout.
            // 布局完成再创建
            child: LayoutBuilder(
                builder: (BuildContext context, BoxConstraints constraints) {
                  return GestureDetector(
                            // Opaque targets can be hit by hit tests, causing them to both receive events within their bounds and prevent targets visually behind them from also receiving events.
                            // 相机Widget能收到手势
                            behavior: HitTestBehavior.opaque, 
                            // 设置开始缩放事件
                            onScaleStart: _handleScaleStart,
                            // 设置缩放值变化事件
                            onScaleUpdate: _handleScaleUpdate,
                            onTapDown:
                            (TapDownDetails details) =>
                                onViewFinderTap(details, constraints),
      );
    },
  ),
),
);

/// 开始缩放时用_baseScale记录当前的缩放值
void _handleScaleStart(ScaleStartDetails details) {
    _baseScale = _currentScale;
}

Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async {
    // 判断是否两指操作
    if (_pointers != 2) {
      return;
    }

    // 当前值 = 基础值 * 缩放的值,clamp方法限制缩放值的范围
    _currentScale = (_baseScale * details.scale).clamp(
        _minAvailableZoom,
        _maxAvailableZoom,
    );
    
    // 设置缩放
    await _controller.setZoomLevel(_currentScale);
}

录制视频

使用Camera组件中的cameraController.startVideoRecording()方法来开始拍摄视频,然后用cameraController.stopVideoRecording()方法可以结束视频。

dart 复制代码
onPressed: () async {
    await _initializeControllerFuture;
    /// 当前在拍照
    if (_isPhotoMode) {
        ...
    } else {
        // 通过camera组件提供的isRecordingVideo字段确认
        // 当前在拍摄视频
        if (_controller.value.isRecordingVideo) {
            onStopButtonPressed();
        } else {
            /// 开始拍摄
            onVideoRecordButtonPressed();
        }
    }
}

录制过程中显示时间

dart 复制代码
class TakePictureScreenState extends State<TakePictureScreen>
    with WidgetsBindingObserver {
    ...
Timer? _timer; // 计时器
int _totalSeconds = 0; // 总秒数

/// 格式化时间为 00:00
String _formatTime() {
    final minutes = (_totalSeconds ~/ 60).toString().padLeft(2, '0');
    final seconds = (_totalSeconds % 60).toString().padLeft(2, '0');
    return '$minutes:$seconds';
}

/// 定时器执行方法
void _startTimer() {
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
        setState(() {
            _totalSeconds++;
       });
    });
}

/// dispose方法中要释放timer
@override
void dispose() {
    _controller.dispose();
    _timer?.cancel();
    super.dispose();
}

/// 定时器组件
Widget timingWidget() {
    return Align(
      alignment: const Alignment(0.9, 0),
      child: Padding(
        padding: EdgeInsets.fromLTRB(0, 20, 10, 20),
        child: Text(
          _formatTime(),
          style: TextStyle(
            color: Colors.white,
            fontSize: 20,
            decoration: TextDecoration.none,
          ),
        ),
      ),
    );
  }
}

点击录制事件

dart 复制代码
void onVideoRecordButtonPressed() {
    startVideoRecording().then((_) {
      /// 异步调用完成后重新刷新页面,比如按钮变化了
      if (mounted) {
        _startTimer(); // 启动定时器
        setState(() {});
      }
    });
}

Future<void> startVideoRecording() async {
    final CameraController cameraController = _controller;

    if (!cameraController.value.isInitialized) {
      return;
    }

    if (cameraController.value.isRecordingVideo) {
      return;
    }

    try {
      await cameraController.startVideoRecording();
    } on CameraException catch (e) {
      debugPrint(e.toString());
      return;
    }
  }

结束录制

dart 复制代码
void onStopButtonPressed() {
    stopVideoRecording().then((XFile? file) {
      if (mounted) {
        // 停止计时器 && 重置计数值
        if (_timer?.isActive == true) {
          _timer?.cancel();
          _totalSeconds = 0;
        }
        setState(() {});
      }
      /// 获取拍摄的视频文件
      if (file != null) {
        /// 设置视频文件显示预览效果
        videoFile = file;
        _startVideoPlayer();
      }
    });
}
  
Future<XFile?> stopVideoRecording() async {
    final CameraController cameraController = _controller;

    if (!cameraController.value.isRecordingVideo) {
      return null;
    }

    try {
      return cameraController.stopVideoRecording();
    } on CameraException catch (e) {
      debugPrint(e.toString());
      return null;
    }
}

查看拍摄视频

添加video_player组件用于预览视频

sh 复制代码
¥ flutter pub add video_player
¥ flutter pub get

录制结束后调用_startVideoPlayer方法

dart 复制代码
Future<void> _startVideoPlayer() async {
    if (videoFile == null) {
      return;
    }
    
    /// Web则用网络加载
    final VideoPlayerController vController =
        kIsWeb
            ? VideoPlayerController.networkUrl(Uri.parse(videoFile!.path))
            : VideoPlayerController.file(File(videoFile!.path));

    /// 创建监听器刷新页面
    videoPlayerListener = () {
      if (videoController != null) {
        if (mounted) {
          setState(() {});
        }
        videoController!.removeListener(videoPlayerListener!);
      }
    };
    
    vController.addListener(videoPlayerListener!);
    /// 是否循环播放
    await vController.setLooping(true);
    await vController.initialize();
    /// 清除原来的
    await videoController?.dispose();
    if (mounted) {
      setState(() {
        imageFile = null;
        videoController = vController;
      });
    }
    /// 播放
    await vController.play();
}

预览图组件

dart 复制代码
Widget _thumbnailWidget() {
    final VideoPlayerController? localVideoController = videoController;
    bool isNoThumbnail = localVideoController == null && imageFile == null;
    return Align(
      alignment: Alignment(-0.8, 0),
      child: SizedBox(
        width: 64,
        height: 64,
        child:
            isNoThumbnail
                ? Container()
                : GestureDetector(
                ...
                child:
                      /// 当前是拍照的场景
                      (localVideoController == null)
                          ? (
                          kIsWeb
                              ? Image.network(imageFile!.path)
                              : Image.file(File(imageFile!.path)))
                          : AspectRatio(
                            /// 当前是视频的场景使用video_player插件提供的组件进行播放
                            aspectRatio: localVideoController.value.aspectRatio,
                            child: VideoPlayer(localVideoController),
                          ),
                )

因为是点击的是同一个按钮进行拍照和视频,一些元素的控制

dart 复制代码
/// 点击Video时清空imageFile

GestureDetector(
  child: Container(
    margin: EdgeInsets.fromLTRB(0, 8, 8, 0),
    child: Text(
      "VIDEO",
      style: TextStyle(
        color: _isPhotoMode ? Colors.white : Colors.orangeAccent,
        fontSize: 16,
        decoration: TextDecoration.none,
      ),
    ),
  ),
  onTap:
      () => {
        setState(() {
          _isPhotoMode = false;
          imageFile = null;
        }),
      },
),

/// 拍照时也清空视频相关的属性
if (_isPhotoMode) {
    final image = await _controller.takePicture();

    if (!context.mounted) return;
    setState(() {
        imageFile = image;
        videoController?.dispose();
        videoController = null;
    });
} else {
    // 点击开始/停止录制视频
    ...
}

问题

锁定相机方向,避免手机横屏时相机视图变化

使用CameraController对象的lockCaptureOrientation方法可以锁定相机的方向

dart 复制代码
...
if (snapshot.connectionState == ConnectionState.done) {
  // 相机初始化完成时,锁定相机的方向
  _controller.lockCaptureOrientation(
    DeviceOrientation.portraitUp,
  );
  return CameraPreview(_controller);
}

参考

  1. Fixing Stretched Camera Preview on Flutter Rotation
  2. Camera
  3. How to set Flutter CameraPreview Size "Fullscreen"
相关推荐
恋猫de小郭15 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
明君8799719 小时前
Flutter 如何给图片添加多行文字水印
前端·flutter
ssshooter1 天前
Tauri 踩坑 appLink 修改后闪退
前端·ios·rust
四眼肥鱼1 天前
flutter 利用flutter_libserialport 实现SQ800 串口通信
前端·flutter
二流小码农1 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos
火柴就是我2 天前
让我们实现一个更好看的内部阴影按钮
android·flutter
王晓枫2 天前
flutter接入三方库运行报错:Error running pod install
前端·flutter
开心就好20252 天前
UniApp开发应用多平台上架全流程:H5小程序iOS和Android
后端·ios
开心就好20252 天前
免 Xcode 的 iOS 开发新选择?聊聊一款更轻量的 iOS 开发 IDE kxapp 快蝎
后端·ios
恋猫de小郭2 天前
Apple 的 ANE 被挖掘,AI 硬件公开,宣传的 38 TOPS 居然是"数字游戏"?
前端·人工智能·ios