RecyclerView messes up when scrolling

1.3k Views Asked by At

I never asked any question before but hope you'll get my point. I am making a chat app in which I am using a RecyclerView to show messages. The problem is when I scroll the RecyclerView some of the items disappear from the top and the whole items messes up when I try to add a message it doesn't even scroll to bottom nor added in the ListView.

Here is my RecyclerView:

<android.support.v7.widget.RecyclerView
    android:id="@+id/conversation_recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:layout_above="@id/typingConversationLayout"
    android:layout_below="@id/topLayout_conversation_activity"
    android:layout_marginBottom="-5dp"
    android:paddingBottom="7dp" />

Initializing and setting the RecycerView:

linearLayoutManager = new LinearLayoutManager(this);
    adapter = new ConversationRecyclerViewAdapter();
    conversationRecyclerView.setAdapter(adapter);
    conversationRecyclerView.setLayoutManager(linearLayoutManager);
    linearLayoutManager.setStackFromEnd(true);
    conversationRecyclerView.setHasFixedSize(true);
    conversationRecyclerView.setNestedScrollingEnabled(false);

Here is my Adapter class:

private class ConversationRecyclerViewAdapter
        extends RecyclerView.Adapter<ConversationRecyclerViewAdapter.ConversationViewHolder> {

    @NonNull
    @Override
    public ConversationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int i) {

        Log.d(TAG, "onCreateViewHolder: Users Find started");

        View conversationsView = LayoutInflater.from(parent.getContext()).inflate(
                R.layout.layout_message_received, parent, false);

        return new ConversationViewHolder(conversationsView);
    }

    @Override
    public void onBindViewHolder(@NonNull final ConversationViewHolder holderConversation, int i) {
        Log.d(TAG, "onBindViewHolder: Users Find started at position is " + i);

        final int position = holderConversation.getAdapterPosition();

        if (mOwnUser_1.get(position)) {
            holderConversation.receivedMsgLayout.setVisibility(View.GONE);

            holderConversation.sentProfileImg.setImageResource(mUserProfileImg_2.get(position));
            holderConversation.sentMsg.setText(mUserText_3.get(position));

        } else {

            holderConversation.sentMsgLayout.setVisibility(View.GONE);

            holderConversation.receivedProfileImg.setImageResource(mUserProfileImg_2.get(position));
            holderConversation.receivedMsg.setText(mUserText_3.get(position));

        }

        Log.d(TAG, "onBindViewHolder: completed at " + position);

    }

    @Override
    public int getItemCount() {
        return mOwnUser_1.size();
    }

    public class ConversationViewHolder extends RecyclerView.ViewHolder {

        RelativeLayout receivedMsgLayout, sentMsgLayout;
        EmojiTextView receivedMsg, sentMsg;
        CircleImageView receivedProfileImg, sentProfileImg;

        public ConversationViewHolder(@NonNull View v) {
            super(v);

            receivedMsgLayout = v.findViewById(R.id.received_message_layout);
            sentMsgLayout = v.findViewById(R.id.sent_message_layout);
            receivedMsg = v.findViewById(R.id.received_message_text);
            sentMsg = v.findViewById(R.id.sent_message_text);
            receivedProfileImg = v.findViewById(R.id.received_message_user__profile_image);
            sentProfileImg = v.findViewById(R.id.sent_message_user__profile_image);

        }
    }
}

Here I am adding data to ListView and displaying to the RecyclerView:

sendBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {

            String msg = editText.getText().toString().trim();

            if (TextUtils.isEmpty(msg)) {

                editText.setError("Please add a message");
                editText.requestFocus();

            } else {
                Log.d(TAG, "onClick: send Btn ADDED TEXT.. ");

                mOwnUser_1.add(user);
                mUserProfileImg_2.add(image);
                mUserText_3.add(message);

                editText.setText("");
                editText.requestFocus();

                adapter.notifyItemInserted(mOwnUser_1.size());
                conversationRecyclerView.scrollToPosition(mOwnUser_1.size() - 1);


            }

        }
    });

I don't know what i am doing wrong but it does not seem to work as i wanted.

Update Code:

The three listviews:

private ArrayList<Boolean> mOwnUser_1 = new ArrayList<>();
private ArrayList<Integer> mUserProfileImg_2 = new ArrayList<>();
private ArrayList<String> mUserText_3 = new ArrayList<>();

And the way of adding data to adapter:

mOwnUser_1.add(true);
mUserProfileImg_2.add(R.drawable.boy);
mUserText_3.add(edittext.getText().toString().trim());
adapter.notifyItemInserted(mOwnUser_1.size());
conversationRecyclerView.scrollToPosition(mOwnUser_1.size() - 1);

My Whole Conversation Activity Class:

