Flutter中Render 0.1.3+1录制Widget功能异常求助
问题:Flutter摄像头录制时将计时器叠加到视频中
我正在开发一款Flutter摄像头应用,使用camera依赖。核心需求是:在摄像头界面显示计时器,并且将计时器与摄像头画面一同录制到视频里,确保视频在相册查看时仍能看到计时器。
由于camera依赖无法实现该需求,我尝试使用render依赖(版本0.1.3+1),通过录制屏幕上的CameraPreview组件和计时器Widget来达成目标,但当前该功能失效。
当前实现代码
// ignore_for_file: use_build_context_synchronously, unnecessary_null_comparison, avoid_print import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:video_app/providers/countdown_provider.dart'; import 'package:video_app/providers/countup_provider.dart'; import 'package:video_app/providers/tabata_provider.dart'; import 'package:video_app/screens/timer_screen.dart'; import 'package:video_app/screens/video_screen.dart'; import 'package:render/render.dart'; class CameraPage extends StatefulWidget { const CameraPage({super.key, required this.timerType}); final TimerType timerType; @override CameraPageState createState() => CameraPageState(); } enum WidgetState {none, loading, loaded, error} class CameraPageState extends State<CameraPage> { bool _isRecording = false; WidgetState widgetState = WidgetState.none; late CameraController _cameraController; Offset? _focusPoint; bool _isFlashOn = false; late MotionRecorder stopController; final RenderController renderController = RenderController(logLevel: LogLevel.debug); // final renderController = RenderController(); @override void initState() { _initCamera(); super.initState(); } @override void dispose() { _cameraController.dispose(); super.dispose(); } _initCamera() async { widgetState = WidgetState.loading; final cameras = await availableCameras(); final back = cameras.firstWhere((camera) => camera.lensDirection == CameraLensDirection.back); _cameraController = CameraController(back, ResolutionPreset.max); await _cameraController.initialize(); //setState(() => _isLoading = false); if (_cameraController.value.hasError) { widgetState = WidgetState.error; if (mounted) setState(() {}); } else { widgetState = WidgetState.loaded; if (mounted) setState(() {}); } } _recordVideo() async { if (_isRecording == false) { stopController = renderController.recordMotion(); // await _cameraController.prepareForVideoRecording(); // await _cameraController.startVideoRecording(); setState(() => _isRecording = true); } else { RenderResult result = await stopController.stop(); setState(() => _isRecording = false); File refile = result.output; XFile file = XFile(refile.path); // final file = await _cameraController.stopVideoRecording(); // setState(() => _isRecording = false); final route = MaterialPageRoute( fullscreenDialog: true, builder: (_) => VideoPage(file: file), ); Navigator.push(context, route); } } @override Widget build(BuildContext context) { final countdownProvider = Provider.of<CountDownProvider>(context); final countupProvider = Provider.of<CountupProvider>(context); final tabataProvider = Provider.of<TabataProvider>(context); switch (widgetState) { case WidgetState.none: case WidgetState.loading: return buildScaffold(context, const Text('Iniciando camara...')); case WidgetState.loaded: return SafeArea( child: Scaffold( body: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Stack( alignment: Alignment.bottomCenter, children: [ // Positioned.fill( top: 50, child: AspectRatio( aspectRatio: _cameraController.value.aspectRatio, child: GestureDetector( onTapDown: (TapDownDetails details){ final Offset tapPosition = details.localPosition; final Offset relativTapPosition = Offset( tapPosition.dx / constraints.maxWidth, tapPosition.dy / constraints.maxHeight, ); _setFocusPoint(relativTapPosition); }, child: Render( controller: renderController, child: CameraPreview(_cameraController, child: Positioned( top: 0, right: 10, child: Container( color: Colors.transparent, child: Text( widget.timerType != TimerType.tabata ? (widget.timerType == TimerType.countdown ? context.select((CountDownProvider count) => count.timeLeftString) : context.select((CountupProvider count) => count.timeLeftString)) : context.select((TabataProvider count) => count.timeLeftString), style: const TextStyle(fontSize: 50, fontWeight: FontWeight.normal, color: Colors.white),), ), ), ), ), ), ), ); Positioned( bottom: 0, child: Padding( padding: const EdgeInsets.all(25), child: IconButton( icon: Icon(_isRecording ? Icons.stop : Icons.circle, color: _isRecording ? Colors.black : Colors.red), iconSize: 75, onPressed: () { switch (widget.timerType){ case TimerType.countdown: countdownProvider.startStopTimer(); break; case TimerType.countup: countupProvider.startStopTimer(); break; case TimerType.tabata: tabataProvider.startStopTimer(); break; } _recordVideo(); }, ), ), ), ], ); }, ), ), ); case WidgetState.error: return buildScaffold(context, const Text("¡Ooops! Error al cargar la cámara 😩. Reinicia la apliación.")); }} Widget buildScaffold(BuildContext context, Text text) { return Scaffold( appBar: AppBar( centerTitle: true, title: const Text("Cámara"), backgroundColor: Colors.transparent, elevation: 0, ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ if(widgetState == WidgetState.loading) const CircularProgressIndicator(), text ], ), ), ); } }
参考应用
- Google Play的“WeTime”
- Google Play和App Store的“WODProof”
目前未找到其他可行实现方式,诚邀提供解决方案或思路。
内容的提问来源于stack exchange,提问作者Ignacio Lanzzavecchia Cespedes
相关产品推荐
相关产品推荐

