问题描述:我正在 Flutter 中开发一个聊天应用程序,其中使用
ListView.separated
显示消息。我希望消息显示在屏幕底部(就像典型的聊天应用程序一样)。我无法使用内置 reverse: true
属性,因为它会导致不必要地重建音频消息等问题。
目前,我的代码如下所示:
ListView.separated(
controller: controller,
cacheExtent: 1000,
itemCount: state.messages.length,
separatorBuilder: (context, index) {
if (index == state.messages.length - 1) {
return const SizedBox.shrink();
}
final currentMessage = state.messages[index];
final nextMessage = state.messages[index + 1];
final currentDate = currentMessage.timestamp.toLocal();
final nextDate = nextMessage.timestamp.toLocal();
final currentFormattedDate = _formatDate(currentDate);
final nextFormattedDate = _formatDate(nextDate);
return currentFormattedDate != nextFormattedDate
? Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
currentFormattedDate,
style: const TextStyle(color: Colors.grey),
),
),
)
: const SizedBox.shrink();
},
itemBuilder: (context, index) {
final reverseIndex = state.messages.length - 1 - index;
final message = state.messages[reverseIndex];
final isMe = DI.find<Tinode>().isMe(message.from);
final originalMessageOfReply = state.messages.firstWhereOrNull(
(m) => m.seq == message.replySeq);
return GestureDetector(
onTap: () {
if (state.selectedMessageIds.isNotEmpty && message.seq != null) {
context.read<ChatRoomBloc>().add(ToggleMessageSelection(message.seq!));
}
},
onLongPress: () {
_openOptionsOverlay(context, chatMessage: message, state: state);
},
child: Container(
color: state.selectedMessageIds.contains(message.seq)
? Colors.blue.withOpacity(0.2)
: Colors.transparent,
child: MessageBubble(
message: message,
isMe: isMe,
receiver: widget.receiverName,
replyMessage: originalMessageOfReply,
),
),
);
},
)
我尝试过的:
我尝试过使用
reverse: true,
,虽然它可以工作,但它会重建一些组件,例如音频消息,这会导致性能问题。
我尝试手动反转列表,但这并没有将消息放置在底部。
Expected Outcome:
消息应从屏幕底部向上显示,而不使用反向:true,并且不会导致不必要的重建。
Question:
我怎样才能有效地实现这种行为?有没有更好的方法可以将消息粘贴到底部而不反转 ListView?
1。初始化一个ScrollController:
ScrollController _scrollController = ScrollController();
2。消息更新时滚动至底部:
将新消息添加到列表后,滚动到 ListView 的末尾:
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
}
3.更新你的ListView:
将 ScrollController 传递给 ListView.separated
ListView.separated(
controller: _scrollController,
itemCount: messages.length,
itemBuilder: (context, index) {
return MessageWidget(message: messages[index]);
},
separatorBuilder: (context, index) => SizedBox(height: 8),
);
4。有新消息时调用 _scrollToBottom:
每当您向列表添加新消息时,请确保调用 _scrollToBottom 以保持视图更新。
void addMessage(String newMessage) {
setState(() {
messages.add(newMessage);
});
_scrollToBottom();
}
示例:
import 'package:flutter/material.dart';
class ChatScreen extends StatefulWidget {
@override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final ScrollController _scrollController = ScrollController();
final List<Message> messages = [];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Chat")),
body: Column(
children: [
Expanded(
child: ListView.separated(
controller: _scrollController,
itemCount: messages.length,
itemBuilder: (context, index) {
return MessageWidget(message: messages[index]);
},
separatorBuilder: (context, index) => SizedBox(height: 8),
),
),
_MessageInputField(onSend: _addMessage),
],
),
);
}
void _scrollToBottom() {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
}
void _addMessage(String content, MessageType type) {
setState(() {
messages.add(Message(content: content, type: type));
});
_scrollToBottom();
}
}
class _MessageInputField extends StatefulWidget {
final void Function(String, MessageType) onSend;
_MessageInputField({required this.onSend});
@override
State<_MessageInputField> createState() => _MessageInputFieldState();
}
class _MessageInputFieldState extends State<_MessageInputField> {
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(hintText: "Enter message..."),
),
),
IconButton(
icon: Icon(Icons.send),
onPressed: () {
if (_controller.text.trim().isNotEmpty) {
widget.onSend(_controller.text.trim(), MessageType.text);
_controller.clear();
}
},
),
],
),
);
}
}
class MessageWidget extends StatelessWidget {
final Message message;
MessageWidget({required this.message});
@override
Widget build(BuildContext context) {
switch (message.type) {
case MessageType.text:
return _TextMessage(content: message.content);
case MessageType.audio:
return _AudioMessage(content: message.content);
default:
return SizedBox.shrink();
}
}
}
class _TextMessage extends StatelessWidget {
final String content;
_TextMessage({required this.content});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.circular(8),
),
child: Text(content, style: TextStyle(color: Colors.white)),
);
}
}
class _AudioMessage extends StatefulWidget {
final String content;
_AudioMessage({required this.content});
@override
State<_AudioMessage> createState() => _AudioMessageState();
}
class _AudioMessageState extends State<_AudioMessage> {
bool isPlaying = false;
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
icon: Icon(isPlaying ? Icons.pause : Icons.play_arrow),
onPressed: () {
setState(() {
isPlaying = !isPlaying;
});
// Implement actual audio play/pause logic here
},
),
Text(isPlaying ? "Playing..." : "Paused"),
],
);
}
}
enum MessageType { text, audio }
class Message {
final String content;
final MessageType type;
Message({required this.content, required this.type});
}