267 lines
7.8 KiB
Dart
267 lines
7.8 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:yumi/app/constants/sc_screen.dart';
|
||
import 'package:yumi/app_localizations.dart';
|
||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||
|
||
enum RoomChatMessageChannel { all, chat, gift }
|
||
|
||
class RoomChatMessagePane extends StatefulWidget {
|
||
const RoomChatMessagePane({
|
||
super.key,
|
||
required this.channel,
|
||
this.onMessageTap,
|
||
});
|
||
|
||
final RoomChatMessageChannel channel;
|
||
final ValueChanged<SocialChatUserProfile?>? onMessageTap;
|
||
|
||
@override
|
||
State<RoomChatMessagePane> createState() => _RoomChatMessagePaneState();
|
||
}
|
||
|
||
class _RoomChatMessagePaneState extends State<RoomChatMessagePane> {
|
||
static const int _maxVisibleMessages = 250;
|
||
static const Duration _messageBatchWindow = Duration(milliseconds: 16);
|
||
|
||
final ScrollController _controller = ScrollController();
|
||
final List<Msg> _msgList = [];
|
||
final List<Msg> _pendingMessages = [];
|
||
Timer? _messageBatchTimer;
|
||
bool _isAtLatestMessage = true;
|
||
bool _isDisposed = false;
|
||
late RtmProvider _provider;
|
||
late final RoomNewMsgListener _newMsgListener = _onNewMsg;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_controller.addListener(_scrollListener);
|
||
_provider = context.read<RtmProvider>();
|
||
_msgList.addAll(_trimmedInitialMessages(_provider, widget.channel));
|
||
_attachListener(_provider, widget.channel);
|
||
_jumpToLatestMessageAfterLayout();
|
||
}
|
||
|
||
@override
|
||
void didUpdateWidget(covariant RoomChatMessagePane oldWidget) {
|
||
super.didUpdateWidget(oldWidget);
|
||
if (oldWidget.channel == widget.channel) {
|
||
return;
|
||
}
|
||
_detachListener(_provider, oldWidget.channel);
|
||
_messageBatchTimer?.cancel();
|
||
_messageBatchTimer = null;
|
||
_pendingMessages.clear();
|
||
_isAtLatestMessage = true;
|
||
_msgList
|
||
..clear()
|
||
..addAll(_trimmedInitialMessages(_provider, widget.channel));
|
||
_attachListener(_provider, widget.channel);
|
||
_jumpToLatestMessageAfterLayout();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_isDisposed = true;
|
||
_messageBatchTimer?.cancel();
|
||
_pendingMessages.clear();
|
||
_detachListener(_provider, widget.channel);
|
||
_controller.removeListener(_scrollListener);
|
||
_controller.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
List<Msg> _trimmedInitialMessages(
|
||
RtmProvider provider,
|
||
RoomChatMessageChannel channel,
|
||
) {
|
||
final messages = switch (channel) {
|
||
RoomChatMessageChannel.all => provider.roomAllMsgList,
|
||
RoomChatMessageChannel.chat => provider.roomChatMsgList,
|
||
RoomChatMessageChannel.gift => provider.roomGiftMsgList,
|
||
};
|
||
return messages.take(_maxVisibleMessages).toList(growable: false);
|
||
}
|
||
|
||
void _attachListener(RtmProvider provider, RoomChatMessageChannel channel) {
|
||
switch (channel) {
|
||
case RoomChatMessageChannel.all:
|
||
provider.msgAllListener = _newMsgListener;
|
||
return;
|
||
case RoomChatMessageChannel.chat:
|
||
provider.msgChatListener = _newMsgListener;
|
||
return;
|
||
case RoomChatMessageChannel.gift:
|
||
provider.msgGiftListener = _newMsgListener;
|
||
return;
|
||
}
|
||
}
|
||
|
||
void _detachListener(RtmProvider provider, RoomChatMessageChannel channel) {
|
||
switch (channel) {
|
||
case RoomChatMessageChannel.all:
|
||
if (identical(provider.msgAllListener, _newMsgListener)) {
|
||
provider.msgAllListener = null;
|
||
}
|
||
return;
|
||
case RoomChatMessageChannel.chat:
|
||
if (identical(provider.msgChatListener, _newMsgListener)) {
|
||
provider.msgChatListener = null;
|
||
}
|
||
return;
|
||
case RoomChatMessageChannel.gift:
|
||
if (identical(provider.msgGiftListener, _newMsgListener)) {
|
||
provider.msgGiftListener = null;
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
void _scrollListener() {
|
||
final position = _controller.position;
|
||
const tolerance = 10.0;
|
||
final isAtLatest = position.pixels < position.minScrollExtent + tolerance;
|
||
if (isAtLatest != _isAtLatestMessage) {
|
||
_isAtLatestMessage = isAtLatest;
|
||
if (!_isDisposed) {
|
||
setState(() {});
|
||
}
|
||
}
|
||
}
|
||
|
||
void _onNewMsg(Msg msg) {
|
||
if (_isDisposed) {
|
||
return;
|
||
}
|
||
if (msg.groupId == "-1000") {
|
||
_clearMessages();
|
||
return;
|
||
}
|
||
_pendingMessages.add(msg);
|
||
_scheduleMessageBatchFlush();
|
||
}
|
||
|
||
void _clearMessages() {
|
||
_messageBatchTimer?.cancel();
|
||
_messageBatchTimer = null;
|
||
_pendingMessages.clear();
|
||
_isAtLatestMessage = true;
|
||
_msgList.clear();
|
||
setState(() {});
|
||
}
|
||
|
||
void _scheduleMessageBatchFlush() {
|
||
if (_messageBatchTimer?.isActive ?? false) {
|
||
return;
|
||
}
|
||
// 房间消息会成批到达,按一帧合并 setState,避免每条消息都触发布局和滚动。
|
||
_messageBatchTimer = Timer(_messageBatchWindow, _flushPendingMessages);
|
||
}
|
||
|
||
void _flushPendingMessages() {
|
||
_messageBatchTimer = null;
|
||
if (_isDisposed || _pendingMessages.isEmpty) {
|
||
return;
|
||
}
|
||
final messages = List<Msg>.of(_pendingMessages);
|
||
_pendingMessages.clear();
|
||
final shouldStickToLatest = _isAtLatestMessage;
|
||
|
||
setState(() {
|
||
for (final msg in messages) {
|
||
_mergeMessage(msg);
|
||
}
|
||
if (_msgList.length > _maxVisibleMessages) {
|
||
_msgList.removeRange(_maxVisibleMessages, _msgList.length);
|
||
}
|
||
});
|
||
|
||
if (shouldStickToLatest) {
|
||
_jumpToLatestMessageAfterLayout();
|
||
}
|
||
}
|
||
|
||
void _mergeMessage(Msg msg) {
|
||
if (widget.channel == RoomChatMessageChannel.chat &&
|
||
_msgList.contains(msg)) {
|
||
return;
|
||
}
|
||
final existingIndex = _msgList.indexOf(msg);
|
||
if (existingIndex >= 0) {
|
||
_msgList.removeAt(existingIndex);
|
||
}
|
||
_msgList.insert(0, msg);
|
||
}
|
||
|
||
void _jumpToLatestMessageAfterLayout() {
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (_isDisposed) {
|
||
return;
|
||
}
|
||
_jumpToLatestMessage();
|
||
});
|
||
}
|
||
|
||
void _jumpToLatestMessage() {
|
||
if (!_controller.hasClients) {
|
||
return;
|
||
}
|
||
_controller.jumpTo(_controller.position.minScrollExtent);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
backgroundColor: Colors.transparent,
|
||
resizeToAvoidBottomInset: false,
|
||
body: Stack(
|
||
alignment: Alignment.bottomLeft,
|
||
children: [
|
||
ListView.builder(
|
||
reverse: true,
|
||
controller: _controller,
|
||
// 父级已经提供确定高度;不使用 shrinkWrap,避免每批消息重算全部 item 高度。
|
||
itemBuilder:
|
||
(BuildContext context, int index) => MsgItem(
|
||
msg: _msgList[index],
|
||
onClick: (user) => widget.onMessageTap?.call(user),
|
||
),
|
||
itemCount: _msgList.length,
|
||
),
|
||
Visibility(
|
||
visible: !_isAtLatestMessage,
|
||
child: GestureDetector(
|
||
onTap: _jumpToLatestMessage,
|
||
child: Container(
|
||
margin: EdgeInsets.symmetric(horizontal: width(15)),
|
||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(52),
|
||
color: Colors.white,
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: <Widget>[
|
||
Text(
|
||
SCAppLocalizations.of(context)!.scrollToTheBottom,
|
||
style: TextStyle(fontSize: 10.sp),
|
||
),
|
||
SizedBox(width: 4.w),
|
||
Icon(Icons.chevron_right, size: 10.w),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|