15 changed files with 992 additions and 16 deletions
Split View
Diff Options
-
BINassets/images/accept_call.png
-
BINassets/images/plat_voice_message.png
-
BINassets/images/plat_voice_message_self.png
-
BINassets/images/reject_call.png
-
BINassets/images/send_call.png
-
BINassets/images/send_video_call.png
-
BINassets/images/voice_bg_message.png
-
BINassets/images/voice_bg_message_self.png
-
395lib/controller/message/call_manager.dart
-
122lib/controller/message/chat_controller.dart
-
8lib/generated/assets.dart
-
74lib/pages/message/chat_page.dart
-
314lib/widget/message/call_item.dart
-
58lib/widget/message/chat_input_bar.dart
-
37lib/widget/message/message_item.dart
@ -0,0 +1,395 @@ |
|||
import 'dart:async'; |
|||
import 'dart:convert'; |
|||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:im_flutter_sdk/im_flutter_sdk.dart'; |
|||
|
|||
import 'chat_controller.dart'; |
|||
|
|||
/// 通话类型 |
|||
enum CallType { |
|||
voice, // 语音通话 |
|||
video, // 视频通话 |
|||
} |
|||
|
|||
/// 通话状态 |
|||
enum CallStatus { |
|||
calling, // 通话中 |
|||
missed, // 未接听 |
|||
cancelled, // 已取消 |
|||
rejected, // 已拒绝 |
|||
connected, // 已接通(暂时用不到,但保留用于未来扩展) |
|||
} |
|||
|
|||
/// 通话管理器,单例模式,统一管理通话逻辑 |
|||
class CallManager extends GetxController { |
|||
static CallManager? _instance; |
|||
static CallManager get instance { |
|||
_instance ??= Get.put(CallManager()); |
|||
return _instance!; |
|||
} |
|||
|
|||
// 当前正在进行的通话 |
|||
final Rx<CallSession?> currentCall = Rx<CallSession?>(null); |
|||
|
|||
// 通话计时器(用于记录通话时长) |
|||
Timer? _callTimer; |
|||
int _callDurationSeconds = 0; |
|||
final RxInt callDurationSeconds = RxInt(0); |
|||
|
|||
CallManager() { |
|||
print('📞 [CallManager] 通话管理器已初始化'); |
|||
} |
|||
|
|||
/// 发起通话 |
|||
/// [targetUserId] 目标用户ID |
|||
/// [callType] 通话类型:语音或视频 |
|||
/// [chatController] 聊天控制器,用于发送通话消息 |
|||
Future<bool> initiateCall({ |
|||
required String targetUserId, |
|||
required CallType callType, |
|||
ChatController? chatController, |
|||
}) async { |
|||
try { |
|||
if (currentCall.value != null) { |
|||
SmartDialog.showToast('已有通话正在进行中'); |
|||
return false; |
|||
} |
|||
|
|||
print('📞 [CallManager] 发起${callType == CallType.video ? "视频" : "语音"}通话,目标用户: $targetUserId'); |
|||
|
|||
// 创建通话会话 |
|||
final session = CallSession( |
|||
targetUserId: targetUserId, |
|||
callType: callType, |
|||
status: CallStatus.calling, |
|||
isInitiator: true, |
|||
startTime: DateTime.now(), |
|||
); |
|||
currentCall.value = session; |
|||
|
|||
// 发送通话消息(发起) |
|||
final callTypeStr = callType == CallType.video ? 'video' : 'voice'; |
|||
await _sendCallMessage( |
|||
targetUserId: targetUserId, |
|||
callType: callTypeStr, |
|||
callStatus: 'missed', // 初始状态为未接听,等待对方响应 |
|||
chatController: chatController, |
|||
); |
|||
|
|||
// TODO: 这里可以集成实际的通话SDK,发起真正的通话 |
|||
// 例如:await RTCManager.instance.startCall(targetUserId, callType); |
|||
|
|||
return true; |
|||
} catch (e) { |
|||
print('❌ [CallManager] 发起通话失败: $e'); |
|||
SmartDialog.showToast('发起通话失败: $e'); |
|||
currentCall.value = null; |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 接听通话 |
|||
/// [message] 通话消息 |
|||
/// [chatController] 聊天控制器,用于更新通话消息 |
|||
Future<bool> acceptCall({ |
|||
required EMMessage message, |
|||
ChatController? chatController, |
|||
}) async { |
|||
try { |
|||
final callInfo = _parseCallInfo(message); |
|||
if (callInfo == null) { |
|||
return false; |
|||
} |
|||
|
|||
final targetUserId = message.from ?? ''; |
|||
final callTypeStr = callInfo['callType'] as String?; |
|||
final callType = callTypeStr == 'video' ? CallType.video : CallType.voice; |
|||
|
|||
print('📞 [CallManager] 接听${callType == CallType.video ? "视频" : "语音"}通话'); |
|||
|
|||
// 创建通话会话 |
|||
final session = CallSession( |
|||
targetUserId: targetUserId, |
|||
callType: callType, |
|||
status: CallStatus.calling, |
|||
isInitiator: false, |
|||
startTime: DateTime.now(), |
|||
); |
|||
currentCall.value = session; |
|||
|
|||
// 开始计时 |
|||
_startCallTimer(); |
|||
|
|||
// 更新通话消息状态为通话中 |
|||
await _updateCallMessageStatus( |
|||
message: message, |
|||
callStatus: 'calling', |
|||
callDuration: 0, |
|||
chatController: chatController, |
|||
); |
|||
|
|||
// TODO: 这里可以集成实际的通话SDK,接听通话 |
|||
// 例如:await RTCManager.instance.acceptCall(targetUserId, callType); |
|||
|
|||
return true; |
|||
} catch (e) { |
|||
print('❌ [CallManager] 接听通话失败: $e'); |
|||
SmartDialog.showToast('接听通话失败: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 拒绝通话 |
|||
/// [message] 通话消息 |
|||
/// [chatController] 聊天控制器,用于更新通话消息 |
|||
Future<bool> rejectCall({ |
|||
required EMMessage message, |
|||
ChatController? chatController, |
|||
}) async { |
|||
try { |
|||
print('📞 [CallManager] 拒绝通话'); |
|||
|
|||
// 更新通话消息状态为已拒绝 |
|||
await _updateCallMessageStatus( |
|||
message: message, |
|||
callStatus: 'rejected', |
|||
chatController: chatController, |
|||
); |
|||
|
|||
// 清理通话会话 |
|||
currentCall.value = null; |
|||
|
|||
// TODO: 这里可以集成实际的通话SDK,拒绝通话 |
|||
// 例如:await RTCManager.instance.rejectCall(message.from ?? ''); |
|||
|
|||
return true; |
|||
} catch (e) { |
|||
print('❌ [CallManager] 拒绝通话失败: $e'); |
|||
SmartDialog.showToast('拒绝通话失败: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 取消通话 |
|||
/// [message] 通话消息(可选,如果是发起方取消) |
|||
/// [chatController] 聊天控制器,用于更新通话消息 |
|||
Future<bool> cancelCall({ |
|||
EMMessage? message, |
|||
ChatController? chatController, |
|||
}) async { |
|||
try { |
|||
print('📞 [CallManager] 取消通话'); |
|||
|
|||
// 如果有消息,更新通话消息状态为已取消 |
|||
if (message != null) { |
|||
await _updateCallMessageStatus( |
|||
message: message, |
|||
callStatus: 'cancelled', |
|||
chatController: chatController, |
|||
); |
|||
} |
|||
|
|||
// 停止计时 |
|||
_stopCallTimer(); |
|||
|
|||
// 清理通话会话 |
|||
currentCall.value = null; |
|||
|
|||
// TODO: 这里可以集成实际的通话SDK,取消通话 |
|||
// 例如:await RTCManager.instance.cancelCall(); |
|||
|
|||
return true; |
|||
} catch (e) { |
|||
print('❌ [CallManager] 取消通话失败: $e'); |
|||
SmartDialog.showToast('取消通话失败: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 结束通话(通话完成) |
|||
/// [callDuration] 通话时长(秒) |
|||
/// [chatController] 聊天控制器,用于更新通话消息 |
|||
Future<bool> endCall({ |
|||
required int callDuration, |
|||
EMMessage? message, |
|||
ChatController? chatController, |
|||
}) async { |
|||
try { |
|||
print('📞 [CallManager] 结束通话,时长: ${callDuration}秒'); |
|||
|
|||
// 停止计时 |
|||
_stopCallTimer(); |
|||
|
|||
// 如果有消息,更新通话消息状态为通话中(显示时长) |
|||
if (message != null) { |
|||
await _updateCallMessageStatus( |
|||
message: message, |
|||
callStatus: 'calling', |
|||
callDuration: callDuration, |
|||
chatController: chatController, |
|||
); |
|||
} |
|||
|
|||
// 清理通话会话 |
|||
currentCall.value = null; |
|||
|
|||
// TODO: 这里可以集成实际的通话SDK,结束通话 |
|||
// 例如:await RTCManager.instance.endCall(); |
|||
|
|||
return true; |
|||
} catch (e) { |
|||
print('❌ [CallManager] 结束通话失败: $e'); |
|||
SmartDialog.showToast('结束通话失败: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 开始通话计时 |
|||
void _startCallTimer() { |
|||
_callDurationSeconds = 0; |
|||
callDurationSeconds.value = 0; |
|||
_callTimer?.cancel(); |
|||
_callTimer = Timer.periodic(Duration(seconds: 1), (timer) { |
|||
_callDurationSeconds++; |
|||
callDurationSeconds.value = _callDurationSeconds; |
|||
}); |
|||
} |
|||
|
|||
/// 停止通话计时 |
|||
void _stopCallTimer() { |
|||
_callTimer?.cancel(); |
|||
_callTimer = null; |
|||
_callDurationSeconds = 0; |
|||
callDurationSeconds.value = 0; |
|||
} |
|||
|
|||
/// 发送通话消息 |
|||
Future<bool> _sendCallMessage({ |
|||
required String targetUserId, |
|||
required String callType, |
|||
required String callStatus, |
|||
int? callDuration, |
|||
ChatController? chatController, |
|||
}) async { |
|||
try { |
|||
// 如果提供了 chatController,使用它发送消息 |
|||
if (chatController != null) { |
|||
return await chatController.sendCallMessage( |
|||
callType: callType, |
|||
callStatus: callStatus, |
|||
callDuration: callDuration, |
|||
); |
|||
} |
|||
|
|||
// 否则直接通过 IMManager 发送 |
|||
final callInfoMap = <String, dynamic>{ |
|||
'callType': callType, |
|||
'callStatus': callStatus, |
|||
}; |
|||
if (callDuration != null) { |
|||
callInfoMap['callDuration'] = callDuration; |
|||
} |
|||
final callInfoJson = jsonEncode(callInfoMap); |
|||
final content = '[CALL:]$callInfoJson'; |
|||
|
|||
final message = EMMessage.createTxtSendMessage( |
|||
targetId: targetUserId, |
|||
content: content, |
|||
); |
|||
|
|||
await EMClient.getInstance.chatManager.sendMessage(message); |
|||
return true; |
|||
} catch (e) { |
|||
print('❌ [CallManager] 发送通话消息失败: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 更新通话消息状态 |
|||
Future<bool> _updateCallMessageStatus({ |
|||
required EMMessage message, |
|||
required String callStatus, |
|||
int? callDuration, |
|||
ChatController? chatController, |
|||
}) async { |
|||
try { |
|||
// 解析现有通话信息 |
|||
final callInfo = _parseCallInfo(message); |
|||
if (callInfo == null) { |
|||
return false; |
|||
} |
|||
|
|||
final callType = callInfo['callType'] as String? ?? 'voice'; |
|||
final targetUserId = message.from ?? message.to ?? ''; |
|||
|
|||
// 发送更新的通话消息 |
|||
return await _sendCallMessage( |
|||
targetUserId: targetUserId, |
|||
callType: callType, |
|||
callStatus: callStatus, |
|||
callDuration: callDuration, |
|||
chatController: chatController, |
|||
); |
|||
} catch (e) { |
|||
print('❌ [CallManager] 更新通话消息状态失败: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 从消息中解析通话信息 |
|||
Map<String, dynamic>? _parseCallInfo(EMMessage message) { |
|||
try { |
|||
if (message.body.type == MessageType.TXT) { |
|||
final textBody = message.body as EMTextMessageBody; |
|||
final content = textBody.content; |
|||
if (content != null && content.startsWith('[CALL:]')) { |
|||
final jsonStr = content.substring(7); // 移除 '[CALL:]' 前缀 |
|||
return jsonDecode(jsonStr) as Map<String, dynamic>; |
|||
} |
|||
} |
|||
} catch (e) { |
|||
print('解析通话信息失败: $e'); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/// 检查是否有正在进行的通话 |
|||
bool get isInCall => currentCall.value != null; |
|||
|
|||
/// 获取当前通话时长(秒) |
|||
int get currentCallDuration => callDurationSeconds.value; |
|||
|
|||
@override |
|||
void onClose() { |
|||
_stopCallTimer(); |
|||
currentCall.value = null; |
|||
super.onClose(); |
|||
} |
|||
} |
|||
|
|||
/// 通话会话信息 |
|||
class CallSession { |
|||
final String targetUserId; |
|||
final CallType callType; |
|||
final CallStatus status; |
|||
final bool isInitiator; // 是否是发起方 |
|||
final DateTime startTime; |
|||
DateTime? endTime; |
|||
|
|||
CallSession({ |
|||
required this.targetUserId, |
|||
required this.callType, |
|||
required this.status, |
|||
required this.isInitiator, |
|||
required this.startTime, |
|||
this.endTime, |
|||
}); |
|||
|
|||
/// 获取通话时长(秒) |
|||
int get duration { |
|||
final end = endTime ?? DateTime.now(); |
|||
return end.difference(startTime).inSeconds; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,314 @@ |
|||
import 'dart:convert'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:im_flutter_sdk/im_flutter_sdk.dart'; |
|||
|
|||
import '../../generated/assets.dart'; |
|||
|
|||
/// 通话类型 |
|||
enum CallType { |
|||
voice, // 语音通话 |
|||
video, // 视频通话 |
|||
} |
|||
|
|||
/// 通话状态 |
|||
enum CallStatus { |
|||
calling, // 通话中(显示时长) |
|||
missed, // 未接听 |
|||
cancelled, // 已取消 |
|||
rejected, // 已拒绝 |
|||
} |
|||
|
|||
class CallItem extends StatelessWidget { |
|||
final EMMessage message; |
|||
final bool isSentByMe; |
|||
final bool showTime; |
|||
final String formattedTime; |
|||
final VoidCallback? onResend; |
|||
|
|||
const CallItem({ |
|||
required this.message, |
|||
required this.isSentByMe, |
|||
required this.showTime, |
|||
required this.formattedTime, |
|||
this.onResend, |
|||
super.key, |
|||
}); |
|||
|
|||
/// 从消息内容中解析通话信息(使用特殊的JSON格式) |
|||
Map<String, dynamic>? _parseCallInfo() { |
|||
try { |
|||
if (message.body.type == MessageType.TXT) { |
|||
final textBody = message.body as EMTextMessageBody; |
|||
final content = textBody.content; |
|||
|
|||
// 检查是否是通话消息(以 [CALL:] 开头) |
|||
if (content != null && content.startsWith('[CALL:]')) { |
|||
final jsonStr = content.substring(7); // 移除 '[CALL:]' 前缀 |
|||
return jsonDecode(jsonStr) as Map<String, dynamic>; |
|||
} |
|||
} |
|||
} catch (e) { |
|||
print('解析通话信息失败: $e'); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/// 从消息中解析通话类型 |
|||
CallType? _getCallType() { |
|||
final callInfo = _parseCallInfo(); |
|||
if (callInfo != null) { |
|||
final callTypeStr = callInfo['callType'] as String?; |
|||
if (callTypeStr == 'voice') { |
|||
return CallType.voice; |
|||
} else if (callTypeStr == 'video') { |
|||
return CallType.video; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/// 从消息中解析通话状态 |
|||
CallStatus? _getCallStatus() { |
|||
final callInfo = _parseCallInfo(); |
|||
if (callInfo != null) { |
|||
final statusStr = callInfo['callStatus'] as String?; |
|||
if (statusStr == 'calling') { |
|||
return CallStatus.calling; |
|||
} else if (statusStr == 'missed') { |
|||
return CallStatus.missed; |
|||
} else if (statusStr == 'cancelled') { |
|||
return CallStatus.cancelled; |
|||
} else if (statusStr == 'rejected') { |
|||
return CallStatus.rejected; |
|||
} |
|||
} |
|||
return CallStatus.missed; // 默认未接听 |
|||
} |
|||
|
|||
/// 获取通话时长(秒) |
|||
int? _getCallDuration() { |
|||
final callInfo = _parseCallInfo(); |
|||
if (callInfo != null) { |
|||
return callInfo['callDuration'] as int?; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/// 格式化通话时长 |
|||
String _formatDuration(int seconds) { |
|||
final minutes = seconds ~/ 60; |
|||
final secs = seconds % 60; |
|||
if (minutes > 0) { |
|||
return '${minutes}:${secs.toString().padLeft(2, '0')}'; |
|||
} else { |
|||
return '${secs}秒'; |
|||
} |
|||
} |
|||
|
|||
/// 获取图标资源 |
|||
String _getIconAsset() { |
|||
final callType = _getCallType(); |
|||
final callStatus = _getCallStatus(); |
|||
|
|||
if (isSentByMe) { |
|||
// 自己发起的通话 |
|||
if (callType == CallType.video) { |
|||
return Assets.imagesSendVideoCall; |
|||
} else { |
|||
return Assets.imagesSendCall; |
|||
} |
|||
} else { |
|||
// 别人发起的通话 |
|||
if (callStatus == CallStatus.rejected) { |
|||
return Assets.imagesRejectCall; |
|||
} else { |
|||
return Assets.imagesAcceptCall; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 获取状态文本 |
|||
String _getStatusText() { |
|||
final callType = _getCallType(); |
|||
final callStatus = _getCallStatus(); |
|||
final duration = _getCallDuration(); |
|||
|
|||
if (callStatus == CallStatus.calling && duration != null) { |
|||
// 通话中,显示时长 |
|||
return _formatDuration(duration); |
|||
} else if (callStatus == CallStatus.missed) { |
|||
return callType == CallType.video ? '未接听视频通话' : '未接听语音通话'; |
|||
} else if (callStatus == CallStatus.cancelled) { |
|||
return callType == CallType.video ? '已取消视频通话' : '已取消语音通话'; |
|||
} else if (callStatus == CallStatus.rejected) { |
|||
return callType == CallType.video ? '已拒绝视频通话' : '已拒绝语音通话'; |
|||
} else { |
|||
return callType == CallType.video ? '视频通话' : '语音通话'; |
|||
} |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
final callType = _getCallType(); |
|||
if (callType == null) { |
|||
// 如果解析失败,显示默认文本消息 |
|||
return SizedBox.shrink(); |
|||
} |
|||
|
|||
final iconAsset = _getIconAsset(); |
|||
final statusText = _getStatusText(); |
|||
final callStatus = _getCallStatus(); |
|||
final isCalling = callStatus == CallStatus.calling; |
|||
|
|||
return Column( |
|||
children: [ |
|||
// 显示时间 |
|||
if (showTime) _buildTimeLabel(), |
|||
Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), |
|||
child: Row( |
|||
mainAxisAlignment: |
|||
isSentByMe ? MainAxisAlignment.end : MainAxisAlignment.start, |
|||
crossAxisAlignment: CrossAxisAlignment.center, |
|||
children: [ |
|||
if (!isSentByMe) _buildAvatar(), |
|||
if (!isSentByMe) SizedBox(width: 8.w), |
|||
// 发送消息时,状态在左侧 |
|||
if (isSentByMe) |
|||
Align( |
|||
alignment: Alignment.center, |
|||
child: Container( |
|||
margin: EdgeInsets.only(top: 10.h), |
|||
child: _buildMessageStatus(), |
|||
), |
|||
), |
|||
if (isSentByMe) SizedBox(width: 10.w), |
|||
// 通话消息容器 |
|||
Container( |
|||
constraints: BoxConstraints(maxWidth: 200.w), |
|||
margin: EdgeInsets.only(top: 10.h), |
|||
padding: EdgeInsets.symmetric( |
|||
horizontal: 16.w, |
|||
vertical: 12.h, |
|||
), |
|||
decoration: BoxDecoration( |
|||
color: isSentByMe ? Color(0xff8E7BF6) : Colors.white, |
|||
borderRadius: BorderRadius.only( |
|||
topLeft: |
|||
isSentByMe ? Radius.circular(12.w) : Radius.circular(0), |
|||
topRight: |
|||
isSentByMe ? Radius.circular(0) : Radius.circular(12.w), |
|||
bottomLeft: Radius.circular(12.w), |
|||
bottomRight: Radius.circular(12.w), |
|||
), |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
// 图标 |
|||
Image.asset( |
|||
iconAsset, |
|||
width: 24.w, |
|||
height: 24.w, |
|||
fit: BoxFit.contain, |
|||
), |
|||
SizedBox(width: 8.w), |
|||
// 状态文本 |
|||
Flexible( |
|||
child: Text( |
|||
statusText, |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
color: isSentByMe ? Colors.white : Colors.black87, |
|||
fontWeight: isCalling ? FontWeight.w500 : FontWeight.normal, |
|||
), |
|||
maxLines: 2, |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
if (isSentByMe) SizedBox(width: 8.w), |
|||
if (isSentByMe) _buildAvatar(), |
|||
], |
|||
), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
// 构建时间标签 |
|||
Widget _buildTimeLabel() { |
|||
return Container( |
|||
alignment: Alignment.center, |
|||
padding: EdgeInsets.symmetric(horizontal: 16.w), |
|||
child: Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 12.w), |
|||
child: Text( |
|||
formattedTime, |
|||
style: TextStyle(fontSize: 12.sp, color: Colors.grey), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 构建头像 |
|||
Widget _buildAvatar() { |
|||
return Container( |
|||
width: 40.w, |
|||
height: 40.w, |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(20.w), |
|||
image: DecorationImage( |
|||
image: AssetImage(Assets.imagesAvatarsExample), |
|||
fit: BoxFit.cover, |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 构建消息状态(发送中、已发送、失败重发) |
|||
Widget _buildMessageStatus() { |
|||
if (!isSentByMe) { |
|||
return SizedBox.shrink(); |
|||
} |
|||
|
|||
final status = message.status; |
|||
|
|||
if (status == MessageStatus.FAIL) { |
|||
// 发送失败,显示重发按钮 |
|||
return GestureDetector( |
|||
onTap: onResend, |
|||
child: Container( |
|||
width: 20.w, |
|||
height: 20.w, |
|||
decoration: BoxDecoration( |
|||
color: Colors.red.withOpacity(0.1), |
|||
shape: BoxShape.circle, |
|||
), |
|||
child: Icon( |
|||
Icons.refresh, |
|||
size: 14.w, |
|||
color: Colors.red, |
|||
), |
|||
), |
|||
); |
|||
} else if (status == MessageStatus.PROGRESS) { |
|||
// 发送中,显示加载动画 |
|||
return Container( |
|||
width: 16.w, |
|||
height: 16.w, |
|||
child: CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey), |
|||
), |
|||
); |
|||
} else { |
|||
// 发送成功,不显示任何状态 |
|||
return SizedBox.shrink(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
Write
Preview
Loading…
Cancel
Save