import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import 'package:wechat_camera_picker/wechat_camera_picker.dart'; import 'package:video_player/video_player.dart'; class VideoInputView extends StatefulWidget { final bool isVisible; final Function(String filePath, int duration)? onVideoRecorded; const VideoInputView({ required this.isVisible, this.onVideoRecorded, super.key, }); @override State createState() => _VideoInputViewState(); } class _VideoInputViewState extends State { Timer? _timer; int _seconds = 0; bool _isRecording = false; bool _isCanceling = false; Offset _panStartPosition = Offset.zero; @override void dispose() { _timer?.cancel(); super.dispose(); } // 格式化时间显示 String _formatTime(int seconds) { final minutes = seconds ~/ 60; final secs = seconds % 60; return '$minutes:${secs.toString().padLeft(2, '0')}'; } // 开始录制视频 Future _startRecording() async { // 请求相机和麦克风权限 final cameraStatus = await Permission.camera.request(); final micStatus = await Permission.microphone.request(); if (!cameraStatus.isGranted || !micStatus.isGranted) { SmartDialog.showToast('需要相机和麦克风权限才能录制视频'); return; } setState(() { _isRecording = true; _seconds = 0; _isCanceling = false; _panStartPosition = Offset.zero; }); // 启动计时器 _timer = Timer.periodic(const Duration(seconds: 1), (timer) { setState(() { _seconds++; }); }); } // 停止录制视频 Future _stopRecording({bool cancel = false}) async { _timer?.cancel(); _timer = null; if (_isRecording) { final finalSeconds = _seconds; setState(() { _isRecording = false; _seconds = 0; _isCanceling = false; _panStartPosition = Offset.zero; }); // 如果不是取消,打开相机录制视频 if (!cancel && finalSeconds > 0) { _openCameraForVideo(); } } } // 打开相机录制视频 Future _openCameraForVideo() async { try { print('🎬 [VideoInputView] 打开相机录制视频'); AssetEntity? entity = await CameraPicker.pickFromCamera( Get.context!, pickerConfig: const CameraPickerConfig( enableRecording: true, onlyEnableRecording: true, ), ); print('🎬 [VideoInputView] 录制结果: ${entity != null ? "成功" : "取消"}'); if (entity != null) { print('资源类型: ${entity.type}'); print('资源时长: ${entity.duration} 秒'); final file = await entity.file; if (file != null) { print('✅ [VideoInputView] 视频文件获取成功'); print('文件路径: ${file.path}'); // 获取视频时长 final duration = await _getVideoDuration(file.path); print('视频时长: $duration 秒'); widget.onVideoRecorded?.call(file.path, duration); } } } catch (e) { print('❌ [VideoInputView] 录制视频失败: $e'); if (Get.isLogEnable) { Get.log("录制视频失败: $e"); } } } // 从相册选择视频 Future _pickVideoFromGallery() async { try { print('🎬 [VideoInputView] 开始选择视频'); List? result = await AssetPicker.pickAssets( Get.context!, pickerConfig: const AssetPickerConfig( maxAssets: 1, requestType: RequestType.video, specialPickerType: SpecialPickerType.noPreview, ), ); print('🎬 [VideoInputView] 选择结果: ${result?.length ?? 0} 个文件'); if (result != null && result.isNotEmpty) { final asset = result.first; print('资源类型: ${asset.type}'); print('资源时长: ${asset.duration} 秒'); final file = await asset.file; if (file != null) { print('✅ [VideoInputView] 视频文件获取成功'); print('文件路径: ${file.path}'); print('文件大小: ${file.lengthSync()} 字节'); // 获取视频时长(秒) final duration = asset.duration; print('准备回调,时长: $duration 秒'); widget.onVideoRecorded?.call(file.path, duration); } else { print('❌ [VideoInputView] 文件为空'); } } else { print('⚠️ [VideoInputView] 用户取消选择或未选择任何文件'); } } catch (e) { print('❌ [VideoInputView] 选择视频失败: $e'); if (Get.isLogEnable) { Get.log("选择视频失败: $e"); } SmartDialog.showToast('❌ 选择视频失败: $e'); } } // 获取视频时长 Future _getVideoDuration(String filePath) async { try { final controller = VideoPlayerController.file(File(filePath)); await controller.initialize(); final duration = controller.value.duration.inSeconds; await controller.dispose(); return duration; } catch (e) { print('获取视频时长失败: $e'); return 0; } } // 处理长按开始 void _onLongPressStart(LongPressStartDetails details) { _panStartPosition = details.globalPosition; _startRecording(); } // 处理长按结束 void _onLongPressEnd() { if (_isCanceling) { _stopRecording(cancel: true); } else { _stopRecording(cancel: false); } } // 生成视频波形(模拟),计时器嵌入在中间 Widget _buildWaveformWithTimer() { const int leftBars = 15; const int rightBars = 15; const int purpleBarsPerSide = 2; return Container( height: 30.h, width: double.infinity, child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ // 左侧波形 ...List.generate(leftBars, (index) { final isNearTimer = index >= leftBars - purpleBarsPerSide; final isPurple = _isRecording && isNearTimer; if (!_isRecording) { return Container( width: 2.5.w, height: 6.h, margin: EdgeInsets.symmetric(horizontal: 0.8.w), decoration: BoxDecoration( color: Colors.grey.withOpacity(0.3), borderRadius: BorderRadius.circular(1.25.w), ), ); } final random = (index * 7 + _seconds * 3) % 10; final isActive = isPurple || random > 5; final baseHeight = isActive ? (isPurple ? 12 + random : 8 + random) : 6; final height = (baseHeight.clamp(6, 20)).h; final color = isPurple ? const Color(0xFF8359FF) : isActive ? Colors.grey.withOpacity(0.5) : Colors.grey.withOpacity(0.3); return AnimatedContainer( duration: const Duration(milliseconds: 100), curve: Curves.easeOut, width: 2.5.w, height: height, margin: EdgeInsets.symmetric(horizontal: 0.8.w), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(1.25.w), ), ); }), // 计时器 Padding( padding: EdgeInsets.symmetric(horizontal: 8.w), child: Text( _isRecording ? _formatTime(_seconds) : '0:00', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w500, color: _isRecording ? Colors.black87 : Colors.grey, ), ), ), // 右侧波形 ...List.generate(rightBars, (index) { final isNearTimer = index < purpleBarsPerSide; final isPurple = _isRecording && isNearTimer; if (!_isRecording) { return Container( width: 2.5.w, height: 6.h, margin: EdgeInsets.symmetric(horizontal: 0.8.w), decoration: BoxDecoration( color: Colors.grey.withOpacity(0.3), borderRadius: BorderRadius.circular(1.25.w), ), ); } final random = (index * 7 + _seconds * 3) % 10; final isActive = isPurple || random > 5; final baseHeight = isActive ? (isPurple ? 12 + random : 8 + random) : 6; final height = (baseHeight.clamp(6, 20)).h; final color = isPurple ? const Color(0xFF8359FF) : isActive ? Colors.grey.withOpacity(0.5) : Colors.grey.withOpacity(0.3); return AnimatedContainer( duration: const Duration(milliseconds: 100), curve: Curves.easeOut, width: 2.5.w, height: height, margin: EdgeInsets.symmetric(horizontal: 0.8.w), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(1.25.w), ), ); }), ], ), ); } @override Widget build(BuildContext context) { return AnimatedContainer( duration: const Duration(milliseconds: 300), height: widget.isVisible ? 180.h : 0, color: Colors.white, child: widget.isVisible ? Container( width: 1.sw, color: Colors.white, padding: EdgeInsets.symmetric(horizontal: 16.w), child: Stack( alignment: Alignment.center, children: [ // 顶部:视频波形(计时器嵌入在中间)- 只在录音且未取消时显示 if (_isRecording && !_isCanceling) Positioned( top: 0, left: -16.w, right: -16.w, child: _buildWaveformWithTimer(), ), // 取消按钮(上滑时显示在视频按钮上方居中) if (_isCanceling) Positioned( top: 0, left: 0, right: 0, child: Center( child: Container( width: 38.w, height: 38.w, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.red.shade400, boxShadow: [ BoxShadow( color: Colors.red.withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: Center( child: Icon( Icons.close_rounded, color: Colors.white, size: 20.w, ), ), ), ), ), // 中间:左右两个按钮 Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox(height: 16.h), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // 左侧:相册按钮 GestureDetector( onTap: _pickVideoFromGallery, child: Column( children: [ Container( width: 70.w, height: 70.w, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( colors: const [ Color(0xFF8359FF), Color(0xFF3D8AE0), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( color: const Color(0xFF8359FF) .withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Center( child: Icon( Icons.photo_library_rounded, color: Colors.white, size: 40.w, ), ), ), SizedBox(height: 8.h), Text( '相册', style: TextStyle( fontSize: 14.sp, color: Colors.black.withOpacity(0.6), ), ), ], ), ), // 右侧:拍摄按钮(长按录制) Listener( onPointerDown: (details) { _panStartPosition = details.position; }, onPointerMove: (details) { if (_isRecording) { if (_panStartPosition == Offset.zero) { _panStartPosition = details.position; return; } final deltaY = _panStartPosition.dy - details.position.dy; final shouldCancel = deltaY > 60; if (_isCanceling != shouldCancel) { setState(() { _isCanceling = shouldCancel; }); } } }, onPointerUp: (_) { if (_isRecording) { _onLongPressEnd(); } }, child: GestureDetector( onLongPressStart: _onLongPressStart, onLongPressEnd: (_) => _onLongPressEnd(), behavior: HitTestBehavior.opaque, child: Column( children: [ Stack( alignment: Alignment.center, children: [ // 外圈边框(长按时显示,取消时隐藏) if (_isRecording && !_isCanceling) Container( width: 80.w, height: 80.w, decoration: BoxDecoration( shape: BoxShape.circle, color: Color(0xFF8359FF) .withOpacity(0.3), ), ), // 主按钮 Container( width: _isRecording ? 70.w : 80.w, height: _isRecording ? 70.w : 80.w, decoration: BoxDecoration( shape: BoxShape.circle, gradient: _isCanceling ? LinearGradient( colors: [ Colors.grey.shade300, Colors.grey.shade400, ], begin: Alignment.topLeft, end: Alignment.bottomRight, ) : LinearGradient( colors: const [ Color(0xFF8359FF), Color(0xFF3D8AE0), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), boxShadow: [ BoxShadow( color: _isCanceling ? Colors.grey.withOpacity(0.3) : const Color(0xFF8359FF) .withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Center( child: Icon( Icons.videocam_rounded, color: Colors.white, size: 50.w, ), ), ), ], ), SizedBox(height: 8.h), Text( _isCanceling ? '松开取消' : _isRecording ? '松开发送,上滑取消' : '长按拍摄', style: TextStyle( fontSize: 14.sp, color: _isCanceling ? Colors.red : Colors.black.withOpacity(0.6), ), ), ], ), ), ), ], ), ], ), ], ), ) : null, ); } }