From a7627b172f2c2073baf5331c037740a124d82ce5 Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Tue, 25 Nov 2025 11:27:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(live):=20=E6=B7=BB=E5=8A=A0=E5=8F=AF?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E7=9A=84=E5=85=A8=E5=B1=80=20Overlay=20?= =?UTF-8?q?=E5=B0=8F=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 DraggableOverlayWidget,支持在屏幕上自由拖拽并自动吸附到边缘 - 实现主播视频视图展示逻辑,兼容主播和观众端的不同渲染方式 - 添加全局 Overlay 控制器 OverlayController,用于管理 overlay 的显示/隐藏状态 - 在 main.dart 中集成全局 overlay 组件,并通过 OverlayController 控制其显示 - 修改 LiveRoomPage 页面,在关闭直播间时切换 overlay 显示状态 - 更新 LiveRechargePopup 弹窗,使用 Get.back() 替代 Navigator.pop() - 移除 RoomController 中多余的分号及未使用的 import 和 dispose 逻辑 --- lib/controller/discover/room_controller.dart | 1 - lib/controller/overlay_controller.dart | 23 +++ lib/main.dart | 32 ++- lib/pages/discover/live_room_page.dart | 10 +- lib/widget/live/draggable_overlay_widget.dart | 192 ++++++++++++++++++ lib/widget/live/live_recharge_popup.dart | 4 +- 6 files changed, 254 insertions(+), 8 deletions(-) create mode 100644 lib/controller/overlay_controller.dart create mode 100644 lib/widget/live/draggable_overlay_widget.dart diff --git a/lib/controller/discover/room_controller.dart b/lib/controller/discover/room_controller.dart index c0e8597..ffb85a9 100644 --- a/lib/controller/discover/room_controller.dart +++ b/lib/controller/discover/room_controller.dart @@ -285,7 +285,6 @@ class RoomController extends GetxController { await RTCManager.instance.unpublish(currentRole); } currentRole = CurrentRole.normalUser; - ; await RTCManager.instance.leaveChannel(); } diff --git a/lib/controller/overlay_controller.dart b/lib/controller/overlay_controller.dart new file mode 100644 index 0000000..fe7f851 --- /dev/null +++ b/lib/controller/overlay_controller.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; + } +} + diff --git a/lib/main.dart b/lib/main.dart index 76b227d..e67b7e7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,12 +2,13 @@ import 'dart:io'; import 'package:dating_touchme_app/config/env_config.dart'; import 'package:dating_touchme_app/controller/global.dart'; +import 'package:dating_touchme_app/controller/overlay_controller.dart'; import 'package:dating_touchme_app/im/im_manager.dart'; import 'package:dating_touchme_app/network/network_service.dart'; import 'package:dating_touchme_app/pages/main/main_page.dart'; import 'package:dating_touchme_app/pages/mine/login_page.dart'; -import 'package:dating_touchme_app/pages/mine/user_info_page.dart'; import 'package:dating_touchme_app/rtc/rtc_manager.dart'; +import 'package:dating_touchme_app/widget/live/draggable_overlay_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -31,6 +32,8 @@ void main() async { Get.put(networkService); Get.put(networkService.userApi); Get.put(networkService.homeApi); + // 初始化全局 Overlay 控制器 + Get.put(OverlayController()); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( @@ -70,7 +73,30 @@ void main() async { ), ), ), - builder: FlutterSmartDialog.init(), + builder: (context, child) { + final smartDialogBuilder = FlutterSmartDialog.init(); + return smartDialogBuilder( + context, + Stack( + children: [ + child ?? const SizedBox(), + // 全局 overlay 组件 + Obx(() { + final overlayController = Get.find(); + return overlayController.showOverlay.value + ? DraggableOverlayWidget( + size: 60, + backgroundColor: const Color.fromRGBO(0, 0, 0, 0.6), + onClose: () { + overlayController.hide(); + }, + ) + : const SizedBox.shrink(); + }), + ], + ), + ); + }, home: MyApp(), ), ); @@ -100,7 +126,7 @@ class _MyAppState extends State { doOnIOS: true, universalLink: 'https://your.univerallink.com/link/', ); - var result = await fluwx.isWeChatInstalled; + await fluwx.isWeChatInstalled; } // This widget is the root of your application. diff --git a/lib/pages/discover/live_room_page.dart b/lib/pages/discover/live_room_page.dart index 370848d..ad6f922 100644 --- a/lib/pages/discover/live_room_page.dart +++ b/lib/pages/discover/live_room_page.dart @@ -1,5 +1,5 @@ import 'package:dating_touchme_app/controller/discover/room_controller.dart'; -import 'package:dating_touchme_app/controller/global.dart'; +import 'package:dating_touchme_app/controller/overlay_controller.dart'; import 'package:dating_touchme_app/generated/assets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -8,7 +8,6 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:dating_touchme_app/widget/live/live_room_user_header.dart'; import 'package:dating_touchme_app/widget/live/live_room_anchor_showcase.dart'; -import 'package:dating_touchme_app/widget/live/live_room_seat_list.dart'; import 'package:dating_touchme_app/widget/live/live_room_active_speaker.dart'; import 'package:dating_touchme_app/widget/live/live_room_notice_chat_panel.dart'; import 'package:dating_touchme_app/widget/live/live_room_action_bar.dart'; @@ -25,6 +24,7 @@ class LiveRoomPage extends StatefulWidget { class _LiveRoomPageState extends State { late final RoomController _roomController; + late final OverlayController _overlayController; String message = ''; final TextEditingController _messageController = TextEditingController(); @@ -71,11 +71,11 @@ class _LiveRoomPageState extends State { _roomController = Get.isRegistered() ? Get.find() : Get.put(RoomController()); + _overlayController = Get.find(); } @override void dispose() { - _roomController.leaveChannel(); _messageController.dispose(); super.dispose(); } @@ -171,6 +171,10 @@ class _LiveRoomPageState extends State { userName: userName, popularityText: popularityText, avatarAsset: avatarAsset, + onCloseTap: () { + Get.back(); + _overlayController.toggle(); + }, ); }), SizedBox(height: 7.w), diff --git a/lib/widget/live/draggable_overlay_widget.dart b/lib/widget/live/draggable_overlay_widget.dart new file mode 100644 index 0000000..3b3caa4 --- /dev/null +++ b/lib/widget/live/draggable_overlay_widget.dart @@ -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 createState() => _DraggableOverlayWidgetState(); +} + +class _DraggableOverlayWidgetState extends State { + Offset _position = Offset.zero; + bool _isDragging = false; + final RTCManager _rtcManager = RTCManager.instance; + final RoomController _roomController = Get.find(); + + @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), + ), + ), + ), + ], + ), + ), + ); + }), + ); + } +} diff --git a/lib/widget/live/live_recharge_popup.dart b/lib/widget/live/live_recharge_popup.dart index d672031..0bc1e9e 100644 --- a/lib/widget/live/live_recharge_popup.dart +++ b/lib/widget/live/live_recharge_popup.dart @@ -2,6 +2,8 @@ import 'package:dating_touchme_app/generated/assets.dart'; import 'package:dating_touchme_app/widget/live/live_room_pay_item.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:retrofit/http.dart'; class LiveRechargePopup extends StatelessWidget { const LiveRechargePopup({ @@ -47,7 +49,7 @@ class LiveRechargePopup extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ InkWell( - onTap: () => Navigator.maybePop(context), + onTap: () => Get.back(), child: Icon( Icons.close, size: 14.w,