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? onMessageTap; @override State createState() => _RoomChatMessagePaneState(); } class _RoomChatMessagePaneState extends State { static const int _maxVisibleMessages = 250; static const Duration _messageBatchWindow = Duration(milliseconds: 16); final ScrollController _controller = ScrollController(); final List _msgList = []; final List _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(); _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 _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.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: [ Text( SCAppLocalizations.of(context)!.scrollToTheBottom, style: TextStyle(fontSize: 10.sp), ), SizedBox(width: 4.w), Icon(Icons.chevron_right, size: 10.w), ], ), ), ), ), ], ), ); } }