example/lib/pages/live/live_page.dart (350 lines of code) (raw):
// Copyright © 2025 Alibaba Cloud. All rights reserved.
//
// Author: keria
// Date: 2025/2/17
// Brief: 直播间页面(横屏)
import 'package:aliplayer_widget/aliplayer_widget_lib.dart';
import 'package:aliplayer_widget_example/pages/link/link_constants.dart';
import 'package:aliplayer_widget_example/constants/page_routes.dart';
import 'package:aliplayer_widget_example/manager/sp_manager.dart';
import 'package:aliplayer_widget_example/pages/link/link_page.dart';
import 'package:aliplayer_widget_example/utils/color_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// 直播间页面(支持横竖屏切换)
///
/// Live Streaming Page with Orientation Support
/// This widget represents a simple live streaming page with:
/// - A top area for the title.
/// - A video area for live streaming.
/// - A scrollable chat area.
/// - A message input box at the bottom.
class LivePage extends StatefulWidget {
/// 是否为竖屏直播间
final bool isPortrait;
const LivePage({super.key, this.isPortrait = false});
@override
State<LivePage> createState() => _LivePageState();
}
class _LivePageState extends State<LivePage> with WidgetsBindingObserver {
/// 播放器组件控制器
late AliPlayerWidgetController _controller;
/// 键盘高度
double _keyboardHeight = 0;
/// 聊天消息列表
final List<String> _chatMessages = [];
/// 输入框控制器
final TextEditingController _messageController = TextEditingController();
/// 聊天区域的滚动控制器
final ScrollController _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return OrientationBuilder(
builder: (context, orientation) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: Colors.black,
body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), // 点击页面任何地方收起键盘
child: _buildContentBody(orientation),
),
);
},
);
}
/// 构建主体内容
Widget _buildContentBody(Orientation orientation) {
if (widget.isPortrait || orientation == Orientation.portrait) {
// 竖屏布局:使用 Stack 实现浮层
return _buildPortraitLayout();
} else {
// 横屏布局:保持原有布局
return _buildPlayWidget();
}
}
/// 构建竖屏布局
Widget _buildPortraitLayout() {
return Stack(
children: [
// 底层:播放器组件
_buildPlayWidget(),
// 顶部区域
Align(
alignment: Alignment.topCenter,
child: _buildTopArea(),
),
// 聊天区域
Positioned(
top: MediaQuery.of(context).size.height / 2,
// 顶部区域高度
bottom: _keyboardHeight > 0 ? _keyboardHeight : 60,
// 键盘弹起时调整底部偏移
left: 0,
right: 0,
child: _buildChatArea(),
),
// 消息输入框
Positioned(
bottom: _keyboardHeight,
left: 0,
right: 0,
child: _buildMessageInput(),
),
],
);
}
/// 构建顶部区域
Widget _buildTopArea() {
return SafeArea(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 返回按钮
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.pop(context), // 退出直播间
),
// 直播间标题和观众人数
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"云小宝的直播间",
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Row(
children: [
Icon(
Icons.visibility,
color: Colors.grey,
size: 16,
),
SizedBox(width: 4),
Text(
"3.75万",
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
],
),
],
),
),
/// 更多选项按钮
IconButton(
icon: const Icon(
Icons.more_vert_rounded,
color: Colors.white,
),
onPressed: _showMoreOptions,
),
],
),
);
}
/// 显示更多选项菜单
void _showMoreOptions() {
showModalBottomSheet(
context: context,
builder: (context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.share),
title: const Text("分享直播间"),
onTap: () => Navigator.pop(context), // 关闭菜单
),
ListTile(
leading: const Icon(Icons.report),
title: const Text("举报"),
onTap: () => Navigator.pop(context), // 关闭菜单
),
],
),
);
},
);
}
/// 构建播放器组件
Widget _buildPlayWidget() {
return AliPlayerWidget(_controller);
}
/// 构建滚动的聊天区域
Widget _buildChatArea() {
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(8.0),
itemCount: _chatMessages.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 16,
backgroundColor: ColorUtil.getMaterialColor(index: index),
),
const SizedBox(width: 8),
Expanded(
child: Text(
_chatMessages[index],
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
],
),
);
},
);
}
/// 构建弹幕发送框
Widget _buildMessageInput() {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
/// 礼物入口按钮
IconButton(
icon: const Icon(Icons.card_giftcard, color: Colors.pink),
onPressed: _showGiftOptions,
),
/// 输入框
Expanded(
child: SizedBox(
height: 48,
child: TextField(
controller: _messageController,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
hintText: "输入弹幕...",
hintStyle: const TextStyle(color: Colors.grey),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: BorderSide.none,
),
),
),
),
),
/// 发送按钮
IconButton(
icon: const Icon(Icons.send, color: Colors.blue),
onPressed: _sendMessage,
),
],
),
),
);
}
/// 显示礼物选项菜单
void _showGiftOptions() {
showModalBottomSheet(
context: context,
builder: (context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.star, color: Colors.yellow),
title: const Text("送星星"),
onTap: () {
Navigator.pop(context); // 关闭菜单
_sendGift("星星");
},
),
ListTile(
leading: const Icon(Icons.favorite, color: Colors.red),
title: const Text("送爱心"),
onTap: () {
Navigator.pop(context); // 关闭菜单
_sendGift("爱心");
},
),
ListTile(
leading: const Icon(Icons.diamond, color: Colors.purple),
title: const Text("送钻石"),
onTap: () {
Navigator.pop(context); // 关闭菜单
_sendGift("钻石");
},
),
],
),
);
},
);
}
/// 发送礼物
void _sendGift(String giftName) {
setState(() {
// 在聊天区域显示礼物消息
_chatMessages.add("[礼物] 送出了一颗 $giftName");
});
// 滚动到底部
_scrollToBottom();
// 收起键盘
FocusScope.of(context).unfocus();
}
/// 添加新消息到聊天列表
void _sendMessage() {
final message = _messageController.text.trim();
if (message.isNotEmpty) {
setState(() {
_chatMessages.add(message);
_messageController.clear();
});
// 滚动到底部
_scrollToBottom();
}
}
/// 滚动到底部 / Scroll to bottom
void _scrollToBottom({bool animated = true}) {
if (!_scrollController.hasClients) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (animated) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} else {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
});
}
/// 初始化状态
/// StatefulWidget 的状态类中第一个被调用的方法,用于初始化状态,可以执行一些一次性的初始化工作
///
/// Called when the state is first created. Used for one-time initialization.
@override
void initState() {
super.initState();
// 初始化播放器组件控制器
_controller = AliPlayerWidgetController(context);
// 获取保存的链接
final linkItemName = widget.isPortrait
? LinkConstants.livePortrait
: LinkConstants.liveLandscape;
final savedLink = SPManager.instance.getString(linkItemName);
// 如果 URL 为空,提示用户并跳转到 LinkPage
if (savedLink == null || savedLink.isEmpty) {
final pageRoute = widget.isPortrait
? PageRoutes.livePortrait
: PageRoutes.liveLandscape;
final linkItem = LinkItem(name: linkItemName, route: pageRoute);
// 显示提示消息
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('请先设置直播链接'),
backgroundColor: Colors.red,
action: SnackBarAction(
label: '去设置',
onPressed: () {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => LinkPage(
linkItems: [
linkItem,
],
),
),
);
},
),
),
);
});
return;
}
// 动态设置屏幕方向
if (widget.isPortrait) {
// 锁定竖屏方向
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
}
// 添加观察者
WidgetsBinding.instance.addObserver(this);
// 设置播放器视频源
final videoSource = VideoSourceFactory.createUrlSource(savedLink);
// 设置播放器组件数据
final data = AliPlayerWidgetData(
sceneType: SceneType.live,
videoSource: videoSource,
);
_controller.configure(data);
}
/// 清理资源
/// 在 StatefulWidget 被从树中移除并销毁时调用的,这个方法用于清理资源。
///
/// Called when the widget is removed from the tree permanently. Used to release resources.
@override
void dispose() {
// 释放输入框资源
_messageController.dispose();
// 释放滚动控制器
_scrollController.dispose();
// 销毁播放控制器
_controller.destroy();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
/// 监听键盘高度变化
@override
void didChangeMetrics() {
// 获取键盘高度
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
setState(() {
_keyboardHeight = bottomInset;
});
}
}