diff --git a/CODE_ANALYSIS.md b/CODE_ANALYSIS.md index 213931c..ee47689 100644 --- a/CODE_ANALYSIS.md +++ b/CODE_ANALYSIS.md @@ -84,7 +84,7 @@ class VehicleCommand { } ``` -## 🔄 核心服务层 (Services) +## 🔄 核心服务层 (Services) - 完整解析 ### 1. MessageService - 核心消息管理服务 @@ -94,6 +94,7 @@ class VehicleCommand { - 统一管理聊天消息 - 协调语音识别、AI对话、车控命令执行流程 - 维护应用状态机 +- 通过Method Channel与原生ASR服务通信 **核心方法分析**: @@ -101,16 +102,16 @@ class VehicleCommand { ```dart // 开始语音输入 Future startVoiceInput() async { - // 1. 权限检查 - // 2. 状态验证 - // 3. 初始化录音 - // 4. 调用原生ASR服务 + // 1. 权限检查 - 使用permission_handler检查麦克风权限 + // 2. 状态验证 - 确保当前状态为idle + // 3. 初始化录音 - 创建用户消息占位符 + // 4. 调用原生ASR服务 - 通过Method Channel启动阿里云ASR } // 停止并处理语音输入 Future stopAndProcessVoiceInput() async { - // 1. 停止录音 - // 2. 等待ASR结果 + // 1. 停止录音 - 调用原生ASR停止接口 + // 2. 等待ASR结果 - 使用Completer等待异步结果 // 3. 调用reply()处理识别结果 } ``` @@ -143,60 +144,213 @@ Future handleVehicleControl(String text, bool isChinese) async { - 通过`_state`维护服务状态机 - 使用`_isReplyAborted`实现流程中断控制 -### 2. CommandService - 车控命令处理服务 +### 2. ChatSseService - SSE流式通信服务 -**设计模式**: 静态方法 + 回调机制 +**设计模式**: 单例服务 + 流式处理 -**职责**: -- 提供车控命令执行接口 -- 维护主应用注册的回调函数 -- 实现命令执行的解耦 +**核心功能**: +- 基于SSE的实时AI对话 +- 流式文本处理和TTS播报 +- 会话状态管理 +- 智能分句和语音合成 +**关键实现**: + +#### SSE连接管理 ```dart -// 命令回调函数定义 -typedef CommandCallback = Future<(bool, Map? params)> Function( - VehicleCommandType type, Map? params); - -// 执行车控命令 -static Future<(bool, Map? params)> executeCommand( - VehicleCommandType type, {Map? params}) async { - // 调用注册的回调函数执行实际车控逻辑 +void request({ + required String messageId, + required String text, + required bool isChinese, + required Function(String, String, bool) onStreamResponse, +}) async { + // 1. 初始化用户ID和会话ID + // 2. 建立HTTP SSE连接 + // 3. 处理流式响应数据 + // 4. 实时文本清理和分句 + // 5. 协调TTS播报 } ``` -### 3. TextClassificationService - 文本分类服务 +#### 智能分句算法 +```dart +// 支持中英文分句符识别 +final enEnders = RegExp(r'[.!?]'); +final zhEnders = RegExp(r'[。!?]'); -**职责**: 对用户输入文本进行意图分类 +// Markdown内容清理 +// 1. 清理图片语法 ![...] +// 2. 移除列表序号 +// 3. 处理不完整语法 +``` + +### 3. AudioRecorderService - 音频录制服务 + +**核心功能**: +- 基于record插件的音频录制 +- OPUS格式音频编码 +- 权限管理 +- 临时文件管理 + +**实现细节**: +```dart +class AudioRecorderService { + final AudioRecorder _recorder = AudioRecorder(); + bool _isRecording = false; + String? _tempFilePath; + + Future startRecording() async { + // 1. 生成临时文件路径 + // 2. 配置OPUS编码格式 + // 3. 启动录音 + } + + Future?> stopRecording() async { + // 1. 停止录音 + // 2. 读取音频文件 + // 3. 返回字节数组 + } +} +``` + +### 4. VoiceRecognitionService - 语音识别服务 + +**功能**: +- HTTP多媒体文件上传 +- 音频格式转换 +- 中英文语言识别 + +**核心逻辑**: +```dart +Future recognizeSpeech(List audioBytes, {String lang = 'cn'}) async { + // 1. 构建MultipartRequest + // 2. 上传音频文件到识别服务 + // 3. 解析JSON响应获取文本 + // 4. 错误处理和日志记录 +} +``` + +### 5. LocalTtsService - 语音合成服务 + +**设计亮点**: 流式TTS处理 + 有序播放队列 + +**核心特性**: +- 基于flutter_tts的跨平台TTS +- 流式文本分句播报 +- 有序任务队列管理 +- 中英文语言自动切换 + +**关键数据结构**: +```dart +class TtsTask { + final int index; // 任务索引 + final String text; // 播报文本 + TtsTaskStatus status; // 任务状态(ready/completed) +} + +enum TtsTaskStatus { + ready, // 准备播放 + completed, // 播放完成 +} +``` + +**流式播放流程**: +```dart +void pushTextForStreamTTS(String text) { + // 1. 创建有序任务 + // 2. 添加到任务队列 + // 3. 触发下一个任务处理 +} + +void _processNextReadyTask() { + // 1. 检查播放状态 + // 2. 按序查找就绪任务 + // 3. 执行TTS播放 + // 4. 等待播放完成回调 +} +``` + +### 6. TextClassificationService - 文本分类服务 + +**功能**: NLP文本意图分类 + +**分类结果**: +- `-1`: 系统错误 +- `2`: 车控命令 +- `4`: 无效问题 +- 其他: 普通问答 ```dart Future classifyText(String text) async { - // HTTP POST请求到分类服务 - // 返回分类结果: - // -1: 错误 - // 2: 车控命令 - // 4: 错误问题 - // 其他: 普通问答 + // HTTP POST到分类服务 + // 返回分类category整数 } ``` -### 4. VehicleCommandService - 车控命令解析服务 +### 7. VehicleCommandService - 车控命令解析服务 **职责**: -- 解析自然语言为车控命令 -- 生成命令执行反馈 +- 自然语言转车控命令 +- 命令参数解析 +- 执行结果反馈生成 +**核心方法**: ```dart Future getCommandFromText(String text) async { - // 1. 发送文本到车控解析服务 + // 1. 发送文本到NLP解析服务 // 2. 解析返回的命令列表 - // 3. 返回VehicleCommandResponse对象 + // 3. 构建VehicleCommandResponse对象 } Future getControlResponse(List successCommandList) async { - // 根据成功执行的命令列表生成友好的反馈文本 + // 根据成功命令列表生成友好反馈文本 } ``` +### 8. CommandService - 车控命令执行服务 + +**设计模式**: 静态方法 + 回调机制 + +**解耦设计**: +```dart +typedef CommandCallback = Future<(bool, Map? params)> Function( + VehicleCommandType type, Map? params); + +static Future<(bool, Map? params)> executeCommand( + VehicleCommandType type, {Map? params}) async { + // 调用主应用注册的回调函数 + // 返回执行结果元组(成功状态, 返回参数) +} +``` + +### 9. RedisService - 缓存服务 + +**功能**: 基于HTTP的Redis缓存操作 + +```dart +class RedisService { + Future setKeyValue(String key, Object value) async { + // HTTP POST设置键值对 + } + + Future getValue(String key) async { + // HTTP GET获取值 + } +} +``` + +### 10. LocationService - 位置服务 + +用于车辆定位相关功能(具体实现需查看完整代码) + +### 11. VehicleStateService - 车辆状态服务 + +**注释代码分析**: +- 原设计基于InGeek MDK车载SDK +- 支持车辆状态实时监听 +- 与Redis服务配合缓存状态数据 +- 当前版本已注释,可能在插件化过程中移除 + ## 🎨 UI层架构分析 ### 1. 主界面 (MainScreen) @@ -286,11 +440,11 @@ _asrChannel.setMethodCallHandler((call) async { **Markdown清理逻辑**: ```dart static String cleanText(String text, bool forTts) { - // 1. 清理粗体/斜体标记 - // 2. 处理代码块 - // 3. 清理表格格式 - // 4. 处理链接和图片 - // 5. 清理列表和引用 + // 1. 清理粗体/斜体标记 **text** *text* + // 2. 处理代码块 ```code``` + // 3. 清理表格格式 |---|---| + // 4. 处理链接和图片 [text](url) ![alt](src) + // 5. 清理列表和引用 1. - * > // 6. 规范化空白字符 } ``` @@ -311,13 +465,30 @@ graph TD H --> I[文本分类] I --> J{分类结果} J -->|车控命令| K[handleVehicleControl] - J -->|普通问答| L[answerQuestion] + J -->|普通问答| L[answerQuestion - ChatSseService] J -->|错误问题| M[answerWrongQuestion] K --> N[解析车控命令] N --> O[执行TTS播报] O --> P[执行车控命令] P --> Q[生成执行反馈] Q --> R[更新UI显示] + L --> S[SSE流式对话] + S --> T[实时TTS播报] + T --> R +``` + +### 服务间协作关系 + +``` +MessageService (核心协调器) +├── ChatSseService (AI对话) +│ └── LocalTtsService (流式TTS) +├── AudioRecorderService (音频录制) +├── VoiceRecognitionService (语音识别) +├── TextClassificationService (文本分类) +├── VehicleCommandService (车控解析) +│ └── CommandService (命令执行) +└── RedisService (状态缓存) ``` ### 状态管理流程 @@ -332,6 +503,11 @@ idle -> recording -> recognizing -> replying -> idle listening -> normal -> thinking -> executing -> success/failure ``` +**TTS任务状态**: +``` +ready -> playing -> completed +``` + ## 🔧 配置与扩展 ### 1. 插件初始化 @@ -352,6 +528,9 @@ ChatAssistantApp.initialize( - 文本分类: `http://143.64.185.20:18606/classify` - 车控解析: `http://143.64.185.20:18606/control` - 控制反馈: `http://143.64.185.20:18606/control_resp` +- SSE对话: `http://143.64.185.20:18606/chat` +- 语音识别: `http://143.64.185.20:18606/voice` +- Redis缓存: `http://143.64.185.20:18606/redis/*` **建议改进**: 将服务端地址配置化,支持动态配置 @@ -377,7 +556,15 @@ ChatAssistantApp.initialize( 部分网络请求缺少完善的错误处理和重试机制。 ### 4. 内存管理 -长时间运行可能存在内存泄漏风险,需要关注Completer和监听器的清理。 +长时间运行可能存在内存泄漏风险,需要关注: +- Completer的正确释放 +- HTTP连接的及时关闭 +- TTS任务队列的清理 + +### 5. 并发控制 +- SSE连接的并发控制 +- TTS播放队列的线程安全 +- 语音识别状态的同步 ## 🚀 扩展建议 @@ -385,21 +572,31 @@ ChatAssistantApp.initialize( - 服务端地址配置化 - 支持多语言配置 - 主题自定义配置 +- TTS引擎选择配置 ### 2. 功能增强 - 添加离线语音识别支持 - 实现消息历史持久化 - 添加更多车控命令类型 +- 支持自定义唤醒词 ### 3. 性能优化 - 实现网络请求缓存 - 优化UI渲染性能 - 添加资源预加载 +- TTS队列优化 ### 4. 测试完善 -- 添加单元测试 -- 集成测试覆盖 +- 添加单元测试覆盖所有服务 +- 集成测试覆盖完整流程 - 性能测试工具 +- 错误场景测试 + +### 5. 架构优化 +- 依赖注入容器 +- 事件总线机制 +- 插件热重载支持 +- 模块化拆分 ## 📝 开发流程建议 @@ -414,12 +611,34 @@ ChatAssistantApp.initialize( - 使用`debugPrint`添加关键节点日志 - 通过Provider DevTools监控状态变化 - 使用Flutter Inspector检查UI结构 +- 监控Method Channel通信日志 ### 3. 集成测试 - 在example项目中测试完整流程 - 验证车控命令回调机制 - 测试不同场景下的错误处理 +- 验证资源文件引用 + +## 💡 技术亮点总结 + +### 1. 流式处理架构 +- SSE实时对话流 +- TTS流式播报 +- 有序任务队列管理 + +### 2. 智能语音处理 +- 实时语音识别显示 +- 中英文自动检测 +- 智能分句算法 + +### 3. 解耦设计 +- 插件与主应用解耦 +- 服务间松耦合 +- 回调机制灵活扩展 + +### 4. 状态管理 +- 响应式UI更新 +- 多层状态协调 +- 异步状态同步 --- - -这份文档涵盖了项目的核心架构、关键代码逻辑、数据流分析和扩展建议,可以帮助您快速理解项目结构并进行后续开发。如有具体问题,可以针对特定模块进行深入分析。 diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 74a78b9..e1f2f5e 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ createState() => _PartScreenState(); @@ -58,6 +65,45 @@ class _PartScreenState extends State { ); } + // 计算PartScreen的位置,使其显示在FloatingIcon附近 + EdgeInsets _calculatePosition(BoxConstraints constraints) { + final screenWidth = constraints.maxWidth; + final screenHeight = constraints.maxHeight; + final iconX = screenWidth - widget.floatingIconPosition.dx; + final iconY = screenHeight - widget.floatingIconPosition.dy; + + // 聊天框的尺寸 + final chatWidth = screenWidth * 0.94; + final maxChatHeight = screenHeight * 0.4; + + // 判断FloatingIcon在屏幕的哪一边 + final isOnRightSide = iconX < screenWidth / 2; + + // 计算水平位置 + double leftPadding; + if (isOnRightSide) { + // 图标在右边,聊天框显示在左边 + leftPadding = (screenWidth - chatWidth) * 0.1; + } else { + // 图标在左边,聊天框显示在右边 + leftPadding = screenWidth - chatWidth - (screenWidth - chatWidth) * 0.1; + } + + // 计算垂直位置,确保聊天框在图标上方 + double bottomPadding = iconY + widget.iconSize + 20; + + // 确保聊天框不会超出屏幕 + final minBottomPadding = screenHeight * 0.15; + final maxBottomPadding = screenHeight - maxChatHeight - 50; + bottomPadding = bottomPadding.clamp(minBottomPadding, maxBottomPadding); + + return EdgeInsets.only( + left: leftPadding, + right: screenWidth - leftPadding - chatWidth, + bottom: bottomPadding, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -67,9 +113,12 @@ class _PartScreenState extends State { if (!_isInitialized) { return const SizedBox.shrink(); } + + final position = _calculatePosition(constraints); final double minHeight = constraints.maxHeight * 0.16; final double maxHeight = constraints.maxHeight * 0.4; final double chatWidth = constraints.maxWidth * 0.94; + return Stack( children: [ Positioned.fill( @@ -92,71 +141,81 @@ class _PartScreenState extends State { ), ), ), - Padding( - padding: EdgeInsets.only(bottom: constraints.maxHeight * 0.22), - child: Align( - alignment: Alignment.bottomCenter, - child: Consumer( - builder: (context, messageService, child) { - final messageCount = messageService.messages.length; - if (messageCount > _lastMessageCount) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToBottom(); - }); - } - _lastMessageCount = messageCount; - return Container( - width: chatWidth, - constraints: BoxConstraints( - minHeight: minHeight, - maxHeight: maxHeight, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), - child: GradientBackground( - colors: const [ - Color(0XBF3B0A3F), - Color(0xBF0E0E24), - Color(0xBF0C0B33), - ], - borderRadius: BorderRadius.circular(6), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.only( - top: 50, left: 6, right: 6, bottom: 30), - child: LayoutBuilder( - builder: (context, boxConstraints) { - return ChatBox( - scrollController: _scrollController, - messages: messageService.messages, - ); - }), - ), - Positioned( - top: 6, - right: 6, - child: IconButton( - icon: Image.asset( - 'assets/images/open_in_full.png', - width: 24, - height: 24, - package: 'ai_chat_assistant', + Positioned( + left: position.left, + right: position.right, + bottom: position.bottom, + child: Consumer( + builder: (context, messageService, child) { + final messageCount = messageService.messages.length; + if (messageCount > _lastMessageCount) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollToBottom(); + }); + } + _lastMessageCount = messageCount; + + return TweenAnimationBuilder( + tween: Tween(begin: 0.8, end: 1.0), + duration: const Duration(milliseconds: 200), + curve: Curves.easeOutBack, + builder: (context, scale, child) { + return Transform.scale( + scale: scale, + child: Container( + width: chatWidth, + constraints: BoxConstraints( + minHeight: minHeight, + maxHeight: maxHeight, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), + child: GradientBackground( + colors: const [ + Color(0XBF3B0A3F), + Color(0xBF0E0E24), + Color(0xBF0C0B33), + ], + borderRadius: BorderRadius.circular(6), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 50, left: 6, right: 6, bottom: 30), + child: LayoutBuilder( + builder: (context, boxConstraints) { + return ChatBox( + scrollController: _scrollController, + messages: messageService.messages, + ); + }), ), - onPressed: _openFullScreen, - padding: EdgeInsets.zero, - ), + Positioned( + top: 6, + right: 6, + child: IconButton( + icon: Image.asset( + 'assets/images/open_in_full.png', + width: 24, + height: 24, + package: 'ai_chat_assistant', + ), + onPressed: _openFullScreen, + padding: EdgeInsets.zero, + ), + ), + ], ), - ], + ), ), ), ), - ), - ); - }, - ), + ); + }, + ); + }, ), ), ], diff --git a/lib/widgets/chat_footer.dart b/lib/widgets/chat_footer.dart index 59df9f8..e0d969d 100644 --- a/lib/widgets/chat_footer.dart +++ b/lib/widgets/chat_footer.dart @@ -135,7 +135,9 @@ class _ChatFooterState extends State Padding( padding: const EdgeInsets.only(bottom: 12, right: 16), child: GestureDetector( - onTap: () {}, + onTap: () { + + }, child: Image.asset( 'assets/images/keyboard_mini.png',package: 'ai_chat_assistant', width: 24, diff --git a/lib/widgets/floating_icon.dart b/lib/widgets/floating_icon.dart index 2ad7b8e..6c7a04a 100644 --- a/lib/widgets/floating_icon.dart +++ b/lib/widgets/floating_icon.dart @@ -4,7 +4,7 @@ import '../services/message_service.dart'; import '../screens/full_screen.dart'; import '../screens/part_screen.dart'; import 'floating_icon_with_wave.dart'; -import 'dart:async'; // 添加此行 +import 'dart:async'; import 'package:basic_intl/intl.dart'; class FloatingIcon extends StatefulWidget { @@ -14,15 +14,25 @@ class FloatingIcon extends StatefulWidget { State createState() => _FloatingIconState(); } -class _FloatingIconState extends State - with SingleTickerProviderStateMixin { +class _FloatingIconState extends State with TickerProviderStateMixin { Offset _position = const Offset(10, 120); final iconSize = 80.0; bool _isShowPartScreen = false; + bool _isDragging = false; + bool _isAnimating = false; + Offset _targetPosition = const Offset(10, 120); + + // 水波纹动画控制器 late AnimationController _waveAnimationController; + // PartScreen 显示动画控制器 + late AnimationController _partScreenAnimationController; + // 图片切换定时器 + Timer? _imageTimer; + // 添加长按状态标记 + bool _isLongPressing = false; - // 新增:图片切换相关 + // 图片切换相关 int _imageIndex = 0; late final List _iconImages = [ 'assets/images/ai1_hd.png', @@ -33,7 +43,6 @@ class _FloatingIconState extends State 'assets/images/ai1_hd_en.png', 'assets/images/ai0_hd_en.png', ]; - Timer? _imageTimer; // 用于定时切换图片 @override void initState() { @@ -42,18 +51,28 @@ class _FloatingIconState extends State duration: const Duration(milliseconds: 1000), vsync: this, ); - // 使用Timer.periodic定时切换图片 + + // PartScreen动画控制器 + _partScreenAnimationController = AnimationController( + duration: const Duration(milliseconds: 250), + vsync: this, + ); + + // 图片切换定时器 _imageTimer = Timer.periodic(const Duration(seconds: 3), (timer) { - setState(() { - _imageIndex = (_imageIndex + 1) % _iconImages.length; - }); + if (mounted && !_isDragging) { + setState(() { + _imageIndex = (_imageIndex + 1) % _iconImages.length; + }); + } }); } @override void dispose() { _waveAnimationController.dispose(); - _imageTimer?.cancel(); // 释放Timer + _partScreenAnimationController.dispose(); + _imageTimer?.cancel(); super.dispose(); } @@ -61,14 +80,20 @@ class _FloatingIconState extends State setState(() { _isShowPartScreen = true; }); + _partScreenAnimationController.forward(); } void _hidePartScreen() { - setState(() { - _isShowPartScreen = false; + _partScreenAnimationController.reverse().then((_) { + if (mounted) { + setState(() { + _isShowPartScreen = false; + }); + } }); } + // 显示全屏界面 void _showFullScreen() async { _hidePartScreen(); await Navigator.of(context).push( @@ -78,50 +103,106 @@ class _FloatingIconState extends State ); } + // 更新位置 void _updatePosition(Offset delta) { + if (!_isDragging) return; + setState(() { final screenSize = MediaQuery.of(context).size; - double newX = - (_position.dx - delta.dx).clamp(0.0, screenSize.width - iconSize); - double newY = - (_position.dy - delta.dy).clamp(0.0, screenSize.height - iconSize); + double newX = (_position.dx - delta.dx).clamp(0.0, screenSize.width - iconSize); + double newY = (_position.dy - delta.dy).clamp(0.0, screenSize.height - iconSize); _position = Offset(newX, newY); }); } + // 吸附到屏幕边缘 + void _snapToEdge() { + final screenSize = MediaQuery.of(context).size; + final centerX = screenSize.width / 2; + + // 判断应该吸到左边还是右边 + final shouldSnapToLeft = _position.dx < centerX - iconSize / 2; + final targetX = shouldSnapToLeft ? 0.0 : screenSize.width - iconSize; + + setState(() { + _targetPosition = Offset(targetX, _position.dy); + _isAnimating = true; + }); + + // 使用定时器在动画完成后更新状态 + Timer(const Duration(milliseconds: 100), () { + if (mounted) { + setState(() { + _position = _targetPosition; + _isAnimating = false; + }); + } + }); + } + + void _onPanStart(DragStartDetails details) { + _isDragging = true; + _waveAnimationController.stop(); + } + + void _onPanEnd(DragEndDetails details) { + _isDragging = false; + _snapToEdge(); + } + @override Widget build(BuildContext context) { + // 奇怪的代码,不应该放在build里面 + // WidgetsBinding.instance.addPostFrameCallback((_) { + // if (messageService.isRecording) { + // if (!_waveAnimationController.isAnimating) { + // _waveAnimationController.repeat(); + // } + // } else { + // if (_waveAnimationController.isAnimating) { + // _waveAnimationController.stop(); + // } + // } + // }); + return Stack( children: [ if (_isShowPartScreen) - PartScreen( - onHide: _hidePartScreen, + FadeTransition( + opacity: _partScreenAnimationController, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 0.3), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _partScreenAnimationController, + curve: Curves.easeOutQuart, + )), + child: PartScreen( + onHide: _hidePartScreen, + floatingIconPosition: _position, + iconSize: iconSize, + )), ), - Positioned( - bottom: _position.dy, - right: _position.dx, + AnimatedPositioned( + duration: const Duration(milliseconds: 250), + curve: Curves.easeOutBack, + bottom: _isAnimating ? _targetPosition.dy : _position.dy, + right: _isAnimating ? _targetPosition.dx : _position.dx, child: Consumer( builder: (context, messageService, child) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (messageService.isRecording) { - if (!_waveAnimationController.isAnimating) { - _waveAnimationController.repeat(); - } - } else { - if (_waveAnimationController.isAnimating) { - _waveAnimationController.stop(); - } - } - }); - return GestureDetector( onTap: _showFullScreen, onLongPress: () async { + debugPrint('Long press'); + _isLongPressing = true; // 标记开始长按 _showPartScreen(); _waveAnimationController.repeat(); await messageService.startVoiceInput(); }, onLongPressUp: () async { + debugPrint('Long press up'); + _isLongPressing = false; // 清除长按标记 _waveAnimationController.stop(); await messageService.stopAndProcessVoiceInput(); final hasMessage = messageService.messages.isNotEmpty; @@ -129,23 +210,49 @@ class _FloatingIconState extends State _hidePartScreen(); } }, - onPanUpdate: (details) => _updatePosition(details.delta), - child: messageService.isRecording - ? FloatingIconWithWave( - animationController: _waveAnimationController, - iconSize: iconSize, - waveColor: Colors.white, - ) - : Image.asset( - Intl.getCurrentLocale().startsWith('zh') - ? _iconImages[_imageIndex]:_iconImagesEn[_imageIndex], - width: iconSize, - height: iconSize,package: 'ai_chat_assistant', - ), + onLongPressEnd: (d) { + debugPrint('Long press end $d'); + _isLongPressing = false; // 清除长按标记 + }, + onPanStart: (details) { + // 只有在非长按状态下才允许拖拽 + if (!_isLongPressing) { + _onPanStart(details); + } + }, + onPanUpdate: (details) { + // 只有在拖拽状态下才更新位置 + if (_isDragging && !_isLongPressing) { + _updatePosition(details.delta); + } + }, + onPanEnd: (details) { + if (!_isLongPressing) { + _onPanEnd(details); + } + }, + child: AnimatedScale( + scale: _isDragging ? 1.1 : 1.0, + duration: const Duration(milliseconds: 150), + child: messageService.isRecording + ? FloatingIconWithWave( + animationController: _waveAnimationController, + iconSize: iconSize, + waveColor: Colors.white, + ) + : Image.asset( + Intl.getCurrentLocale().startsWith('zh') + ? _iconImages[_imageIndex] + : _iconImagesEn[_imageIndex], + width: iconSize, + height: iconSize, + package: 'ai_chat_assistant', + ), + ), ); }, ), - ), + ) ], ); }