Flutter原生相机交互实现曝光缩放与视频录制
时间:2026-08-21 | 作者:云端旅人 | 阅读:0曝光
在Flutter中,CupertinoSlider组件可以呈现iOS上的Slider样式。
我们可以通过它来控制曝光量。用于这个功能的API,由Camera插件提供的CameraController实现。
Future<double> setExposureOffset(double offset) async {
...
}
最后调用iOS端的系统方法控制曝光值。
- (void)setExposureTargetBias:(float)bias completionHandler:(nullable void (^)(CMTime syncTime))handler API_A VAILABLE(ios(8.0), macCatalyst(14.0), tvos(17.0)) API_UNA VAILABLE(macos, visionos);
曝光控制实现
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来实现。
缩放相关变量
/// 最小缩放比例
double _minA vailableZoom = 1.0;
/// 最大缩放比例
double _maxA vailableZoom = 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能收到手势
beha vior: HitTestBeha vior.opaque,
// 设置开始缩放事件
onScaleStart: _handleScaleStart,
// 设置缩放值变化事件
onScaleUpdate: _handleScaleUpdate,
onTapDown:
(TapDownDetails details) =>
onViewFinderTap(details, constraints),
);
},
),
),
);
缩放逻辑
开始缩放时,用_baseScale记录当前的缩放值。
/// 开始缩放时用_baseScale记录当前的缩放值
void _handleScaleStart(ScaleStartDetails details) {
_baseScale = _currentScale;
}
Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async {
// 判断是否两指操作
if (_pointers != 2) {
return;
}
// 当前值 = 基础值 * 缩放的值,clamp方法限制缩放值的范围
_currentScale = (_baseScale * details.scale).clamp(
_minA vailableZoom,
_maxA vailableZoom,
);
// 设置缩放
await _controller.setZoomLevel(_currentScale);
}
录制视频
开始录制可以使用Camera组件中的cameraController.startVideoRecording()方法。
结束录制可以通过cameraController.stopVideoRecording()方法实现。
录制入口判断
onPressed: () async {
await _initializeControllerFuture;
/// 当前在拍照
if (_isPhotoMode) {
...
} else {
// 通过camera组件提供的isRecordingVideo字段确认
// 当前在拍摄视频
if (_controller.value.isRecordingVideo) {
onStopButtonPressed();
} else {
/// 开始拍摄
onVideoRecordButtonPressed();
}
}
}
录制过程中显示时间
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,
),
),
),
);
}
}
开始录制
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;
}
}
结束录制
void onStopButtonPressed() {
stopVideoRecording().then((XFile file) {
if (mounted) {
// 停止计时器 && 重置计数值
if (_timer.isActive == true) {
_timer.cancel();
_totalSeconds = 0;
}
setState(() {});
}
/// 获取拍摄的视频文件
if (file != null) {
/// 设置视频文件显示预览效果
videoFile = file;
_startVideoPlayer();
}
});
}
Future 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组件,用于预览视频。
flutter pub add video_player
flutter pub get
初始化视频播放器
录制结束后,调用_startVideoPlayer方法。
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();
}
预览图组件
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),
),
)
拍照与视频模式切换
因为拍照和视频共用同一个按钮,所以需要控制相关元素状态。
/// 点击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方法,可以锁定相机的方向。
...
if (snapshot.connectionState == ConnectionState.done) {
// 相机初始化完成时,锁定相机的方向
_controller.lockCaptureOrientation(
DeviceOrientation.portraitUp,
);
return CameraPreview(_controller);
}
参考
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。
相关文章
更多-
- Flutter 原生交互实战:Camera 插件集成与核心机制解析
- 时间:2026-08-27
-
- NOMO相机怎么放大取景框设置方法
- 时间:2026-08-18
-
- iniVation DVXplorer事件相机640×480纯事件感知高速视觉应用
- 时间:2026-08-13
-
- 手机文件怎么传到相机存储卡更方便
- 时间:2026-08-12
-
- iniVation事件相机发展历程:从DVS研究到工程化视觉平台
- 时间:2026-08-12
-
- 美妆相机关闭音效的详细操作步骤教程
- 时间:2026-07-27
-
- Premiere模拟相机快门拍照效果教程
- 时间:2026-07-23
-
- AE实景合成3D摄像机跟踪文字特效制作教程
- 时间:2026-07-21
精选合集
更多大家都在玩
大家都在看
更多-
- 蚂蚁新村小课堂今日答案9月23日 火宫殿臭豆腐是哪个地方的非遗美食
- 时间:2026-09-23
-
- 蚂蚁新村2026年9月23日答案最新
- 时间:2026-09-23
-
- 蚂蚁庄园答案2026年9月24日
- 时间:2026-09-23
-
- 蚂蚁庄园今天答题答案2026年9月24日
- 时间:2026-09-23
-
- 蚂蚁庄园今日答案2026年9月24日
- 时间:2026-09-23
-
- 为什么大多数热水瓶的内胆是银色的 蚂蚁庄园今日答案9.24
- 时间:2026-09-23
-
- 小鸡答题今天的答案是什么2026年9月24日
- 时间:2026-09-23
-
- 蚂蚁庄园每日答题答案2026年9月24日
- 时间:2026-09-23