dart Firebase Firestore数据库聊天应用程序分页当点击消息文本字段时,它会自动向上滚动,而不是向下滚动到列表的末尾

mutmk8jj  于 2024-01-03  发布在  其他
关注(0)|答案(2)|浏览(134)

这是从firestore分页数据的代码,在这里我使用一个名为firebase_ui_firestore ^1.5.15的库从pub.dev,分页工作正常,但这里的问题是当我点击文本字段它自动向上滚动列表,在这里我总是希望列表是底部的聊天应用程序,但对于第一次当我们点击聊天页面它显示列表和滚动列表的底部部分,并按预期工作,但问题只发生在我们点击文本字段或发送任何消息列表向上滚动.
这是程序库问题吗?或者是逻辑错误,请帮助。
在这里我的主要目标列表总是在底部,当我们试图给某人发消息或轻敲键盘或文本字段.在下面我粘贴聊天页面的代码.

  1. class _ChatAppState extends State<ChatApp> {
  2. final TextEditingController _messageController = TextEditingController();
  3. final ChatService _chatService = ChatService();
  4. bool isImageSelected = false;
  5. bool isSendingImage = false;
  6. final ScrollController _scrollController = ScrollController();
  7. XFile? image;
  8. @override
  9. void dispose() {
  10. _messageController.dispose();
  11. _scrollController.dispose();
  12. super.dispose();
  13. }
  14. @override
  15. Widget build(BuildContext context) {
  16. WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
  17. return Scaffold(
  18. backgroundColor: ThemeManager.scaffoldBackgroundColor,
  19. body: Column(
  20. mainAxisAlignment: MainAxisAlignment.start,
  21. children: [
  22. ChatScreenAppBar(
  23. senderName: widget.name, avatarUrl: widget.profileUrl),
  24. Expanded(child: _buildMessageList()),
  25. TextField(
  26. onMessageSent: (text) {
  27. sendMessage();
  28. _scrollToBottom();
  29. },
  30. onImageSelected: (selectedImage) async {}, messageController: _messageController,
  31. ),
  32. ],
  33. ),
  34. );
  35. }
  36. /// send chat message.
  37. void sendMessage()async{
  38. try{
  39. if(_messageController.text.isNotEmpty)
  40. {
  41. await _chatService.sendMessage(widget.userId,
  42. widget.currentUserId,
  43. _messageController.text,'recieverEmail','text','gp-01');
  44. _messageController.clear();
  45. }else{
  46. log('its empty');
  47. }
  48. }catch(e)
  49. {
  50. log("send Error: ${e.toString()}");
  51. }
  52. }
  53. _scrollToBottom() {
  54. if(_scrollController.hasClients)
  55. {
  56. _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
  57. }
  58. }
  59. Widget _buildMessageList() {
  60. List<String> ids = [widget.currentUserId, widget.userId];
  61. ids.sort();
  62. String chatRoomId = ids.join("_");
  63. return FirestoreQueryBuilder(
  64. pageSize: 5,
  65. query: FirebaseFirestore.instance
  66. .collection('chat_rooms')
  67. .doc(chatRoomId)
  68. .collection('messages')
  69. .orderBy('TimeStamp',descending: true),
  70. builder: (context, snapshot, index) {
  71. print("currrent index $index");
  72. if (snapshot.hasError) {
  73. return Text('Error ${snapshot.error}');
  74. }
  75. if (snapshot.isFetching) {
  76. return const Center(child: CircularProgressIndicator());
  77. }
  78. print("firebase docs ${snapshot.docs}");
  79. List<Message> allMessages = snapshot.docs.map((doc) {
  80. return Message.fromFireStore(doc);
  81. }).toList();
  82. // Group messages by date
  83. return GroupedListView<Message, DateTime>(
  84. controller: _scrollController,
  85. reverse: true,
  86. order: GroupedListOrder.ASC,
  87. floatingHeader: true,
  88. elements:allMessages.toList(),
  89. groupBy: (message) =>DateTime(
  90. DateTime.parse(message.timeStamp.toDate().toString()).year,
  91. DateTime.parse(message.timeStamp.toDate().toString()).month,
  92. DateTime.parse(message.timeStamp.toDate().toString()).day,
  93. ),
  94. itemComparator: (item1, item2) => item1.compareTo(item2),
  95. sort: false, //
  96. groupHeaderBuilder: (Message message) {
  97. final formattedDate =
  98. formatMessageDate(DateTime.parse(message.timeStamp.toDate().toString()));
  99. return SizedBox(
  100. height: 40.h,
  101. child: Center(
  102. child: Padding(
  103. padding: const EdgeInsets.all(8),
  104. child: Text(
  105. formattedDate,
  106. style: const TextStyle(color: ThemeManager.primaryBlack),
  107. ),
  108. ),
  109. ),
  110. );
  111. },
  112. itemBuilder: (context, Message message) {
  113. // WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
  114. int messageIndex = allMessages.indexOf(message);
  115. final hasEndReached = snapshot.hasMore &&
  116. messageIndex + 1 == snapshot.docs.length &&
  117. !snapshot.isFetchingMore;
  118. print("has reached the end: $hasEndReached");
  119. if(hasEndReached) {
  120. print("fetch more");
  121. snapshot.fetchMore();
  122. }
  123. String messageId = snapshot.docs[messageIndex].id;
  124. return Align(
  125. alignment: message.receiverId == widget.userId
  126. ? Alignment.centerRight : Alignment.centerLeft,
  127. child: Padding(
  128. padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 5.h),
  129. child: Column(
  130. children: [
  131. message.receiverId == widget.userId ? Dismissible(
  132. key: UniqueKey(),
  133. confirmDismiss: (direction) async {
  134. bool shouldDelete = await Dialogs.showDeleteConfirmationDialog(context);
  135. return shouldDelete;
  136. },
  137. onDismissed: (direction) async{
  138. _chatService.deleteMessage(widget.currentUserId, widget.userId, messageId);
  139. },
  140. child: Column(
  141. crossAxisAlignment: CrossAxisAlignment.end,
  142. children: [
  143. TextMessageContainer(message: message,
  144. senderId: widget.currentUserId,
  145. receiverId:widget.userId,),
  146. SizedBox(height: 5.h),
  147. Text(
  148. DateFormat("hh:mm a").format(DateTime.parse(message.timeStamp.toDate().toString())),
  149. style: TextStyle(
  150. fontSize: 12.sp,
  151. fontWeight: FontWeight.w400,
  152. color: ThemeManager.secondaryBlack,
  153. ),
  154. ),
  155. ],
  156. ),
  157. ):Column(
  158. crossAxisAlignment: CrossAxisAlignment.end,
  159. children: [
  160. TextMessageContainer(message: message,
  161. senderId: widget.currentUserId,
  162. receiverId:widget.userId,),
  163. SizedBox(height: 5.h),
  164. Text(
  165. DateFormat("hh:mm a").format(DateTime.parse(message.timeStamp.toDate().toString())),
  166. style: TextStyle(
  167. fontSize: 12.sp,
  168. fontWeight: FontWeight.w400,
  169. color: ThemeManager.secondaryBlack,
  170. ),
  171. ),
  172. ],
  173. ),
  174. ],
  175. ),
  176. ),
  177. );
  178. }
  179. );
  180. },
  181. );
  182. }
  183. String formatMessageDate(DateTime? dateTime) {
  184. final now = DateTime.now();
  185. final yesterday = now.subtract(const Duration(days: 1));
  186. if (dateTime?.year == now.year &&
  187. dateTime?.month == now.month &&
  188. dateTime?.day == now.day) {
  189. return "Today";
  190. } else if (dateTime?.year == yesterday.year &&
  191. dateTime?.month == yesterday.month &&
  192. dateTime?.day == yesterday.day) {
  193. return "Yesterday";
  194. } else {
  195. return DateFormat.yMMMd().format(dateTime!);
  196. }
  197. }
  198. }

