diff --git a/assets/images/history_mini.png b/assets/images/history_mini.png new file mode 100644 index 0000000..0a3def2 Binary files /dev/null and b/assets/images/history_mini.png differ diff --git a/assets/images/keyboard_mini.png b/assets/images/keyboard_mini.png new file mode 100644 index 0000000..6bf8630 Binary files /dev/null and b/assets/images/keyboard_mini.png differ diff --git a/lib/widgets/chat_footer.dart b/lib/widgets/chat_footer.dart index a515eda..d4d67d0 100644 --- a/lib/widgets/chat_footer.dart +++ b/lib/widgets/chat_footer.dart @@ -4,34 +4,76 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../services/message_service.dart'; import '../utils/common_util.dart'; -import 'voice_animation.dart'; +import 'large_audio_wave.dart'; -class ChatFooter extends StatelessWidget { +class ChatFooter extends StatefulWidget { final VoidCallback? onClear; const ChatFooter({super.key, this.onClear}); + @override + State createState() => _ChatFooterState(); +} + +class _ChatFooterState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 1000), + vsync: this, + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override Widget build(BuildContext context) { + // 定义颜色常量 + const Color buttonColor = Color(0xFF6F72F1); // 主题紫色(应用栏和按钮边框) + return Container( color: Colors.transparent, child: Row( children: [ Padding( - padding: const EdgeInsets.only(left: 12, bottom: 12), - child: IconButton( - icon: const Icon(Icons.delete_outline, - color: CommonUtil.commonColor), - iconSize: 30, - tooltip: '清除会话', - onPressed: onClear, + padding: const EdgeInsets.only(left: 16, bottom: 12), + child: GestureDetector( + onTap: widget.onClear, + child: Image.asset( + 'assets/images/history_mini.png', + width: 24, + height: 24, + ), ), ), Expanded( child: Padding( - padding: const EdgeInsets.only(bottom: 12), + key: const Key('startBtn'), + padding: const EdgeInsets.only(bottom: 12, left: 16, right: 16), child: Consumer( builder: (context, messageService, child) { + // 立即响应录音状态变化,无延迟启动动画 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (messageService.isRecording) { + if (!_animationController.isAnimating) { + _animationController.repeat(); + } + } else { + if (_animationController.isAnimating) { + _animationController.stop(); + } + } + }); + return GestureDetector( onTap: () { HapticFeedback.heavyImpact(); @@ -39,6 +81,8 @@ class ChatFooter extends StatelessWidget { }, onLongPress: () { HapticFeedback.mediumImpact(); + // 立即启动动画,无延迟 + _animationController.repeat(); Future.delayed(const Duration(milliseconds: 100), () { HapticFeedback.heavyImpact(); }); @@ -46,30 +90,42 @@ class ChatFooter extends StatelessWidget { }, onLongPressUp: () { HapticFeedback.lightImpact(); + // 立即停止动画 + _animationController.stop(); messageService.stopAndProcessVoiceInput(); }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 150), - height: 48, - width: double.infinity, - alignment: Alignment.center, - decoration: BoxDecoration( - color: messageService.isRecording - ? Color(0xFF8E24AA).withValues(alpha: 0.42) - : Colors.white.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(12), - ), - child: messageService.isRecording - ? const VoiceWaveAnimation() - : Text( - Intl.getCurrentLocale().startsWith('zh') - ? '按 住 说 话' - : 'Hold to Speak', - style: TextStyle( - color: CommonUtil.commonColor, - fontSize: 18, - fontWeight: FontWeight.bold), + child: LayoutBuilder( + builder: (context, constraints) { + final containerWidth = constraints.maxWidth; + final waveWidth = containerWidth * 0.57; // 波形宽度为容器的57% + + return Container( + height: 48, // 固定高度 48px + alignment: Alignment.center, + decoration: BoxDecoration( + color: const Color(0x14FFFFFF), // 白色背景透明度8% + borderRadius: BorderRadius.circular(8), // 圆角按钮 8px + border: Border.all( + color: buttonColor, // 边框颜色 #6F72F1 + width: 0.5, // 边框粗细 0.5px ), + ), + child: messageService.isRecording + ? AudioWaveLarge( + animationController: _animationController, + totalWidth: waveWidth, + ) + : Text( + Intl.getCurrentLocale().startsWith('zh') + ? '按 住 说 话' + : 'Hold to Speak', + style: TextStyle( + color: CommonUtil.commonColor, + fontSize: 18, + fontWeight: FontWeight.bold), + ), + ); + }, ), ); }, @@ -77,11 +133,14 @@ class ChatFooter extends StatelessWidget { ), ), Padding( - padding: const EdgeInsets.only(bottom: 12, right: 12), - child: IconButton( - icon: const Icon(Icons.keyboard, color: CommonUtil.commonColor), - iconSize: 30, - onPressed: () {}, + padding: const EdgeInsets.only(bottom: 12, right: 16), + child: GestureDetector( + onTap: () {}, + child: Image.asset( + 'assets/images/keyboard_mini.png', + width: 24, + height: 24, + ), ), ), ], diff --git a/lib/widgets/large_audio_wave.dart b/lib/widgets/large_audio_wave.dart new file mode 100644 index 0000000..115d680 --- /dev/null +++ b/lib/widgets/large_audio_wave.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +/// 大型音频波形动画组件 +/// +/// 用于显示适合主按钮的波形动画,竖线数量根据总宽度自动计算 +class AudioWaveLarge extends StatelessWidget { + /// 动画控制器,用于驱动波形动画 + final AnimationController animationController; + + /// 波形颜色,默认为白色 + final Color waveColor; + + /// 波形条间距,默认为4.0px + final double barSpacing; + + /// 总宽度,默认为148.0px + final double totalWidth; + + /// 最大高度,默认为22.0px + final double maxHeight; + + /// 最小高度,默认为4.0px + final double minHeight; + + /// 构造函数 + const AudioWaveLarge({ + super.key, + required this.animationController, + this.waveColor = Colors.white, + this.barSpacing = 4.0, + this.totalWidth = 148.0, + this.maxHeight = 22.0, + this.minHeight = 4.0, + }); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + key: const Key('audioWaveAnimation'), // 为音频波形添加key便于引用 + animation: animationController, + builder: (context, child) { + // 根据总宽度动态计算竖线数量 + // 每个竖线宽度2px + 间距,保持合理的密度 + final barWidthWithSpacing = 2 + barSpacing; + final dynamicBarCount = (totalWidth / barWidthWithSpacing).floor(); + final actualBarCount = dynamicBarCount > 0 ? dynamicBarCount : 1; + + return SizedBox( + width: totalWidth, // 设置动态宽度 + height: maxHeight, // 设置固定高度 + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, // 垂直居中 + children: List.generate(actualBarCount, (index) { + // 为每个竖线创建不同的相位,使动画不同步 + final phase = index * 0.3; + + // 设置不同的频率增加自然变化 + final frequency = 1.0 + (index % 3) * 0.2; + + // 计算波形高度 + final height = minHeight + (maxHeight - minHeight) * math.sin( + (animationController.value * 2 * math.pi * frequency) + phase + ).abs(); + + // 构建单个波形条 + return Padding( + padding: EdgeInsets.symmetric(horizontal: barSpacing / 2), // 左右间距 + child: Container( + width: 2, // 波形条宽度2px + height: height, // 高度随动画变化 + decoration: BoxDecoration( + color: waveColor, + borderRadius: BorderRadius.circular(2), // 圆角效果 + ), + ), + ); + }), + ), + ); + }, + ); + } +} diff --git a/lib/widgets/mini_audio_wave.dart b/lib/widgets/mini_audio_wave.dart new file mode 100644 index 0000000..d463a7c --- /dev/null +++ b/lib/widgets/mini_audio_wave.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +/// 小型按钮波形动画组件 +/// +/// 用于显示适合小按钮的波形动画,包含8个竖线,动画效果由外部动画控制器驱动 +class AudioWaveLargeMini extends StatelessWidget { + /// 动画控制器,用于驱动波形动画 + final AnimationController animationController; + + /// 波形颜色,默认为白色 + final Color waveColor; + + /// 构造函数 + const AudioWaveLargeMini({ + super.key, + required this.animationController, + this.waveColor = Colors.white, + }); + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: animationController, // 使用传入的动画控制器 + builder: (context, child) { + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: List.generate(8, (index) { // 生成8个波形条 + // 为每个竖线设置不同的相位,使动画更自然 + final phase = index * 0.4; + // 设置不同的频率变化 + final frequency = 1.0 + (index % 3) * 0.2; + + // 计算波形高度(最小4.0,最大14.0) + final height = 4.0 + 10.0 * math.sin( + (animationController.value * 2 * math.pi * frequency) + phase + ).abs(); + + // 构建单个波形条 + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2.0), // 左右间隔4px + child: Container( + width: 2, // 波形条宽度2px + height: height, // 高度随动画变化 + decoration: BoxDecoration( + color: waveColor, + borderRadius: BorderRadius.circular(2), // 圆角效果 + ), + ), + ); + }), + ); + }, + ); + } +}