public class ConversationActivity extends AppCompatActivity {
private static final String TAG = "ConversationActivity";

private EditText editText;
private LinearLayout linearLayout;
private LinearLayoutManager linearLayoutManager;

private ImageView sendBtn;
private ImageView emojiImage;
private View rootView;
private Boolean popUpShown = false;
private Boolean micShown = false;
private ImageView micBtn;
private RelativeLayout micLayout;
private RecyclerView conversationRecyclerView;

// Array Lists for Find USERS
private ArrayList<Boolean> mOwnUser_1 = new ArrayList<>();
private ArrayList<Integer> mUserProfileImg_2 = new ArrayList<>();
private ArrayList<String> mUserText_3 = new ArrayList<>();

private ConversationRecyclerViewAdapter adapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
    Log.d(TAG, "onCreate: started");
    super.onCreate(savedInstanceState);
    EmojiManager.install(new TwitterEmojiProvider());
    setContentView(R.layout.activity_conversation);

    editText = findViewById(R.id.conversationEditText);
    linearLayout = findViewById(R.id.optionsOther);
    emojiImage = findViewById(R.id.emojiIconOther);
    rootView = findViewById(R.id.root_view_conversation);
    micBtn = findViewById(R.id.microphoneBtn);
    micLayout = findViewById(R.id.microphoneLayout);
    conversationRecyclerView = findViewById(R.id.conversation_recyclerView);
    sendBtn = findViewById(R.id.sendBtnConversation);

    if (!(Build.VERSION.SDK_INT >= 21))
        findViewById(R.id.typingConversationLayout).setBackgroundResource(R.drawable.edit_text_conversation_background_below_api);

    sendBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {

            String msg = editText.getText().toString().trim();

            if (TextUtils.isEmpty(msg)) {

                editText.setError("Please add a message");
                editText.requestFocus();

            } else {
                Log.d(TAG, "onClick: send Btn ADDED TEXT.. ");

                addData(true, R.drawable.boy0, msg);
            }

        }
    });

    initConversationArrayList();

}

private void addData(Boolean user, int image, String message) {

    mOwnUser_1.add(user);
    mUserProfileImg_2.add(image);
    mUserText_3.add(message);

    editText.setText("");
    editText.requestFocus();

    adapter.notifyItemInserted(mOwnUser_1.size());
    conversationRecyclerView.scrollToPosition(mOwnUser_1.size() - 1);

}


private void initConversationArrayList() {
    Log.d(TAG, "initConversationArrayList: created");

    mOwnUser_1.add(true);
    mUserProfileImg_2.add(R.drawable.boy0);
    mUserText_3.add("Hello How are you?");


    Log.d(TAG, "initConversationArrayList: completed");

    initConversationRecyclerView();

}

private void initConversationRecyclerView() {
    Log.d(TAG, "initConversationRecyclerView: started");

    linearLayoutManager = new LinearLayoutManager(this);
    adapter = new ConversationRecyclerViewAdapter();
    conversationRecyclerView.setAdapter(adapter);
    conversationRecyclerView.setLayoutManager(linearLayoutManager);
    linearLayoutManager.setStackFromEnd(true);
    conversationRecyclerView.setHasFixedSize(true);
    conversationRecyclerView.setNestedScrollingEnabled(false);

    Log.d(TAG, "initConversationRecyclerView: completed");
}
2

There are 2 best solutions below

8
On BEST ANSWER

Currently I am also working on chat module, let me show you how am I doing this. I am going to show you in steps.

Step 1: make two separate layout for recyclerview items, one for message that has been sent from your side and one for message received from another side.

Step 2 : make two view holders to populate different layout according to your scenario, made in above step, like this:

public class ChatNewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<Chat> chats;

public ChatNewAdapter(List<Chat> chats) {
    this.chats = chats;
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

    if (viewType == 0) {
        View viewSend = (View) LayoutInflater.from(parent.getContext()).inflate(R.layout.item_message_send, parent, false);
        return new ViewHolderSend(viewSend);
    } else {
        View viewReceive = (View) LayoutInflater.from(parent.getContext()).inflate(R.layout.item_message_received, parent, false);
        return new ViewHolderReceive(viewReceive);
    }
}

@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
    switch (holder.getItemViewType()) {
        case 0:
            ViewHolderSend viewHolderSend = (ViewHolderSend) holder;
            viewHolderSend.messageSend.setText(chats.get(position).getMessage());
            break;

        case 1:
            ViewHolderReceive viewHolderReceive = (ViewHolderReceive) holder;
            viewHolderReceive.messageReceived.setText(chats.get(position).getMessage());
            break;
    }
}

@Override
public int getItemCount() {
    return chats.size();
}

@Override
public int getItemViewType(int position) {
    if (chats != null && !chats.get(position).fromAdmin) {
        return 0;
    } else
        return 1;
}

class ViewHolderSend extends RecyclerView.ViewHolder {
    TextView messageSend;