字符串
我不知道有没有这个库或我的代码逻辑的任何问题,我是新来的.我的主要目标是列表总是在底部结束一样的聊天应用程序,当我们点击文本字段或发送消息它总是需要底部.
聊天文本字段类代码

  1. class TextField extends StatefulWidget {
  2. final Function(String)? onMessageSent;
  3. final Function(XFile)? onImageSelected;
  4. final FocusNode? focusNode;
  5. final TextEditingController messageController;
  6. const TextField({super.key, this.onMessageSent, this.onImageSelected,required this.messageController,
  7. this.focusNode
  8. });
  9. @override
  10. State<TextField> createState() => _TextFieldState();
  11. }
  12. class _TextFieldState extends State<TextField> {
  13. XFile? image;
  14. bool isAttached = false;
  15. @override
  16. Widget build(BuildContext context) {
  17. return Column(
  18. children: [
  19. isAttached
  20. ? Padding(
  21. padding: EdgeInsets.symmetric(horizontal: 15.w),
  22. child: Column(
  23. children: [
  24. if (isAttached) ...[
  25. SizedBox(height: 10.h),
  26. SizedBox(height: 12.h),
  27. ],
  28. ],
  29. ),
  30. )
  31. : const SizedBox.shrink(),
  32. Container(
  33. height: 72.h,
  34. padding: EdgeInsets.only(left: 15.w, right: 18.w),
  35. decoration: const BoxDecoration(
  36. color: Colors.white,
  37. ),
  38. child: Row(
  39. children: [
  40. GestureDetector(
  41. onTap: () {
  42. setState(() {
  43. isAttached = !isAttached;
  44. });
  45. },
  46. child: Image.asset(
  47. "assets/images/icon.png",
  48. width: 22.w,
  49. height: 23.h,
  50. color: isAttached
  51. ? Theme.primaryColor
  52. : Theme.inactivateColor,
  53. )),
  54. SizedBox(width: 16.w),
  55. Expanded(
  56. child: TextField(
  57. focusNode: widget.focusNode,
  58. maxLines: null,
  59. controller: widget.messageController,
  60. decoration: InputDecoration(
  61. hintStyle: TextStyle(
  62. fontSize: 14.sp,
  63. fontWeight: FontWeight.w400,
  64. color: ThemeManager.secondaryBlack),
  65. border: InputBorder.none,
  66. ),
  67. ),
  68. ),
  69. GestureDetector(
  70. onTap: () {
  71. final messageText = widget.messageController.text;
  72. if (messageText.isNotEmpty) {
  73. widget.onMessageSent!(messageText);
  74. widget.messageController.clear();
  75. }
  76. },
  77. child: CircleAvatar(
  78. backgroundImage:
  79. const AssetImage("assets/images/ellipse_gradient.png"),
  80. radius: 22.5.r,
  81. child: Image.asset(
  82. "assets/images/send_icon.png",
  83. height: 24.h,
  84. width: 30.w,
  85. ),
  86. )),
  87. ],
  88. ),
  89. ),
  90. ],
  91. );
  92. }
  93. Widget _buildAttachOption(String text, ImageSource source) {
  94. return GestureDetector(
  95. onTap: () async {
  96. },
  97. child: Container(
  98. padding: EdgeInsets.only(left: 15.w, top: 13, bottom: 13),
  99. decoration: BoxDecoration(
  100. borderRadius: BorderRadius.circular(15),
  101. color: ThemeManager.primaryWhite,
  102. ),
  103. child: Row(
  104. children: [
  105. Image.asset("assets/images/images_attach.png"),
  106. SizedBox(width: 20.w),
  107. Text( text,
  108. style: TextStyle(
  109. fontSize: 15.sp,
  110. fontWeight: FontWeight.w400,
  111. color: Theme.primaryBlack),
  112. ),
  113. ],
  114. ),
  115. ),
  116. );
  117. }
  118. }


