I'm creating a chat app and for displaying messages I'm using recycler view. Newest messages are displayed on the bottom. The user scrolls up to see more messages.
When the chat screen is loaded the view doesn't start from the very bottom, and the newest message isn't visible. The user has to scroll down a few rows to see the newest message. This is bad UI and the newest message should be visible at the end/bottom of the screen.
I'm using setReverseLayout(true)
and setStackFromEnd(false)
, and I've searched online for similar issues with no luck. For now, I'm setting the scroll position to 0 right after setting up the recycler view with a delay, but this doesn't always work and it's jumpy.
If I set up recyclerview normally(without using setReverseLayout
and setStackFromEnd
), the newest message loads at the top each time perfectly as it should.
Here's the code to start recycler view:
RecyclerView recyclerView = findViewById(R.id.recyclerViewMessageRoom);
adapter = new RA_MessageRoom(this, userFirebaseUid, messagesGroupedByDate, messageUsers);
recyclerView.setAdapter(adapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setReverseLayout(true);
layoutManager.setStackFromEnd(false);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(layoutManager);
// -- Workaround with delay - still doesn't completely work
new Handler().postDelayed(() -> {
recyclerView.scrollToPosition(0);
}, 200);
Anyone who's experienced this issue and knows how to resolve it please share! Thanks.
EDIT (6 JUL 2019):
Here is the recycler adapter's code. As a reminder if I remove the reverse layout settings it works perfectly fine.
RA_MessageRoom:
public class RA_MessageRoom extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = "RA_MessageRoom";
private static final int TYPE_USER = 1;
private static final int TYPE_PARTICIPANT = 2;
private Context context;
private String userFirebaseUid;
private List<MessagesDateGrouper> messagesGroupedByDate;
private HashSet<MessagesUserModel> messageUsers;
public RA_MessageRoom(Context context, String userFirebaseUid, List<MessagesDateGrouper> messagesGroupedByDate, HashSet<MessagesUserModel> messageUsers) {
this.context = context;
this.userFirebaseUid = userFirebaseUid;
this.messagesGroupedByDate = messagesGroupedByDate;
this.messageUsers = messageUsers;
}
@Override
public int getItemViewType(int position) {
if (messagesGroupedByDate.get(position).getViewType() == MessagesDateGrouper.TYPE_CHAT) {
MessageChatItem message = (MessageChatItem) messagesGroupedByDate.get(position);
if (message.getMessages().getSenderFirebaseUid().equals(userFirebaseUid)) {
return TYPE_USER;
} else {
return TYPE_PARTICIPANT;
}
} else {
return messagesGroupedByDate.get(position).getViewType();
}
}
@Override
public int getItemCount() {
return messagesGroupedByDate != null ? messagesGroupedByDate.size() : 0;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final RecyclerView.ViewHolder holder;
View view;
switch (viewType) {
case MessagesDateGrouper.TYPE_DATE:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_message_room_separator, parent, false);
holder = new MessageRoomDateVH(view);
break;
case TYPE_USER:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_message_room_user, parent, false);
holder = new MessageRoomUserVH(view);
break;
case TYPE_PARTICIPANT:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_message_room_participant, parent, false);;
holder = new MessageRoomParticipantVH(view);
break;
default:
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_message_room_user, parent, false);;
holder = new MessageRoomUserVH(view);
break;
}
return holder;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof MessageRoomDateVH) {
MessageDateItem date = (MessageDateItem) messagesGroupedByDate.get(position);
((MessageRoomDateVH)holder).date.setText(date.getDate());
} else if (holder instanceof MessageRoomParticipantVH) {
MessageRoomParticipantVH view = (MessageRoomParticipantVH) holder;
MessageChatItem messageItem = (MessageChatItem) messagesGroupedByDate.get(position);
MessagesModel message = messageItem.getMessages();
view.name.setText(context.getString(R.string.unknown));
view.profileImage.setImageResource(R.drawable.default_profile_image_grey);
for (MessagesUserModel user: messageUsers) {
if (user.getFirebaseId().equals(message.getSenderFirebaseUid())) {
int fallbackImage;
if (user.getMerchant() == null || !user.getMerchant()) {
fallbackImage = R.drawable.default_profile_image_grey;
} else {
fallbackImage = R.drawable.store_profile;
}
GlideApp.with(context)
.load(user.getPhotoThumbUrl())
.placeholder(R.drawable.placeholder)
.fallback(fallbackImage)
.into(view.profileImage);
view.name.setText(user.getName());
break;
}
}
if (message.getImageUrl() != null && !message.getImageUrl().equals("") ) {
view.image.setVisibility(View.VISIBLE);
view.imageSpinner.setVisibility(View.VISIBLE);
view.chat.setVisibility(View.GONE);
view.image.setClipToOutline(true);
GlideApp.with(context)
.load(message.getImageUrl())
.placeholder(R.drawable.placeholder_message)
.fallback(R.drawable.placeholder_message)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
view.imageSpinner.setVisibility(View.GONE);
return false;
}
})
.into(view.image);
} else {
view.chat.setVisibility(View.VISIBLE);
view.image.setVisibility(View.GONE);
view.imageSpinner.setVisibility(View.GONE);
view.chat.setText(message.getMessageText());
}
String time = DateFormatService.messageRoomParseDateToTimeString(message.getDate());
view.date.setText(time);
} else if (holder instanceof MessageRoomUserVH){
MessageRoomUserVH view = (MessageRoomUserVH) holder;
MessageChatItem messageItem = (MessageChatItem) messagesGroupedByDate.get(position);
MessagesModel message = messageItem.getMessages();
if (message.getImageUrl() != null && !message.getImageUrl().equals("") ) {
view.image.setVisibility(View.VISIBLE);
view.imageSpinner.setVisibility(View.VISIBLE);
view.chat.setVisibility(View.GONE);
view.image.setClipToOutline(true);
GlideApp.with(context)
.load(message.getImageUrl())
.placeholder(R.drawable.placeholder_message)
.fallback(R.drawable.placeholder_message)
.listener(new RequestListener<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
view.imageSpinner.setVisibility(View.GONE);
return false;
}
})
.into(view.image);
} else {
view.chat.setVisibility(View.VISIBLE);
view.image.setVisibility(View.GONE);
view.imageSpinner.setVisibility(View.GONE);
view.chat.setText(message.getMessageText());
}
String time = DateFormatService.messageRoomParseDateToTimeString(message.getDate());
view.date.setText(time);
}
}
public class MessageRoomDateVH extends RecyclerView.ViewHolder {
TextView date;
public MessageRoomDateVH(@NonNull View itemView) {
super(itemView);
date = itemView.findViewById(R.id.textMessageRoomDateSection);
}
}
public class MessageRoomParticipantVH extends RecyclerView.ViewHolder {
ImageView profileImage;
TextView name;
TextView chat;
ImageView image;
TextView date;
ProgressBar imageSpinner;
public MessageRoomParticipantVH(@NonNull View itemView) {
super(itemView);
profileImage = itemView.findViewById(R.id.imageMessageRoomParticipantProfile);
name = itemView.findViewById(R.id.textMessageRoomParticipantName);
chat = itemView.findViewById(R.id.textMessageRoomParticipantChat);
image = itemView.findViewById(R.id.imageMessageRoomParticipantImage);
date = itemView.findViewById(R.id.textMessageRoomParticipantDate);
imageSpinner = itemView.findViewById(R.id.progressBarMessageRoomParticipantImage);
}
}
public class MessageRoomUserVH extends RecyclerView.ViewHolder {
TextView chat;
ImageView image;
TextView date;
ProgressBar imageSpinner;
public MessageRoomUserVH(@NonNull View itemView) {
super(itemView);
chat = itemView.findViewById(R.id.textMessageRoomUserChat);
image = itemView.findViewById(R.id.imageMessageRoomUserImage);
date = itemView.findViewById(R.id.textMessageRoomUserDate);
imageSpinner = itemView.findViewById(R.id.progressBarMessageRoomUserImage);
}
}
}
EDIT:
Although the preference was to have stackFromEnd set to false, I ended up changing stackFromEnd from false to true, removed the delay, and kept setting the scroll position as suggested by the accepted answer to resolve this issue.
Updated Working Code:
RecyclerView recyclerView = findViewById(R.id.recyclerViewMessageRoom);
adapter = new RA_MessageRoom(this, userFirebaseUid, messagesGroupedByDate, messageUsers);
recyclerView.setAdapter(adapter);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setReverseLayout(true);
layoutManager.setStackFromEnd(true);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(layoutManager);
recyclerView.scrollToPosition(0);
Thanks for the help!
You will also need to handle message loads on first page load and new messages received to be at bottom of recyclerview:
if (pageLoad) {
list.add(Model)
} else {
// Add to the top of the list (since list is reverse message will come at the bottom)
list.add(0, Model)
}
this is very easy you have to add only single line :-
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setStackFromEnd(true);
recyclerView.setLayoutManager(linearLayoutManager);
Note: The accepted answer misleads you.
There are 4 possibilities regarding LinearLayoutManager
's listing passion.
1.
startStackFromEnd=true
reverseLayout=true
2.
startStackFromEnd=false
reverseLayout=false
3.
startStackFromEnd=true
reverseLayout=false
4.
startStackFromEnd=false //best for chatting
reverseLayout=true //applications
each combination acts differently i don't know what exactly is your requirements, so play around these values, and i'm sure you'll get what you want.
See this answer:
RecyclerView - Reverse Order
And create a setter to your RA_MessageRoom
to update your messagesGroupedByDate
. Something like that:
Collections.reverse(messagesGroupedByDate); // Reverse your dataset like in answer above
adapter.setMessagesGroupedByDate(messagesGroupedByDate); // Update your dataset in adapter
adapter.notifyDataSetChanged(); // Notify your adapter
With this every time new messages arrive your list will be updated. You need to put this snippet in your data fetch and then you can remove your postDelayed handler.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With