    public ViewHolderSend(View itemView) {
        super(itemView);
        messageSend = (TextView) itemView.findViewById(R.id.messageSend);
    }
}

class ViewHolderReceive extends RecyclerView.ViewHolder {
    TextView messageReceived;

    public ViewHolderReceive(View itemView) {
        super(itemView);
        messageReceived = (TextView) itemView.findViewById(R.id.messageReceived);
    }
}

public int addMessages(Chat chat) {
    chats.add(chat);
    notifyDataSetChanged();
    return chats.size();
}

Step 3 : now in your activity:

public class Test extends AppCompatActivity {

RecyclerView chatList;
RecyclerView.LayoutManager mLayoutManager;
ChatNewAdapter adapter;
ImageView sendButton;
EditText messageEditText;
boolean keyboardUp = false;
boolean isRunning = false;
ArrayList<Chat> chats;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_chat);
    getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
    isRunning = true;
    setUpComponents();
}


public void setUpComponents() {
    chatList = (RecyclerView) findViewById(R.id.chat_list);
    chatList.setHasFixedSize(true);
    mLayoutManager = new LinearLayoutManager(this);
    chatList.setLayoutManager(mLayoutManager);
    messageEditText = (EditText) findViewById(R.id.messageText);
    getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
    sendButton = (ImageView) findViewById(R.id.send);

    adapter = new ChatNewAdapter(chats);
    chatList.setAdapter(adapter);
    chatList.scrollToPosition(chatList.getAdapter().getItemCount() - 1);

    messageEditText.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            if (keyboardShown(messageEditText.getRootView())) {
                Log.d("keyboard", "keyboard UP");

                if (keyboardUp == false) {
                    if (chats.size() > 0)
                        chatList.smoothScrollToPosition(chats.size() + 1);
                    keyboardUp = true;
                }

            } else {
                Log.d("keyboard", "keyboard Down");
                keyboardUp = false;
            }
        }
    });


    sendButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {

            final String message = messageEditText.getText().toString().trim();
            if (!message.equals("")) {
                Chat chat = new Chat();
                String name = message;
                chat.setMessage(name);
                messageEditText.setText("");
                adapter.addMessages(chat);
                chatList.scrollToPosition(chatList.getAdapter().getItemCount() - 1);

            } else {
                Log.d("sending message Error", "error fetching dates");
            }
        }
    });
}

private boolean keyboardShown(View rootView) {

    final int softKeyboardHeight = 100;
    Rect r = new Rect();
    rootView.getWindowVisibleDisplayFrame(r);
    DisplayMetrics dm = rootView.getResources().getDisplayMetrics();
    int heightDiff = rootView.getBottom() - r.bottom;
    return heightDiff > softKeyboardHeight * dm.density;
}

And this is my model class, ignore @PrimaryKey and @Required annotation it just because I am using Realm for local DB. In your case you wont required these annotation.

public class Chat extends RealmObject {

@PrimaryKey
@Required
public Long id;
public boolean fromAdmin;
@Required
public String message;
public int type;
public boolean isRead;
public boolean isSent;
public Date date;

public Long getId() {
    return id;
}

public void setId(Long id) {
    this.id = id;
}

public boolean isFromAdmin() {
    return fromAdmin;
}

public void setFromAdmin(boolean fromAdmin) {
    this.fromAdmin = fromAdmin;
}

public String getMessage() {
    return message;
}

public void setMessage(String message) {
    this.message = message;
}

public int getType() {
    return type;
}

public void setType(int type) {
    this.type = type;
}

public boolean isRead() {
    return isRead;
}

public void setRead(boolean read) {
    isRead = read;
}

public boolean isSent() {
    return isSent;
}

public void setSent(boolean sent) {
    isSent = sent;
}

public Date getDate() {
    return date;
}

public void setDate(Date date) {
    this.date = date;
}

I hope it will be helpful for you, you can ask further if you want to know anything else related to code.

1
On

RecyclerView as the name stands recycles the views. When binding data to a view, you need to ensure you set or reset all views that are touched in the adapter. Messups typically occur when there's data that is set only conditionally for some but not all items.

In particular:

if (mOwnUser_1.get(position)) {
    holderConversation.receivedMsgLayout.setVisibility(View.GONE);

    holderConversation.sentProfileImg.setImageResource(mUserProfileImg_2.get(position));
    holderConversation.sentMsg.setText(mUserText_3.get(position));

} else {

    holderConversation.sentMsgLayout.setVisibility(View.GONE);

    holderConversation.receivedProfileImg.setImageResource(mUserProfileImg_2.get(position));
    holderConversation.receivedMsg.setText(mUserText_3.get(position));

}

Both of these branches will need to reset the other layout back to visible.

Anyway with this kind of two-layout approach you are likely better off by having them as separate view types in your adapter. See How to create RecyclerView with multiple view type?