消息模型类代码

  1. class Message implements Comparable<Message>{
  2. final String? senderId;
  3. final String? receiverId;
  4. final String? message;
  5. final String? messageType;
  6. final Timestamp timeStamp;
  7. final String? groupId;
  8. Message({
  9. required this.senderId,
  10. required this.receiverId,
  11. required this.message,
  12. required this.messageType,
  13. required this.groupId,
  14. required this.timeStamp,
  15. });
  16. Map<String, dynamic> toMap() {
  17. return {
  18. 'senderId': senderId,
  19. 'receiverId': receiverId,
  20. 'message': message,
  21. 'messageType': messageType,
  22. 'TimeStamp': timeStamp,
  23. 'groupId': groupId,
  24. };
  25. }
  26. // Factory constructor to create a Message instance from a DocumentSnapshot
  27. factory Message.fromFireStore(DocumentSnapshot doc) {
  28. Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
  29. return Message(
  30. senderId: data['senderId'],
  31. receiverId: data['receiverId'],
  32. message: data['message'],
  33. messageType: data['messageType'],
  34. timeStamp: data['TimeStamp'],
  35. groupId: data['groupId'],
  36. );
  37. }
  38. factory Message.fromMap(Map<String, dynamic> map) {
  39. return Message(
  40. senderId: map['senderId'] as String,
  41. receiverId: map['receiverId'] as String,
  42. message: map['message'] as String,
  43. messageType: map['messageType'] as String,
  44. timeStamp: map['TimeStamp'] as Timestamp,
  45. groupId: map['groupId'] as String,
  46. );
  47. }
  48. @override
  49. String toString() {
  50. return 'Message{senderId: $senderId, receiverId: $receiverId, message: $message, messageType: $messageType, timeStamp: $timeStamp, groupId: $groupId}';
  51. }
  52. @override
  53. int compareTo(Message other) {
  54. return timeStamp.compareTo(other.timeStamp);
  55. } // converted to map
  56. }

