简体   繁体   中英

The RecyclerView scrolls to downward when a new message is added

I have a chat app. I used RecyclerView and I set stackFromEnd true and reverseLayout false to LinearlayoutManager. when a new message is added at the bottom I mean at the end of the list the recyclerView starts auto scroll downward instead of upward. the adapter is notified by notifyItemInserted().

Expected Behaviour: When a new message is added to the list it should scroll to the bottom or upward. Any help is appreciated. Thanks,

Here is adapter:


class ChatMessagesListAdapter(
    var chatMessages: ChatMessagesDataModel
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    companion object {
        // we set the header as 0 so we can add more types to the ConversationItem enum
        private const val ITEM_TYPE_HEADER = 0
    }


    override fun getItemViewType(position: Int): Int {
        return when {
            chatMessages.hasMoreItems -> chatMessages.messages[position].getItemType()
            position == 0 -> ITEM_TYPE_HEADER
            else -> chatMessages.messages[position - 1].getItemType()
        }
    }

    // return +1 to draw the header if there are no items to load
    override fun getItemCount() = chatMessages.messages.size + getContentIndex()

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val inflater = LayoutInflater.from(parent.context)

        return when (viewType) {
            ITEM_TYPE_HEADER -> {
                MessagesHeaderViewHolder(inflater.inflate(R.layout.item_chat_messages_header,
                    parent, false))
            }

            ChatItemDataModel.TYPE_ADDED_USER,
            ChatItemDataModel.TYPE_VIDEO_NOTIFICATION -> {
                MessagesInfoViewHolder(inflater.inflate(R.layout.item_chat_messages_info,
                    parent, false))
            }
            else -> {
                MessagesMessageViewHolder(inflater.inflate(R.layout.item_chat_messages_message,
                    parent, false))
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        (holder as? MessagesHeaderViewHolder)?.bind()
        (holder as? MessagesMessageViewHolder)?.bind(position, chatMessages.messages[getRealPosition(position)])
        (holder as? MessagesInfoViewHolder)?.bind(chatMessages.messages[getRealPosition(position)])
    }

    private fun getContentIndex() = if (chatMessages.hasMoreItems) {
        0
    } else {
        1
    }

    private fun getRealPosition(position: Int) = if (chatMessages.hasMoreItems) {
        position
    } else {
        position - 1
    }

    private fun notifyChanges() {
        if (chatMessages.numberOfItemsInserted == chatMessages.messages.size || isPreview ||
            chatMessages.hadPendingMessages) {
            notifyDataSetChanged()
        } else {
            // +1 because of the header
            notifyItemRangeInserted(chatMessages.insertionIndex + getContentIndex(),
                chatMessages.numberOfItemsInserted)
        }
    }

    fun updateConversation(newChatMessages: ChatMessagesDataModel) {
        chatMessages = newChatMessages
        notifyChanges()
    }

    fun updateMessage(newMessage: ChatItemDataModel, isRemote: Boolean) {
        if (newMessage.message.hashIdentifier.isNullOrBlank()) {
            addNewMessage(newMessage)
            return
        }
        val messageIndex = chatMessages.messages.indexOfFirst { it.message.hashIdentifier == newMessage.message.hashIdentifier }

        if (messageIndex != -1) {
            val localMessage =  chatMessages.messages[messageIndex]
            chatMessages.messages[messageIndex] = newMessage

            if (failedMessages.contains(localMessage.message.sid)) {
                if (isRemote) {
                    failedMessages.remove(localMessage.message.sid)
                }
                notifyItemChanged(messageIndex + getContentIndex())
            }
        }
        else {
            addNewMessage(newMessage)
        }
    }

    private fun addNewMessage(newMessage: ChatItemDataModel) {
        val oldCount = chatMessages.messages.size
        chatMessages.messages.add(newMessage)
        notifyItemInserted(oldCount + getContentIndex())
    }

    fun addLocalMessage(
        sharedPrefsStorage: SharedPrefsStorage,
        message: String, imageUri: Uri?, hashIdentifier: String
    ) {
        val userMessage = UserMessage(messageBody = message, firstName = sharedPrefsStorage.firstName,
            lastName = sharedPrefsStorage.lastName, isFromLoggedUser = true, imageUri = imageUri,
            hashIdentifier = hashIdentifier, files = null, reactions = null)

        val newMessage = ChatItemDataModel(userMessage, sharedPrefsStorage.profileImageUrl, sharedPrefsStorage.userId.toString())
        val oldCount = chatMessages.messages.size
        chatMessages.messages.add(newMessage)
        notifyItemRangeInserted(oldCount + getContentIndex(), 1)
    }
           ....
}

Here is Fragment:

class ChatRoomMessagesFragment : Fragment() {

    @Inject
    lateinit var sharedPrefsStorage: SharedPrefsStorage

    private var adapter: ChatMessagesListAdapter? = null

           ......
override fun onMessagesReceived(chatMessages: ChatMessagesDataModel) {
        if (adapter == null) {
            adapter = ChatMessagesListAdapter(chatMessages)

            adapter?.setHasStableIds(true)
            binding.recyclerview.adapter = adapter
        } else {
            adapter?.updateConversation(chatMessages)
        }
    }

override fun onUserMessageRetrieved(newMessage: ChatItemDataModel, isRemote: Boolean) {
        adapter?.updateMessage(newMessage, isRemote)
    }

    private fun setupUI() {

        binding.apply {

            recyclerview.apply {
                layoutManager = LinearLayoutManager(requireContext()).apply {
                    reverseLayout = false
                    stackFromEnd = true
                    addOnScrollListener(object : RecyclerView.OnScrollListener() {
                        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                            super.onScrolled(recyclerView, dx, dy)
                            showScrollToBottomButtonIfNeeded(findFirstCompletelyVisibleItemPosition())
                        }
                    })
                }

                addOnScrollListener(object : PaginationScrollListener(
                    layoutManager as LinearLayoutManager,
                    ScrollDirection.BOTTOM
                ) {

                    override fun loadMoreItems() {
                        presenter.loadMoreMessages()
                    }

                    override fun isLastPage() = !presenter.hasMoreItems()

                    override fun isLoading() = presenter.isLoadingInProgress()
                })

                // prevent the list from scrolling when the keyboard opens
                addOnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom ->
                    if (bottom < oldBottom) {
                        scrollChatToBottom()
                    }
                }
            }
       ....
                adapter?.addLocalMessage(sharedPrefsStorage, message, imageUri, hashIdentifier)
                presenter.sendMessage("", message, imageUri, hashIdentifier)
}

 private fun scrollChatToBottom() {
        binding.apply {
            recyclerview.post {
                recyclerview.smoothScrollToPosition(recyclerview.adapter?.itemCount ?: 0)
            }
        }
    }



}

You can scrolldown progrmatically using code below

recyclerView.scrollToPosition(chatList.size() - 1);

You can also try another solution if above one doesn't works

setStackFromEnd(true) or setReverseLayout(true)

Let's suppose your adapter is populated by objects called messages and you have a source of data called messageList (could be a Collection, etc) that is passed to your adapter. You could do something like this:

        int position = messageList.size() > 0 ? (messageList.size()-1) : 0;
        mRecyclerView.smoothScrollToPosition(position);

Just ensure your adapter is not null and actually gets data from messageList since this is in your Activity/Fragment class and not in the adapter class.

 yourAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
            override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
                super.onItemRangeInserted(positionStart, itemCount)
                this@EventDetails.binding.rvData.scrollToPosition(positionStart)
            }

            override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
                super.onItemRangeChanged(positionStart, itemCount)
                this@EventDetails.binding.rvData.scrollToPosition(positionStart)
            }
        })

one of these method will work i think 1st one, try and try to avoid notifyDataSetChange() or intemChange() call by your self use DiffUtils or ListAdapter(Extension of recycler view)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM