如何像WhatsApp一样按日历日期时间分组聊天消息?
实现聊天消息的日期分隔符(类似WhatsApp样式)
要实现一组消息上方显示日期的效果,核心思路是在RecyclerView中插入日期分隔符类型的Item,通过判断消息的时间戳来决定何时显示该分隔符。以下是具体实现步骤:
1. 创建日期分隔符布局
新建布局文件item_date_separator.xml,用于展示日期:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tv_date_separator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingVertical="8dp" android:paddingHorizontal="16dp" android:background="@drawable/rounded_gray_bg" android:textColor="@color/white" android:textSize="12sp" android:layout_gravity="center"/>
2. 定义RecyclerView的Item类型密封类
用密封类区分消息Item和日期分隔符Item:
sealed class ChatItem { data class MessageItem(val data: SingleChatData) : ChatItem() data class DateItem(val date: String) : ChatItem() }
3. 修改适配器核心代码
3.1 更新适配器成员变量
替换原有的messages列表为混合类型的chatItems:
class SingleChatAdapter(private var context: Context, messageList: ArrayList<SingleChatData>?, senderRoom: String, receiverRoom: String, private val incDecNum: (Boolean) -> Unit, private val showChosenToolbar: (Boolean, Boolean) -> Unit): RecyclerView.Adapter<ViewHolder?>() { private var selectedShared = IsSelectedShared(context) private val sentNum = 1 private val receivedNum = 2 private val dateSeparatorNum = 3 // 新增日期分隔符类型 private val senderRoom: String private val receiverRoom: String private lateinit var chatItems: ArrayList<ChatItem> // 替换原messages列表 private val itemsData: ArrayList<SelectedItemsData>?= null // ... 其他原有代码
3.2 处理原始消息列表,插入日期分隔符
在init方法中处理消息列表,自动插入日期分隔符:
init { if (messageList != null){ this.chatItems = processMessageList(messageList) } this.senderRoom = senderRoom this.receiverRoom = receiverRoom } // 处理原始消息列表,插入日期分隔符 private fun processMessageList(messageList: ArrayList<SingleChatData>): ArrayList<ChatItem> { val processedList = ArrayList<ChatItem>() if (messageList.isEmpty()) return processedList // 添加第一条消息的日期分隔符 val firstMsg = messageList[0] processedList.add(ChatItem.DateItem(formatDate(firstMsg.timestamp))) processedList.add(ChatItem.MessageItem(firstMsg)) // 遍历剩余消息,判断是否需要插入日期分隔符 for (i in 1 until messageList.size) { val currentMsg = messageList[i] val previousMsg = messageList[i-1] if (!isSameDay(currentMsg.timestamp, previousMsg.timestamp)) { processedList.add(ChatItem.DateItem(formatDate(currentMsg.timestamp))) } processedList.add(ChatItem.MessageItem(currentMsg)) } return processedList } // 判断两个时间戳是否在同一天 private fun isSameDay(timestamp1: Long, timestamp2: Long): Boolean { val calendar1 = Calendar.getInstance().apply { timeInMillis = timestamp1 } val calendar2 = Calendar.getInstance().apply { timeInMillis = timestamp2 } return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR) && calendar1.get(Calendar.DAY_OF_YEAR) == calendar2.get(Calendar.DAY_OF_YEAR) } // 格式化日期,支持"今天"、"昨天"、"yyyy-MM-dd"三种格式 private fun formatDate(timestamp: Long): String { val currentTime = System.currentTimeMillis() val today = Calendar.getInstance().apply { timeInMillis = currentTime } val yesterday = Calendar.getInstance().apply { timeInMillis = currentTime add(Calendar.DAY_OF_YEAR, -1) } return when { isSameDay(timestamp, currentTime) -> "今天" isSameDay(timestamp, yesterday.timeInMillis) -> "昨天" else -> { val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) sdf.format(Date(timestamp)) } } }
3.3 修改ViewHolder创建逻辑
新增日期分隔符的ViewHolder,并更新onCreateViewHolder:
// 新增日期分隔符ViewHolder inner class DateSeparatorViewHolder(itemView: View) : ViewHolder(itemView) { val tvDate: TextView = itemView.findViewById(R.id.tv_date_separator) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return when(viewType) { sentNum -> { val view = LayoutInflater.from(parent.context).inflate(R.layout.sent_message_content, parent, false) SentMsgViewHolder(view) } receivedNum -> { val view = LayoutInflater.from(parent.context).inflate(R.layout.received_message_content, parent, false) ReceivedMsgViewHolder(view) } dateSeparatorNum -> { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_date_separator, parent, false) DateSeparatorViewHolder(view) } else -> super.onCreateViewHolder(parent, viewType) } }
3.4 更新Item数量和类型判断
override fun getItemCount(): Int { return chatItems.size } override fun getItemViewType(position: Int): Int { return when (chatItems[position]) { is ChatItem.MessageItem -> { val data = (chatItems[position] as ChatItem.MessageItem).data if (FirebaseAuth.getInstance().uid == data.senderId) sentNum else receivedNum } is ChatItem.DateItem -> dateSeparatorNum } }
3.5 绑定数据到ViewHolder
更新onBindViewHolder,分别处理消息和日期分隔符的绑定逻辑:
override fun onBindViewHolder(holder: ViewHolder, position: Int) { when (val item = chatItems[position]) { is ChatItem.MessageItem -> { val data = item.data if (holder is SentMsgViewHolder) { holder.data = data // 绑定发送消息的内容、时间等 holder.binding.sentMsgText.text = data.message holder.binding.sentMsgTime.text = formatMessageTime(data.timestamp) } else if (holder is ReceivedMsgViewHolder) { holder.data = data // 绑定接收消息的内容、时间等 holder.binding.receivedMsgText.text = data.message holder.binding.receivedMsgTime.text = formatMessageTime(data.timestamp) } } is ChatItem.DateItem -> { if (holder is DateSeparatorViewHolder) { holder.tvDate.text = item.date } } } } // 格式化消息时间为时分格式(如"14:30") private fun formatMessageTime(timestamp: Long): String { val sdf = SimpleDateFormat("HH:mm", Locale.getDefault()) return sdf.format(Date(timestamp)) }
4. 优化原有ViewHolder的点击逻辑
修改SentMsgViewHolder和ReceivedMsgViewHolder的点击事件绑定,确保数据非空:
inner class SentMsgViewHolder(itemView: View): ViewHolder(itemView){ var binding = SentMessageContentBinding.bind(itemView) private var onItemClick: ((data: SingleChatData, weakImage: WeakReference<ImageView>) -> Unit)?= null var data: SingleChatData?= null init { itemView.setOnClickListener { data?.let { onItemClick?.invoke(it, WeakReference(binding.sentImg)) } } } fun setOnItemClick(listener: (SingleChatData, WeakReference<ImageView>) -> Unit) { this.onItemClick = listener } } // ReceivedMsgViewHolder做同样修改 inner class ReceivedMsgViewHolder(itemView: View): ViewHolder(itemView){ var binding = ReceivedMessageContentBinding.bind(itemView) private var onItemClick: ((data: SingleChatData, weakImage: WeakReference<ImageView>) -> Unit)?= null var data: SingleChatData?= null init { itemView.setOnClickListener { data?.let { onItemClick?.invoke(it, WeakReference(binding.receivedImg)) } } } fun setOnItemClick(listener: (SingleChatData, WeakReference<ImageView>) -> Unit) { this.onItemClick = listener } }
注意事项
- 确保
SingleChatData类包含timestamp字段(存储消息的毫秒级时间戳) - 日期格式化逻辑可根据需求调整(如添加星期、多语言支持)
- 若消息列表是实时更新的,添加新消息时需重新调用
processMessageList处理数据源,或单独判断新消息与最后一条消息的日期是否相同,按需插入日期分隔符
内容的提问来源于stack exchange,提问作者Arthur Thuma
相关产品推荐
相关产品推荐

