Browse Source
feat(live): 添加可拖拽的全局 Overlay 小组件
feat(live): 添加可拖拽的全局 Overlay 小组件
- 新增 DraggableOverlayWidget,支持在屏幕上自由拖拽并自动吸附到边缘 - 实现主播视频视图展示逻辑,兼容主播和观众端的不同渲染方式 - 添加全局 Overlay 控制器 OverlayController,用于管理 overlay 的显示/隐藏状态 - 在 main.dart 中集成全局 overlay 组件,并通过 OverlayController 控制其显示 - 修改 LiveRoomPage 页面,在关闭直播间时切换 overlay 显示状态 - 更新 LiveRechargePopup 弹窗,使用 Get.back() 替代 Navigator.pop() - 移除 RoomController 中多余的分号及未使用的 import 和 dispose 逻辑ios
6 changed files with 254 additions and 8 deletions
Unified View
Diff Options
-
1lib/controller/discover/room_controller.dart
-
23lib/controller/overlay_controller.dart
-
32lib/main.dart
-
10lib/pages/discover/live_room_page.dart
-
192lib/widget/live/draggable_overlay_widget.dart
-
4lib/widget/live/live_recharge_popup.dart
@ -0,0 +1,23 @@ |
|||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
/// 全局 Overlay 控制器 |
||||
|
class OverlayController extends GetxController { |
||||
|
/// overlay 是否显示 |
||||
|
final showOverlay = false.obs; |
||||
|
|
||||
|
/// 显示 overlay |
||||
|
void show() { |
||||
|
showOverlay.value = true; |
||||
|
} |
||||
|
|
||||
|
/// 隐藏 overlay |
||||
|
void hide() { |
||||
|
showOverlay.value = false; |
||||
|
} |
||||
|
|
||||
|
/// 切换 overlay 显示状态 |
||||
|
void toggle() { |
||||
|
showOverlay.value = !showOverlay.value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,192 @@ |
|||||
|
import 'package:agora_rtc_engine/agora_rtc_engine.dart'; |
||||
|
import 'package:dating_touchme_app/controller/discover/room_controller.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:dating_touchme_app/rtc/rtc_manager.dart'; |
||||
|
|
||||
|
/// 可拖拽的 Overlay 小组件 |
||||
|
class DraggableOverlayWidget extends StatefulWidget { |
||||
|
final VoidCallback? onClose; |
||||
|
final Widget? child; |
||||
|
final double size; |
||||
|
final Color backgroundColor; |
||||
|
|
||||
|
const DraggableOverlayWidget({ |
||||
|
super.key, |
||||
|
this.onClose, |
||||
|
this.child, |
||||
|
this.size = 60, |
||||
|
this.backgroundColor = const Color.fromRGBO(0, 0, 0, 0.5), |
||||
|
}); |
||||
|
|
||||
|
@override |
||||
|
State<DraggableOverlayWidget> createState() => _DraggableOverlayWidgetState(); |
||||
|
} |
||||
|
|
||||
|
class _DraggableOverlayWidgetState extends State<DraggableOverlayWidget> { |
||||
|
Offset _position = Offset.zero; |
||||
|
bool _isDragging = false; |
||||
|
final RTCManager _rtcManager = RTCManager.instance; |
||||
|
final RoomController _roomController = Get.find<RoomController>(); |
||||
|
|
||||
|
@override |
||||
|
void initState() { |
||||
|
super.initState(); |
||||
|
// 初始位置设置为右上角 |
||||
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
||||
|
final size = MediaQuery.of(context).size; |
||||
|
setState(() { |
||||
|
_position = Offset( |
||||
|
size.width - 100.w, // 右上角,无间隙(使用100.w因为用户改了size) |
||||
|
100, |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/// 吸附到边缘 |
||||
|
void _snapToEdge(double screenWidth) { |
||||
|
final centerX = screenWidth / 2; |
||||
|
final targetX = _position.dx < centerX |
||||
|
? 0.0 // 吸附到左边,无间隙 |
||||
|
: screenWidth - 100.w; // 吸附到右边,无间隙(使用100.w因为用户改了size) |
||||
|
|
||||
|
// 使用 setState 更新位置,AnimatedPositioned 会自动处理动画 |
||||
|
setState(() { |
||||
|
_position = Offset(targetX, _position.dy); |
||||
|
_isDragging = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/// 构建主播视频视图 |
||||
|
Widget _buildAnchorVideo() { |
||||
|
final engine = _rtcManager.engine; |
||||
|
|
||||
|
if (_roomController.rtcChannelDetail.value?.anchorInfo == null || |
||||
|
engine == null) { |
||||
|
return Container( |
||||
|
decoration: BoxDecoration( |
||||
|
borderRadius: BorderRadius.all(Radius.circular(8.w)), |
||||
|
color: Colors.grey.withOpacity(0.5), |
||||
|
), |
||||
|
child: Center( |
||||
|
child: Text( |
||||
|
'等待主播', |
||||
|
style: TextStyle( |
||||
|
color: Colors.white.withOpacity(0.8), |
||||
|
fontSize: 12.w, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return ClipRRect( |
||||
|
borderRadius: BorderRadius.all(Radius.circular(8.w)), |
||||
|
child: _roomController.currentRole == CurrentRole.broadcaster |
||||
|
? AgoraVideoView( |
||||
|
controller: VideoViewController( |
||||
|
rtcEngine: engine, |
||||
|
canvas: const VideoCanvas(uid: 0), |
||||
|
), |
||||
|
) |
||||
|
: AgoraVideoView( |
||||
|
controller: VideoViewController.remote( |
||||
|
rtcEngine: engine, |
||||
|
canvas: VideoCanvas( |
||||
|
uid: _roomController.rtcChannelDetail.value?.anchorInfo?.uid, |
||||
|
), |
||||
|
connection: RtcConnection( |
||||
|
channelId: _rtcManager.currentChannelId ?? '', |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
@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; |
||||
|
// 限制在屏幕范围内(使用100.w因为用户改了size) |
||||
|
_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(() { |
||||
|
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: [ |
||||
|
// 视频内容 |
||||
|
widget.child ?? _buildAnchorVideo(), |
||||
|
// 右上角关闭按钮 |
||||
|
Positioned( |
||||
|
top: 2.w, |
||||
|
right: 2.w, |
||||
|
child: GestureDetector( |
||||
|
onTap: () async { |
||||
|
await _roomController.leaveChannel(); |
||||
|
widget.onClose?.call(); |
||||
|
}, |
||||
|
child: Container( |
||||
|
width: 20.w, |
||||
|
height: 20.w, |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.black.withOpacity(0.5), |
||||
|
shape: BoxShape.circle, |
||||
|
), |
||||
|
child: Icon(Icons.close, color: Colors.white, size: 14.w), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
}), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save