You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
256 lines
8.3 KiB
256 lines
8.3 KiB
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:dating_touchme_app/controller/message/call_controller.dart';
|
|
import 'package:dating_touchme_app/extension/ex_widget.dart';
|
|
import 'package:dating_touchme_app/generated/assets.dart';
|
|
import 'package:dating_touchme_app/pages/message/video_call_page.dart';
|
|
import 'package:dating_touchme_app/rtc/rtc_manager.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
|
|
|
|
/// 视频通话小窗组件
|
|
class VideoCallOverlayWidget extends StatefulWidget {
|
|
final VoidCallback? onClose;
|
|
final String targetUserId;
|
|
final String? targetUserName;
|
|
final String? targetAvatarUrl;
|
|
final EMMessage? message;
|
|
|
|
const VideoCallOverlayWidget({
|
|
super.key,
|
|
this.onClose,
|
|
required this.targetUserId,
|
|
this.targetUserName,
|
|
this.targetAvatarUrl,
|
|
this.message,
|
|
});
|
|
|
|
@override
|
|
State<VideoCallOverlayWidget> createState() => _VideoCallOverlayWidgetState();
|
|
}
|
|
|
|
class _VideoCallOverlayWidgetState extends State<VideoCallOverlayWidget> {
|
|
Offset _position = Offset.zero;
|
|
bool _isDragging = false;
|
|
final CallController _callController = CallController.instance;
|
|
final RTCManager _rtcManager = RTCManager.instance;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// 初始位置设置为右上角
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
final size = MediaQuery.of(context).size;
|
|
setState(() {
|
|
_position = Offset(
|
|
size.width - 100.w,
|
|
100,
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
/// 吸附到边缘
|
|
void _snapToEdge(double screenWidth) {
|
|
final centerX = screenWidth / 2;
|
|
final targetX = _position.dx < centerX
|
|
? 0.0
|
|
: screenWidth - 100.w;
|
|
|
|
setState(() {
|
|
_position = Offset(targetX, _position.dy);
|
|
_isDragging = false;
|
|
});
|
|
}
|
|
|
|
/// 构建背景(视频或头像)
|
|
Widget _buildBackground() {
|
|
return Obx(() {
|
|
// 在 Obx 中访问响应式变量,确保建立监听关系
|
|
final callSession = _callController.currentCall.value;
|
|
final isVideoCall = callSession != null && callSession.callType == CallType.video;
|
|
final isConnected = callSession != null && _callController.callDurationSeconds.value > 0;
|
|
final remoteUid = _callController.remoteUid.value;
|
|
final remoteUsers = _rtcManager.remoteUsersNotifier.value;
|
|
|
|
// 如果是视频通话且已接通,显示远端视频
|
|
if (isVideoCall && isConnected && remoteUid != null) {
|
|
final engine = _rtcManager.engine;
|
|
if (engine != null) {
|
|
final remoteVideoViewController = VideoViewController(
|
|
rtcEngine: engine,
|
|
canvas: VideoCanvas(uid: remoteUid),
|
|
);
|
|
return SizedBox(
|
|
width: 100.w,
|
|
height: 100.w,
|
|
child: AgoraVideoView(
|
|
controller: remoteVideoViewController,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 如果 remoteUid 为空,尝试从 RTCManager 的远端用户列表中获取
|
|
if (isVideoCall && isConnected && remoteUid == null && remoteUsers.isNotEmpty) {
|
|
final firstRemoteUid = remoteUsers.first;
|
|
final engine = _rtcManager.engine;
|
|
if (engine != null) {
|
|
_callController.remoteUid.value = firstRemoteUid;
|
|
final remoteVideoViewController = VideoViewController(
|
|
rtcEngine: engine,
|
|
canvas: VideoCanvas(uid: firstRemoteUid),
|
|
);
|
|
return SizedBox(
|
|
width: 100.w,
|
|
height: 100.w,
|
|
child: AgoraVideoView(
|
|
controller: remoteVideoViewController,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 默认显示头像
|
|
return Container(
|
|
width: 100.w,
|
|
height: 100.w,
|
|
color: Colors.black,
|
|
child: widget.targetAvatarUrl != null && widget.targetAvatarUrl!.isNotEmpty
|
|
? CachedNetworkImage(
|
|
imageUrl: widget.targetAvatarUrl!,
|
|
fit: BoxFit.cover,
|
|
errorWidget: (context, url, error) => Image.asset(
|
|
Assets.imagesUserAvatar,
|
|
fit: BoxFit.cover,
|
|
),
|
|
)
|
|
: Image.asset(
|
|
Assets.imagesUserAvatar,
|
|
fit: BoxFit.cover,
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
/// 格式化通话时长
|
|
String _formatDuration(int seconds) {
|
|
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
|
final minutes = twoDigits(seconds ~/ 60);
|
|
final secs = twoDigits(seconds % 60);
|
|
return '$minutes:$secs';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final screenSize = MediaQuery.of(context).size;
|
|
|
|
return AnimatedPositioned(
|
|
duration: _isDragging
|
|
? const Duration(milliseconds: 50)
|
|
: const Duration(milliseconds: 300),
|
|
curve: _isDragging ? Curves.linear : Curves.easeOut,
|
|
left: _position.dx,
|
|
top: _position.dy,
|
|
child: _buildContent(screenSize),
|
|
);
|
|
}
|
|
|
|
Widget _buildContent(Size screenSize) {
|
|
return GestureDetector(
|
|
onPanStart: (details) {
|
|
setState(() {
|
|
_isDragging = true;
|
|
});
|
|
},
|
|
onPanUpdate: (details) {
|
|
setState(() {
|
|
_position += details.delta;
|
|
_position = Offset(
|
|
_position.dx.clamp(0.0, screenSize.width - 100.w),
|
|
_position.dy.clamp(0.0, screenSize.height - 100.w),
|
|
);
|
|
});
|
|
},
|
|
onPanEnd: (details) {
|
|
_snapToEdge(screenSize.width);
|
|
},
|
|
child: Obx(() {
|
|
final callSession = _callController.currentCall.value;
|
|
final isConnected = callSession != null && _callController.callDurationSeconds.value > 0;
|
|
final callDuration = _callController.callDurationSeconds.value;
|
|
|
|
return Container(
|
|
width: 100.w,
|
|
height: 100.w,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8.w),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.3),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(8.w),
|
|
child: Stack(
|
|
children: [
|
|
// 背景:视频通话接通时显示远端视频,否则显示头像
|
|
_buildBackground(),
|
|
// 半透明遮罩
|
|
Container(
|
|
color: Colors.black.withOpacity(0.4),
|
|
),
|
|
// 内容:昵称和状态
|
|
Positioned(
|
|
bottom: 8.w,
|
|
left: 4.w,
|
|
right: 4.w,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// 昵称
|
|
Text(
|
|
widget.targetUserName ?? widget.targetUserId,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 10.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
SizedBox(height: 2.h),
|
|
// 通话状态
|
|
Text(
|
|
isConnected ? _formatDuration(callDuration) : '通话中',
|
|
style: TextStyle(
|
|
color: Colors.white.withOpacity(0.9),
|
|
fontSize: 8.sp,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
).onTap(() {
|
|
Get.log("点击小窗${widget.message}");
|
|
// 点击小窗,返回视频通话页面
|
|
Get.to(() => VideoCallPage(
|
|
targetUserId: widget.targetUserId,
|
|
callMessage: widget.message,
|
|
isInitiator: callSession?.isInitiator ?? CallController.instance.callRole == CallRole.caller ? true : false,
|
|
));
|
|
widget.onClose?.call();
|
|
});
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|