zwghvu4y

zwghvu4y1#

我发现解决方案需要从initstate中删除scrollController侦听器,还需要删除_scrollToBottom函数,也不需要控制textfield,列表滚动结束在groupedListview的帮助下与reverse true选项。

  1. Widget build(BuildContext context) {
  2. // WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom()); - need to remove the code from here.
  3. return Scaffold(
  4. backgroundColor: ThemeManager.scaffoldBackgroundColor,
  5. body: Column(
  6. mainAxisAlignment: MainAxisAlignment.start,
  7. children: [
  8. ChatScreenAppBar(
  9. senderName: widget.name, avatarUrl: widget.profileUrl),
  10. Expanded(child: _buildMessageList()),
  11. TextField(
  12. onMessageSent: (text) {
  13. sendMessage();
  14. // _scrollToBottom(); - need to remove the code from here also
  15. },
  16. onImageSelected: (selectedImage) async {}, messageController: _messageController,
  17. ),
  18. ],
  19. ),
  20. );
  21. }

字符串
不需要使用scrollToBottom函数,GroupedListview库的reverse true选项将有助于在底部滚动列表。

展开查看全部
68de4m5k

68de4m5k2#

build方法中,构建小部件时使用WidgetsBinding.instance.addPostFrameCallback(_)滚动到列表底部。

  1. @override
  2. Widget build(BuildContext context) {
  3. WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom());
  4. // rest of the build method
  5. }

字符串
这意味着聊天从底部(最新消息所在的位置)开始,但它不处理其他交互,如消息发送或文本字段焦点。
由于_scrollToBottom已经在多个地方被调用(比如在发送消息之后),我将从build方法中删除自动滚动,以避免在每个帧构建时不必要的滚动。
你认为,在你的情况下,键盘的外观是造成滚动问题?
我认为是的,因为当只触摸文本字段时,它会向上滚动。
当键盘出现时,它会更改可用的屏幕空间,这可能会导致可滚动视图调整其位置。在聊天应用程序中,您通常希望打开键盘时最新的消息保持可见。
尝试使用flutter_keyboard_visibility package监听键盘可见性的变化。当键盘出现时,调整滚动位置以确保最新的消息保持可见。
flutter_keyboard_visibility添加到pubspec.yaml

  1. dependencies:
  2. flutter_keyboard_visibility: ^5.0.2


Listener是:

  1. import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
  2. class _ChatAppState extends State<ChatApp> {
  3. late StreamSubscription<bool> keyboardSubscription;
  4. @override
  5. void initState() {
  6. super.initState();
  7. keyboardSubscription = KeyboardVisibility.onChange.listen((bool visible) {
  8. if (visible) _scrollToBottomDelayed();
  9. });
  10. }
  11. @override
  12. void dispose() {
  13. keyboardSubscription.cancel();
  14. super.dispose();
  15. }
  16. void _scrollToBottomDelayed() {
  17. Future.delayed(Duration(milliseconds: 300), _scrollToBottom);
  18. }
  19. void _scrollToBottom() {
  20. if (_scrollController.hasClients) {
  21. _scrollController.animateTo(
  22. _scrollController.position.maxScrollExtent,
  23. duration: Duration(milliseconds: 200),
  24. curve: Curves.easeOut,
  25. );
  26. }
  27. }
  28. // Rest of your widget code
  29. }


