Browse Source

feat(live): 添加可拖拽的全局 Overlay 小组件

- 新增 DraggableOverlayWidget,支持在屏幕上自由拖拽并自动吸附到边缘
- 实现主播视频视图展示逻辑,兼容主播和观众端的不同渲染方式
- 添加全局 Overlay 控制器 OverlayController,用于管理 overlay 的显示/隐藏状态
- 在 main.dart 中集成全局 overlay 组件,并通过 OverlayController 控制其显示
- 修改 LiveRoomPage 页面,在关闭直播间时切换 overlay 显示状态
- 更新 LiveRechargePopup 弹窗,使用 Get.back() 替代 Navigator.pop()
- 移除 RoomController 中多余的分号及未使用的 import 和 dispose 逻辑
ios
Jolie 4 months ago
parent
commit
a7627b172f
6 changed files with 254 additions and 8 deletions
  1. 1
      lib/controller/discover/room_controller.dart
  2. 23
      lib/controller/overlay_controller.dart
  3. 32
      lib/main.dart
  4. 10
      lib/pages/discover/live_room_page.dart
  5. 192
      lib/widget/live/draggable_overlay_widget.dart
  6. 4
      lib/widget/live/live_recharge_popup.dart

1
lib/controller/discover/room_controller.dart

@ -285,7 +285,6 @@ class RoomController extends GetxController {
await RTCManager.instance.unpublish(currentRole); await RTCManager.instance.unpublish(currentRole);
} }
currentRole = CurrentRole.normalUser; currentRole = CurrentRole.normalUser;
;
await RTCManager.instance.leaveChannel(); await RTCManager.instance.leaveChannel();
} }

23
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;
}
}

32
lib/main.dart

@ -2,12 +2,13 @@ import 'dart:io';
import 'package:dating_touchme_app/config/env_config.dart'; import 'package:dating_touchme_app/config/env_config.dart';
import 'package:dating_touchme_app/controller/global.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/im/im_manager.dart';
import 'package:dating_touchme_app/network/network_service.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/main/main_page.dart';
import 'package:dating_touchme_app/pages/mine/login_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/rtc/rtc_manager.dart';
import 'package:dating_touchme_app/widget/live/draggable_overlay_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@ -31,6 +32,8 @@ void main() async {
Get.put(networkService); Get.put(networkService);
Get.put(networkService.userApi); Get.put(networkService.userApi);
Get.put(networkService.homeApi); Get.put(networkService.homeApi);
// Overlay
Get.put(OverlayController());
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle( 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<OverlayController>();
return overlayController.showOverlay.value
? DraggableOverlayWidget(
size: 60,
backgroundColor: const Color.fromRGBO(0, 0, 0, 0.6),
onClose: () {
overlayController.hide();
},
)
: const SizedBox.shrink();
}),
],
),
);
},
home: MyApp(), home: MyApp(),
), ),
); );
@ -100,7 +126,7 @@ class _MyAppState extends State<MyApp> {
doOnIOS: true, doOnIOS: true,
universalLink: 'https://your.univerallink.com/link/', universalLink: 'https://your.univerallink.com/link/',
); );
var result = await fluwx.isWeChatInstalled;
await fluwx.isWeChatInstalled;
} }
// This widget is the root of your application. // This widget is the root of your application.

10
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/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:dating_touchme_app/generated/assets.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.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:get/get.dart';
import 'package:dating_touchme_app/widget/live/live_room_user_header.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_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_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_notice_chat_panel.dart';
import 'package:dating_touchme_app/widget/live/live_room_action_bar.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<LiveRoomPage> { class _LiveRoomPageState extends State<LiveRoomPage> {
late final RoomController _roomController; late final RoomController _roomController;
late final OverlayController _overlayController;
String message = ''; String message = '';
final TextEditingController _messageController = TextEditingController(); final TextEditingController _messageController = TextEditingController();
@ -71,11 +71,11 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
_roomController = Get.isRegistered<RoomController>() _roomController = Get.isRegistered<RoomController>()
? Get.find<RoomController>() ? Get.find<RoomController>()
: Get.put(RoomController()); : Get.put(RoomController());
_overlayController = Get.find<OverlayController>();
} }
@override @override
void dispose() { void dispose() {
_roomController.leaveChannel();
_messageController.dispose(); _messageController.dispose();
super.dispose(); super.dispose();
} }
@ -171,6 +171,10 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
userName: userName, userName: userName,
popularityText: popularityText, popularityText: popularityText,
avatarAsset: avatarAsset, avatarAsset: avatarAsset,
onCloseTap: () {
Get.back();
_overlayController.toggle();
},
); );
}), }),
SizedBox(height: 7.w), SizedBox(height: 7.w),

192
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<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),
),
),
),
],
),
),
);
}),
);
}
}

4
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:dating_touchme_app/widget/live/live_room_pay_item.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:retrofit/http.dart';
class LiveRechargePopup extends StatelessWidget { class LiveRechargePopup extends StatelessWidget {
const LiveRechargePopup({ const LiveRechargePopup({
@ -47,7 +49,7 @@ class LiveRechargePopup extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
InkWell( InkWell(
onTap: () => Navigator.maybePop(context),
onTap: () => Get.back(),
child: Icon( child: Icon(
Icons.close, Icons.close,
size: 14.w, size: 14.w,

Loading…
Cancel
Save