我正在开发一个基于WebSocket的聊天应用程序,在向RV(RecyclerView)添加新的传入消息时遇到了问题。
我有以下适配器的覆盖功能
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): UserChatMessagesAdapter.Holder {
val binding = AdapterUserChatMessagesRvBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return Holder(binding)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bindData(messages[position])
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItemViewType(position: Int): Int {
return position
}
override fun getItemCount() = messages.size
数据来自API调用,由ViewModel通过LiveData
发布。
出于测试目的,在第一次连接时,我只返回10条要显示的消息。然后,当滚动时,我返回下面的10,依此类推
我在从滚动时超过。
添加新消息的问题
问题是,当用户A向用户B(反之亦然)发送消息时,消息显示不正确。
例如,假设我已经显示了消息:M1,M2。。。,M10.
然后用户A发送M11。但它没有显示新的M11,而是显示另一个,比如M3(或者任何其他,我还没有检测到图案)。
适配器显示新消息的方法:
fun newMessage(newMessage: ChatMessageView) {
val index = LAST_MESSAGE_INDEX
messages.add(index, newMessage)
notifyItemInserted(index)
}
调试时,我没有发现任何错误。
newMessage
值为期望的一个和CCD_ 3列表被正确地更新。我这似乎不是数据问题。消息缓存在内存中,因此当重新输入聊天,则不会再次向API请求它们。话虽如此,当我重新进入聊天室,消息显示正确。
我也尝试过:
- 使用
notifyDataSetChanged
- 替换整个
messages
列表 - 平滑滚动条(我在某个地方读到过,这可能是一个与快速用户滚动有关的问题)
- 重写方法
getItemViewType
、getItemId
- 设置
adapter.setHasStableIds(true)
如果有帮助,这是Holder的定义:
inner class Holder(private val binding: AdapterUserChatMessagesRvBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bindData(message: ChatMessageView) {
if (message.isSentByMe) {
binding.userChatMessagesAdapterSentMessage.configure(
SentMessageComponent.Configuration(
message.content,
message.sentMoment.time
)
)
binding.userChatMessagesAdapterSentMessage.visible()
} else {
binding.userChatMessagesAdapterReceivedMessage.configure(
ReceivedMessageComponent.Configuration(
message.content,
message.sentMoment.time
)
)
binding.userChatMessagesAdapterReceivedMessage.visible()
}
configureDateSeparator(message.sentMoment.date)
}
这是我自定义的RecyclerView.OnScrollListener
定义:
internal abstract class PaginationScrollListener(
private val layoutManager: LinearLayoutManager
) : RecyclerView.OnScrollListener() {
abstract fun isLastPage(): Boolean
abstract fun isLoading(): Boolean
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (!isLoading() && !isLastPage()) {
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount
&& firstVisibleItemPosition >= 0) {
loadMoreItems()
}
}
}
abstract fun loadMoreItems()
}
--------编辑-1--------
我添加了相应的XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/adapter_chat_room_main_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="10dp">
<...components.ChatDateSeparatorComponent
android:id="@+id/user_chat_messages_adapter_date_separator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<...components.ReceivedMessageComponent
android:id="@+id/user_chat_messages_adapter_received_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/user_chat_messages_adapter_date_separator" />
<...components.SentMessageComponent
android:id="@+id/user_chat_messages_adapter_sent_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/user_chat_messages_adapter_date_separator" />
</androidx.constraintlayout.widget.ConstraintLayout>
您的bindData
函数根据消息类型使一个组件可见,但不会使另一个组件不可见-因此,如果ViewHolder
用于显示isSentByMe
为true的项目,然后向下滚动并将其重新用于isSentByMe
为false的项目,则userChatMessagesAdapterSentMessage
和userChatMessagesAdapterReceivedMessage
都将可见。
我猜在您的布局中有两个不同的View
——您还没有显示XML,但ViewHolder
的高度可能是固定的(而不是wrap_content
),所以这两个视图中较低的一个在可见区域之外吗?或者一个覆盖另一个,所以你只能看到顶部的那个?
编辑:是的,看,你把它们叠加在一起:
<...components.ChatDateSeparatorComponent
android:id="@+id/user_chat_messages_adapter_date_separator"
app:layout_constraintTop_toTopOf="parent"/>
<...components.ReceivedMessageComponent
android:id="@+id/user_chat_messages_adapter_received_message"
android:layout_width="match_parent"
...
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/user_chat_messages_adapter_date_separator" />
<...components.SentMessageComponent
android:id="@+id/user_chat_messages_adapter_sent_message"
android:layout_width="match_parent"
...
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/user_chat_messages_adapter_date_separator" />
两个的顶部都被约束到固定在顶部的separator
视图的底部,因此在垂直方向上它们处于完全相同的位置。一个被限制在左边,另一个被约束在右边,但它们的宽度都是match_parent
,所以它们填充了整个宽度。
如果这两个MessageComponent
视图都是visible
,则只会看到SentMessage
视图,因为它是最后指定的(绘制在另一个视图的顶部,因为它具有更高的Z阶)。如果ReceivedMessage
的内容明显更多,使其更高(因为它们的高度都是wrap_content
),那么它会从另一个下面向外窥视。
正如我所说,您只是在发布的代码中将它们设置为可见。您从未将另一个设置为不可见/已消失以确保其隐藏。你不能在显示一个或另一个之间切换,你显示一个,然后它保持可见,即使ViewHolder
需要显示另一种类型的消息。因此,根据您的XML,如果发送的消息显示在ViewHolder
中,您将无法看到任何接收的发送内容。
这就是为什么您可能会看到M3而不是M11——因为之前显示M3的VH正在被重用,所以会显示M11,但您无法在旧的M311,它还没有被用来显示发送的消息,所以它看起来不错(但如果你再次上下滚动,东西可能会坏掉)。
如果您不知道,RecyclerView
的目的是重用(回收)少数ViewHolder
,使用它们来显示特定项目的数据。在messages
0中,您将获得其中一个ViewHolder,可能已用于显示其他内容,您将其设置为显示当前项目。
粗体的这一点很重要,因为这意味着VH将具有旧状态,在您的情况下,包括设置为可见的View
。因此,您必须显式设置所有的状态,这样就不会留下旧状态了——假设它是脏的,需要完全设置。也就是说,当您将其中一个View
设置为VISIBLE
时,必须将另一个设置为INVISIBLE
或GONE
。否则,如果它以前是可见的,现在你有两个可见的东西,这是一个问题!
所以你必须这样做:
if (message.isSentByMe) {
binding.userChatMessagesAdapterSentMessage.configure(
SentMessageComponent.Configuration(
message.content,
message.sentMoment.time
)
)
binding.userChatMessagesAdapterSentMessage.visible()
// explicitly hide the other one - I'm assuming you'll write another function
binding.userChatMessagesAdapterReceivedMessage.invisible()
} else {
binding.userChatMessagesAdapterReceivedMessage.configure(
ReceivedMessageComponent.Configuration(
message.content,
message.sentMoment.time
)
)
binding.userChatMessagesAdapterReceivedMessage.visible()
// same as the other branch - you're showing this, make sure the other is hidden
binding.userChatMessagesAdapterSentMessage.invisible()
}