确保ListView通过调整其填充来考虑键盘的存在:最后一条消息不会隐藏在键盘后面。

  1. Widget _buildMessageList() {
  2. return ListView.builder(
  3. controller: _scrollController,
  4. reverse: true,
  5. padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
  6. itemCount: messages.length,
  7. itemBuilder: (context, index) {
  8. // Message item builder code
  9. },
  10. );
  11. }


确保在TextField获得焦点时调整滚动位置,以防键盘可见性侦听器没有及时捕获它。

  1. final FocusNode _messageFocusNode = FocusNode();
  2. @override
  3. void initState() {
  4. super.initState();
  5. _messageFocusNode.addListener(() {
  6. if (_messageFocusNode.hasFocus) {
  7. _scrollToBottomDelayed();
  8. }
  9. });
  10. }
  11. @override
  12. void dispose() {
  13. _messageFocusNode.dispose();
  14. super.dispose();
  15. }
  16. // Make sure to set the focusNode in your TextField
  17. TextField(
  18. focusNode: _messageFocusNode,
  19. // other TextField properties
  20. ),


OP Nns_ninteyFIve在评论中补充说:
我发现解决方案需要从initstate中删除scrollController监听器,还需要删除_scrollToBottom()功能,在groupedListview的帮助下列表滚动结束,并将reverse设置为true选项。
删除ScrollController侦听器和_scrollToBottom函数,同时依赖于GroupedListView的固有行为,将reverse设置为true,这是一种有效的方法,特别是对于最新消息通常显示在底部的聊天应用程序。

  • 通过在initState中从ScrollController中删除侦听器,可以防止手动滚动控制可能引入的任何其他或不需要的滚动行为。
  • 不使用_scrollToBottom意味着您依赖于列表视图的默认滚动行为,这可能更自然,更不容易出现意外行为,特别是在键盘外观和焦点更改的上下文中。
  • GroupedListView中将reverse设置为true会自动处理滚动,以在列表底部显示最新消息。这是聊天UI中的常见做法,因为它模仿了自然的对话流程。

_ChatAppState类应该是这样的:

  1. class _ChatAppState extends State<ChatApp> {
  2. final TextEditingController _messageController = TextEditingController();
  3. // No need for a custom ScrollController
  4. // final ScrollController _scrollController = ScrollController();
  5. @override
  6. void dispose() {
  7. _messageController.dispose();
  8. // _scrollController.dispose(); // No longer needed
  9. super.dispose();
  10. }
  11. // Remove any method that manually handles scrolling like _scrollToBottom
  12. @override
  13. Widget build(BuildContext context) {
  14. return Scaffold(
  15. // rest of your Scaffold code
  16. body: Column(
  17. children: [
  18. // other widgets
  19. Expanded(child: _buildMessageList()),
  20. // other widgets
  21. ],
  22. ),
  23. );
  24. }
  25. // other methods
  26. }


_buildMessageList方法中,使用GroupedListView,并将reverse设置为true

  1. Widget _buildMessageList() {
  2. // setup for your query and chatRoomId
  3. return FirestoreQueryBuilder(
  4. pageSize: 5,
  5. query: FirebaseFirestore.instance
  6. .collection('chat_rooms')
  7. .doc(chatRoomId)
  8. .collection('messages')
  9. .orderBy('TimeStamp', descending: true),
  10. builder: (context, snapshot, _) {
  11. if (snapshot.hasError) {
  12. return Text('Error: ${snapshot.error}');
  13. }
  14. if (snapshot.isFetching) {
  15. return const Center(child: CircularProgressIndicator());
  16. }
  17. List<Message> allMessages = snapshot.docs.map((doc) => Message.fromFireStore(doc)).toList();
  18. return GroupedListView<Message, DateTime>(
  19. // No need for a custom ScrollController here
  20. // controller: _scrollController,
  21. reverse: true, // That keeps your latest messages at the bottom
  22. elements: allMessages,
  23. groupBy: (message) => DateTime(
  24. message.timeStamp.toDate().year,
  25. message.timeStamp.toDate().month,
  26. message.timeStamp.toDate().day,
  27. ),
  28. // other GroupedListView properties
  29. );
  30. },
  31. );
  32. }


通过删除自定义滚动逻辑,这意味着不需要自定义ScrollController_scrollToBottom方法,因为GroupedListView负责在底部显示最近的消息。
小部件结构得到了简化,从而可能减少与滚动行为相关的问题,特别是在与键盘交互或发送消息时。

展开查看全部

相关问题