这是从firestore分页数据的代码,在这里我使用一个名为firebase_ui_firestore ^1.5.15的库从pub.dev,分页工作正常,但这里的问题是当我点击文本字段它自动向上滚动列表,在这里我总是希望列表是底部的聊天应用程序,但对于第一次当我们点击聊天页面它显示列表和滚动列表的底部部分,并按预期工作,但问题只发生在我们点击文本字段或发送任何消息列表向上滚动.
这是程序库问题吗?或者是逻辑错误,请帮助。
在这里我的主要目标列表总是在底部,当我们试图给某人发消息或轻敲键盘或文本字段.在下面我粘贴聊天页面的代码.
class _ChatAppState extends State<ChatApp> {
final TextEditingController _messageController = TextEditingController();
final ChatService _chatService = ChatService();
bool isImageSelected = false;
bool isSendingImage = false;
final ScrollController _scrollController = ScrollController();
XFile? image;
@override
void dispose() {
_messageController.dispose();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
return Scaffold(
backgroundColor: ThemeManager.scaffoldBackgroundColor,
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
ChatScreenAppBar(
senderName: widget.name, avatarUrl: widget.profileUrl),
Expanded(child: _buildMessageList()),
TextField(
onMessageSent: (text) {
sendMessage();
_scrollToBottom();
},
onImageSelected: (selectedImage) async {}, messageController: _messageController,
),
],
),
);
}
/// send chat message.
void sendMessage()async{
try{
if(_messageController.text.isNotEmpty)
{
await _chatService.sendMessage(widget.userId,
widget.currentUserId,
_messageController.text,'recieverEmail','text','gp-01');
_messageController.clear();
}else{
log('its empty');
}
}catch(e)
{
log("send Error: ${e.toString()}");
}
}
_scrollToBottom() {
if(_scrollController.hasClients)
{
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
}
Widget _buildMessageList() {
List<String> ids = [widget.currentUserId, widget.userId];
ids.sort();
String chatRoomId = ids.join("_");
return FirestoreQueryBuilder(
pageSize: 5,
query: FirebaseFirestore.instance
.collection('chat_rooms')
.doc(chatRoomId)
.collection('messages')
.orderBy('TimeStamp',descending: true),
builder: (context, snapshot, index) {
print("currrent index $index");
if (snapshot.hasError) {
return Text('Error ${snapshot.error}');
}
if (snapshot.isFetching) {
return const Center(child: CircularProgressIndicator());
}
print("firebase docs ${snapshot.docs}");
List<Message> allMessages = snapshot.docs.map((doc) {
return Message.fromFireStore(doc);
}).toList();
// Group messages by date
return GroupedListView<Message, DateTime>(
controller: _scrollController,
reverse: true,
order: GroupedListOrder.ASC,
floatingHeader: true,
elements:allMessages.toList(),
groupBy: (message) =>DateTime(
DateTime.parse(message.timeStamp.toDate().toString()).year,
DateTime.parse(message.timeStamp.toDate().toString()).month,
DateTime.parse(message.timeStamp.toDate().toString()).day,
),
itemComparator: (item1, item2) => item1.compareTo(item2),
sort: false, //
groupHeaderBuilder: (Message message) {
final formattedDate =
formatMessageDate(DateTime.parse(message.timeStamp.toDate().toString()));
return SizedBox(
height: 40.h,
child: Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
formattedDate,
style: const TextStyle(color: ThemeManager.primaryBlack),
),
),
),
);
},
itemBuilder: (context, Message message) {
// WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
int messageIndex = allMessages.indexOf(message);
final hasEndReached = snapshot.hasMore &&
messageIndex + 1 == snapshot.docs.length &&
!snapshot.isFetchingMore;
print("has reached the end: $hasEndReached");
if(hasEndReached) {
print("fetch more");
snapshot.fetchMore();
}
String messageId = snapshot.docs[messageIndex].id;
return Align(
alignment: message.receiverId == widget.userId
? Alignment.centerRight : Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 5.h),
child: Column(
children: [
message.receiverId == widget.userId ? Dismissible(
key: UniqueKey(),
confirmDismiss: (direction) async {
bool shouldDelete = await Dialogs.showDeleteConfirmationDialog(context);
return shouldDelete;
},
onDismissed: (direction) async{
_chatService.deleteMessage(widget.currentUserId, widget.userId, messageId);
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
TextMessageContainer(message: message,
senderId: widget.currentUserId,
receiverId:widget.userId,),
SizedBox(height: 5.h),
Text(
DateFormat("hh:mm a").format(DateTime.parse(message.timeStamp.toDate().toString())),
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w400,
color: ThemeManager.secondaryBlack,
),
),
],
),
):Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
TextMessageContainer(message: message,
senderId: widget.currentUserId,
receiverId:widget.userId,),
SizedBox(height: 5.h),
Text(
DateFormat("hh:mm a").format(DateTime.parse(message.timeStamp.toDate().toString())),
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w400,
color: ThemeManager.secondaryBlack,
),
),
],
),
],
),
),
);
}
);
},
);
}
String formatMessageDate(DateTime? dateTime) {
final now = DateTime.now();
final yesterday = now.subtract(const Duration(days: 1));
if (dateTime?.year == now.year &&
dateTime?.month == now.month &&
dateTime?.day == now.day) {
return "Today";
} else if (dateTime?.year == yesterday.year &&
dateTime?.month == yesterday.month &&
dateTime?.day == yesterday.day) {
return "Yesterday";
} else {
return DateFormat.yMMMd().format(dateTime!);
}
}
}
字符串
我不知道有没有这个库或我的代码逻辑的任何问题,我是新来的.我的主要目标是列表总是在底部结束一样的聊天应用程序,当我们点击文本字段或发送消息它总是需要底部.
聊天文本字段类代码
class TextField extends StatefulWidget {
final Function(String)? onMessageSent;
final Function(XFile)? onImageSelected;
final FocusNode? focusNode;
final TextEditingController messageController;
const TextField({super.key, this.onMessageSent, this.onImageSelected,required this.messageController,
this.focusNode
});
@override
State<TextField> createState() => _TextFieldState();
}
class _TextFieldState extends State<TextField> {
XFile? image;
bool isAttached = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
isAttached
? Padding(
padding: EdgeInsets.symmetric(horizontal: 15.w),
child: Column(
children: [
if (isAttached) ...[
SizedBox(height: 10.h),
SizedBox(height: 12.h),
],
],
),
)
: const SizedBox.shrink(),
Container(
height: 72.h,
padding: EdgeInsets.only(left: 15.w, right: 18.w),
decoration: const BoxDecoration(
color: Colors.white,
),
child: Row(
children: [
GestureDetector(
onTap: () {
setState(() {
isAttached = !isAttached;
});
},
child: Image.asset(
"assets/images/icon.png",
width: 22.w,
height: 23.h,
color: isAttached
? Theme.primaryColor
: Theme.inactivateColor,
)),
SizedBox(width: 16.w),
Expanded(
child: TextField(
focusNode: widget.focusNode,
maxLines: null,
controller: widget.messageController,
decoration: InputDecoration(
hintStyle: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w400,
color: ThemeManager.secondaryBlack),
border: InputBorder.none,
),
),
),
GestureDetector(
onTap: () {
final messageText = widget.messageController.text;
if (messageText.isNotEmpty) {
widget.onMessageSent!(messageText);
widget.messageController.clear();
}
},
child: CircleAvatar(
backgroundImage:
const AssetImage("assets/images/ellipse_gradient.png"),
radius: 22.5.r,
child: Image.asset(
"assets/images/send_icon.png",
height: 24.h,
width: 30.w,
),
)),
],
),
),
],
);
}
Widget _buildAttachOption(String text, ImageSource source) {
return GestureDetector(
onTap: () async {
},
child: Container(
padding: EdgeInsets.only(left: 15.w, top: 13, bottom: 13),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: ThemeManager.primaryWhite,
),
child: Row(
children: [
Image.asset("assets/images/images_attach.png"),
SizedBox(width: 20.w),
Text( text,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w400,
color: Theme.primaryBlack),
),
],
),
),
);
}
}
型
消息模型类代码
class Message implements Comparable<Message>{
final String? senderId;
final String? receiverId;
final String? message;
final String? messageType;
final Timestamp timeStamp;
final String? groupId;
Message({
required this.senderId,
required this.receiverId,
required this.message,
required this.messageType,
required this.groupId,
required this.timeStamp,
});
Map<String, dynamic> toMap() {
return {
'senderId': senderId,
'receiverId': receiverId,
'message': message,
'messageType': messageType,
'TimeStamp': timeStamp,
'groupId': groupId,
};
}
// Factory constructor to create a Message instance from a DocumentSnapshot
factory Message.fromFireStore(DocumentSnapshot doc) {
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
return Message(
senderId: data['senderId'],
receiverId: data['receiverId'],
message: data['message'],
messageType: data['messageType'],
timeStamp: data['TimeStamp'],
groupId: data['groupId'],
);
}
factory Message.fromMap(Map<String, dynamic> map) {
return Message(
senderId: map['senderId'] as String,
receiverId: map['receiverId'] as String,
message: map['message'] as String,
messageType: map['messageType'] as String,
timeStamp: map['TimeStamp'] as Timestamp,
groupId: map['groupId'] as String,
);
}
@override
String toString() {
return 'Message{senderId: $senderId, receiverId: $receiverId, message: $message, messageType: $messageType, timeStamp: $timeStamp, groupId: $groupId}';
}
@override
int compareTo(Message other) {
return timeStamp.compareTo(other.timeStamp);
} // converted to map
}
型
2条答案
按热度按时间zwghvu4y1#
我发现解决方案需要从initstate中删除scrollController侦听器,还需要删除_scrollToBottom函数,也不需要控制textfield,列表滚动结束在groupedListview的帮助下与reverse true选项。
字符串
不需要使用scrollToBottom函数,GroupedListview库的reverse true选项将有助于在底部滚动列表。
68de4m5k2#
在
build
方法中,构建小部件时使用WidgetsBinding.instance.addPostFrameCallback(_)
滚动到列表底部。字符串
这意味着聊天从底部(最新消息所在的位置)开始,但它不处理其他交互,如消息发送或文本字段焦点。
由于
_scrollToBottom
已经在多个地方被调用(比如在发送消息之后),我将从build方法中删除自动滚动,以避免在每个帧构建时不必要的滚动。你认为,在你的情况下,键盘的外观是造成滚动问题?
我认为是的,因为当只触摸文本字段时,它会向上滚动。
当键盘出现时,它会更改可用的屏幕空间,这可能会导致可滚动视图调整其位置。在聊天应用程序中,您通常希望打开键盘时最新的消息保持可见。
尝试使用
flutter_keyboard_visibility
package监听键盘可见性的变化。当键盘出现时,调整滚动位置以确保最新的消息保持可见。将
flutter_keyboard_visibility
添加到pubspec.yaml
:型
Listener
是:型
确保
ListView
通过调整其填充来考虑键盘的存在:最后一条消息不会隐藏在键盘后面。型
确保在
TextField
获得焦点时调整滚动位置,以防键盘可见性侦听器没有及时捕获它。型
OP Nns_ninteyFIve在评论中补充说:
我发现解决方案需要从
initstate
中删除scrollController
监听器,还需要删除_scrollToBottom()
功能,在groupedListview
的帮助下列表滚动结束,并将reverse
设置为true
选项。删除
ScrollController
侦听器和_scrollToBottom
函数,同时依赖于GroupedListView
的固有行为,将reverse
设置为true
,这是一种有效的方法,特别是对于最新消息通常显示在底部的聊天应用程序。initState
中从ScrollController
中删除侦听器,可以防止手动滚动控制可能引入的任何其他或不需要的滚动行为。_scrollToBottom
意味着您依赖于列表视图的默认滚动行为,这可能更自然,更不容易出现意外行为,特别是在键盘外观和焦点更改的上下文中。GroupedListView
中将reverse
设置为true
会自动处理滚动,以在列表底部显示最新消息。这是聊天UI中的常见做法,因为它模仿了自然的对话流程。_ChatAppState
类应该是这样的:型
在
_buildMessageList
方法中,使用GroupedListView
,并将reverse
设置为true
:型
通过删除自定义滚动逻辑,这意味着不需要自定义
ScrollController
或_scrollToBottom
方法,因为GroupedListView
负责在底部显示最近的消息。小部件结构得到了简化,从而可能减少与滚动行为相关的问题,特别是在与键盘交互或发送消息时。