feat: 直播弹幕

This commit is contained in:
guozhigq
2024-08-18 23:30:51 +08:00
parent 0803444d74
commit 91856b5c21
9 changed files with 876 additions and 20 deletions

View File

@ -1,9 +1,12 @@
import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/live/message.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'controller.dart';
@ -16,7 +19,8 @@ class LiveRoomPage extends StatefulWidget {
State<LiveRoomPage> createState() => _LiveRoomPageState();
}
class _LiveRoomPageState extends State<LiveRoomPage> {
class _LiveRoomPageState extends State<LiveRoomPage>
with TickerProviderStateMixin {
final LiveRoomController _liveRoomController = Get.put(LiveRoomController());
PlPlayerController? plPlayerController;
late Future? _futureBuilder;
@ -25,6 +29,9 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
bool isShowCover = true;
bool isPlay = true;
Floating? floating;
final ScrollController _scrollController = ScrollController();
late AnimationController fabAnimationCtr;
bool _shouldAutoScroll = true;
@override
void initState() {
@ -34,6 +41,13 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
}
videoSourceInit();
_futureBuilderFuture = _liveRoomController.queryLiveInfo();
// 监听滚动事件
_scrollController.addListener(_onScroll);
fabAnimationCtr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
value: 0.0,
);
}
Future<void> videoSourceInit() async {
@ -41,12 +55,52 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
plPlayerController = _liveRoomController.plPlayerController;
}
void _onScroll() {
// 反向时,展示按钮
if (_scrollController.position.userScrollDirection ==
ScrollDirection.forward) {
_shouldAutoScroll = false;
fabAnimationCtr.forward();
} else {
_shouldAutoScroll = true;
fabAnimationCtr.reverse();
}
}
// 监听messageList的变化自动滚动到底部
@override
void didChangeDependencies() {
super.didChangeDependencies();
_liveRoomController.messageList.listen((_) {
if (_shouldAutoScroll) {
_scrollToBottom();
}
});
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController
.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
)
.then((value) {
_shouldAutoScroll = true;
// fabAnimationCtr.forward();
});
}
}
@override
void dispose() {
plPlayerController!.dispose();
if (floating != null) {
floating!.dispose();
}
_scrollController.dispose();
fabAnimationCtr.dispose();
super.dispose();
}
@ -80,20 +134,6 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
backgroundColor: Colors.black,
body: Stack(
children: [
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Opacity(
opacity: 0.8,
child: Image.asset(
'assets/images/live/default_bg.webp',
fit: BoxFit.cover,
// width: Get.width,
// height: Get.height,
),
),
),
Obx(
() => Positioned(
left: 0,
@ -106,7 +146,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
.roomInfoH5.value.roomInfo?.appBackground !=
null
? Opacity(
opacity: 0.8,
opacity: 0.6,
child: NetworkImgLayer(
width: Get.width,
height: Get.height,
@ -116,7 +156,15 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
'',
),
)
: const SizedBox(),
: Opacity(
opacity: 0.6,
child: Image.asset(
'assets/images/live/default_bg.webp',
fit: BoxFit.cover,
// width: Get.width,
// height: Get.height,
),
),
),
),
Column(
@ -198,8 +246,45 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
child: videoPlayerPanel,
),
),
const SizedBox(height: 20),
// 显示消息的列表
buildMessageListUI(
context,
_liveRoomController,
_scrollController,
),
// 底部安全距离
SizedBox(
height: MediaQuery.of(context).padding.bottom + 20,
)
],
),
// 定位 快速滑动到底部
Positioned(
right: 20,
bottom: MediaQuery.of(context).padding.bottom + 20,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 2),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: fabAnimationCtr,
curve: Curves.easeInOut,
)),
child: ElevatedButton.icon(
onPressed: () {
_scrollToBottom();
},
icon: const Icon(Icons.keyboard_arrow_down), // 图标
label: const Text('新消息'), // 文字
style: ElevatedButton.styleFrom(
// primary: Colors.blue, // 按钮背景颜色
// onPrimary: Colors.white, // 按钮文字颜色
padding: const EdgeInsets.fromLTRB(14, 12, 20, 12), // 按钮内边距
),
),
),
),
],
),
);
@ -214,3 +299,153 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
}
}
}
Widget buildMessageListUI(
BuildContext context,
LiveRoomController liveRoomController,
ScrollController scrollController,
) {
return Expanded(
child: Obx(
() => MediaQuery.removePadding(
context: context,
removeTop: true,
removeBottom: true,
child: ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black,
Colors.black,
],
stops: [0.0, 0.1, 1.0],
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: ListView.builder(
controller: scrollController,
itemCount: liveRoomController.messageList.length,
itemBuilder: (context, index) {
final LiveMessageModel liveMsgItem =
liveRoomController.messageList[index];
return Padding(
padding: EdgeInsets.only(
top: index == 0 ? 40.0 : 4.0,
bottom: 4.0,
left: 20.0,
right: 20.0,
),
child: Text.rich(
TextSpan(
style: const TextStyle(color: Colors.white),
children: [
TextSpan(
text: '${liveMsgItem.userName}: ',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
),
recognizer: TapGestureRecognizer()
..onTap = () {
// 处理点击事件
print('Text clicked');
},
),
TextSpan(
children: [
...buildMessageTextSpan(context, liveMsgItem)
],
// text: liveMsgItem.message,
),
],
),
),
);
},
),
),
),
),
);
}
List<InlineSpan> buildMessageTextSpan(
BuildContext context,
LiveMessageModel liveMsgItem,
) {
final List<InlineSpan> inlineSpanList = [];
// 是否包含表情包
if (liveMsgItem.emots == null) {
// 没有表情包的消息
inlineSpanList.add(
TextSpan(
text: liveMsgItem.message ?? '',
style: const TextStyle(
shadows: [
Shadow(
offset: Offset(2.0, 2.0),
blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(-1.0, -1.0),
blurRadius: 3.0,
color: Colors.black,
),
],
),
),
);
} else {
// 有表情包的消息 使用正则匹配 表情包用图片渲染
final List<String> emotsKeys = liveMsgItem.emots!.keys.toList();
final RegExp pattern = RegExp(emotsKeys.map(RegExp.escape).join('|'));
liveMsgItem.message?.splitMapJoin(
pattern,
onMatch: (Match match) {
final emoteItem = liveMsgItem.emots![match.group(0)];
if (emoteItem != null) {
inlineSpanList.add(
WidgetSpan(
child: NetworkImgLayer(
width: emoteItem['width'].toDouble(),
height: emoteItem['height'].toDouble(),
type: 'emote',
src: emoteItem['url'],
),
),
);
}
return '';
},
onNonMatch: (String nonMatch) {
inlineSpanList.add(
TextSpan(
text: nonMatch,
style: const TextStyle(
shadows: [
Shadow(
offset: Offset(2.0, 2.0),
blurRadius: 3.0,
color: Colors.black,
),
Shadow(
offset: Offset(-1.0, -1.0),
blurRadius: 3.0,
color: Colors.black,
),
],
),
),
);
return nonMatch;
},
);
}
return inlineSpanList;
}