You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何像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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 01:45:08