Drew Sly
Drew Sly

Reputation: 11

Items vanishing from RecyclerView when scrolled off-screen

So, I am working on a single user text messaging app and the whole thing essentially works, but I'm experiencing an odd error with the conversation view (where the chatting takes place). Essentially, when the RecyclerView extends beyond a certain point, it seems to bug when it reads in the messages. The RecyclerView will expand, but messages won't display, or messages that were displaying will suddenly be invisible. I'm guessing I'm not sure if its how I'm updating the RecyclerView or how I'm loading data into the message blocks. Here is an example of the glitch:

Messages not displaying correctly

I've noticed that while scrolling up and down, the messages that are visible and invisible seem to change around, often leading to large groups of messages being invisible.

Here is the code for the Conversations activity:

package net.whispwriting.mantischat;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.EditText;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.storage.FirebaseStorage;
import com.google.firebase.storage.StorageReference;
import com.google.firebase.storage.UploadTask;
import com.squareup.picasso.Picasso;
import com.theartofdev.edmodo.cropper.CropImage;
import com.theartofdev.edmodo.cropper.CropImageView;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import de.hdodenhof.circleimageview.CircleImageView;

public class Conversation extends AppCompatActivity {

private String userID, name, image;
private DocumentReference userDoc;
private CircleImageView profileIcon;
private FloatingActionButton sendMessageButton;
private FloatingActionButton attachImageButton;
private EditText messageBox;
private FirebaseUser currentUser;
private DatabaseReference rootRef;
private RecyclerView messagesView;
private List<Message> messageList = new ArrayList<>();
private LinearLayoutManager linearLayoutManager;
private MessageAdapter messageAdapter;
private StorageReference imageStorage;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_conversation);
    final Toolbar convToolbar = findViewById(R.id.conversation_bar);
    setSupportActionBar(convToolbar);
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);

    profileIcon = (CircleImageView) findViewById(R.id.profile_image_conv);
    sendMessageButton = (FloatingActionButton) findViewById(R.id.sendMessage);
    attachImageButton = (FloatingActionButton) findViewById(R.id.attachImageButton);
    messageBox = (EditText) findViewById(R.id.message);
    linearLayoutManager = new LinearLayoutManager(this);
    messagesView = (RecyclerView) findViewById(R.id.conversation_recycler);

    messageAdapter = new MessageAdapter(messageList, this, messagesView);

    messagesView.setHasFixedSize(true);
    messagesView.setLayoutManager(linearLayoutManager);
    messagesView.setAdapter(messageAdapter);

    userID = getIntent().getStringExtra("userID");
    name = getIntent().getStringExtra("name");
    image = getIntent().getStringExtra("image");
    currentUser = FirebaseAuth.getInstance().getCurrentUser();
    rootRef = FirebaseDatabase.getInstance().getReference();
    imageStorage = FirebaseStorage.getInstance().getReference();

    Picasso.get().load(image).placeholder(R.drawable.avatar).into(profileIcon);
    getSupportActionBar().setTitle(name);

    messagesView = (RecyclerView) findViewById(R.id.conversation_recycler);

    loadMessages();

    sendMessageButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            sendMessage(messageBox.getText().toString(), "text");
            messageBox.setText("");
        }
    });

    attachImageButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            CropImage.activity()
                    .setGuidelines(CropImageView.Guidelines.ON)
                    .start(Conversation.this);
        }
    });
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
        CropImage.ActivityResult result = CropImage.getActivityResult(data);
        if (resultCode == RESULT_OK) {
            Uri resultUri = result.getUri();
            sendMessage(resultUri.toString(), "image");
        } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
            Exception error = result.getError();
        }
    }
}

private void sendMessage(String message, String type){
    final Map<String, String> messageMap = new HashMap<>();
    DatabaseReference userMessagePush = rootRef.child("messages").child(currentUser.getUid())
            .child(userID).push();
    String pushID = userMessagePush.getKey();

    String currentUserRef = "messages/" + currentUser.getUid() + "/" + userID;
    String chatUserRef = "messages/" + userID + "/" + currentUser.getUid();

    final Map<String, Object> messageUserMap = new HashMap<>();
    messageUserMap.put(currentUserRef + "/" + pushID, messageMap);
    messageUserMap.put(chatUserRef + "/" + pushID, messageMap);

    if (type.equals("text")){
        if (!TextUtils.isEmpty(message)) {
            messageMap.put("message", message);
            messageMap.put("type", "text");
            messageMap.put("from", currentUser.getUid());
            messageMap.put("timestamp", System.currentTimeMillis() + "");
            rootRef.updateChildren(messageUserMap, new DatabaseReference.CompletionListener() {
                @Override
                public void onComplete(@Nullable DatabaseError error, @NonNull DatabaseReference ref) {
                    if (error != null) {
                        Log.w("CHAT", error.getMessage());
                    }
                }
            });
        }
    }else{
        final StorageReference filepath = imageStorage.child("message_images").child(pushID + ".jpg");
        Uri imageUri = Uri.parse(message);
        filepath.putFile(imageUri).addOnCompleteListener(new OnCompleteListener<UploadTask.TaskSnapshot>() {
            @Override
            public void onComplete(@NonNull Task<UploadTask.TaskSnapshot> task) {
                filepath.getDownloadUrl().addOnSuccessListener(new OnSuccessListener<Uri>() {
                    @Override
                    public void onSuccess(@NonNull Uri uri) {
                        messageMap.put("message", uri.toString());
                        messageMap.put("type", "image");
                        messageMap.put("from", currentUser.getUid());
                        messageMap.put("timestamp", System.currentTimeMillis() + "");
                        rootRef.updateChildren(messageUserMap, new DatabaseReference.CompletionListener() {
                            @Override
                            public void onComplete(@Nullable DatabaseError error, @NonNull DatabaseReference ref) {
                                if (error != null) {
                                    Log.w("CHAT", error.getMessage());
                                }
                            }
                        });
                    }
                });
            }
        });
    }
}

public void loadMessages(){
    rootRef.child("messages").child(currentUser.getUid()).child(userID).addChildEventListener(new ChildEventListener() {
        @Override
        public void onChildAdded(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {
            Message message = snapshot.getValue(Message.class);
            messageList.add(message);
            messageAdapter.notifyDataSetChanged();
            //messageAdapter.notifyItemInserted(messageList.size());
            //messageAdapter.notifyItemInserted(messageList.size()-1);
        }

        @Override
        public void onChildChanged(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {

        }

        @Override
        public void onChildRemoved(@NonNull DataSnapshot snapshot) {

        }

        @Override
        public void onChildMoved(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {

        }

        @Override
        public void onCancelled(@NonNull DatabaseError error) {

        }
    });
}
}

This line, messageAdapter.notifyDataSetChanged();, is where the view is updated. I commented out the other notifiers I've tried. Then there's my messages class, where the message views are configured.

package net.whispwriting.mantischat;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.MimeTypeMap;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FirebaseFirestore;
import com.squareup.picasso.NetworkPolicy;
import com.squareup.picasso.Picasso;

import java.util.List;

import de.hdodenhof.circleimageview.CircleImageView;

public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.MessageViewHolder>{

private List<Message> messageList;
private FirebaseUser currentUser;
private Context context;
private RecyclerView messageView;

public MessageAdapter(List<Message> messageList, Context context, RecyclerView messageView){
    this.messageList = messageList;
    currentUser = FirebaseAuth.getInstance().getCurrentUser();
    this.context = context;
    this.messageView = messageView;
}

@Override
public MessageViewHolder onCreateViewHolder(ViewGroup parent, int viewType){
    View v = LayoutInflater.from(parent.getContext())
            .inflate(R.layout.message_single_layout, parent, false);
    return new MessageViewHolder(v);
}

@Override
public void onBindViewHolder(final MessageViewHolder viewHolder, final int i){
    final Message message = messageList.get(i);

    if (message.getFrom().equals(currentUser.getUid())){
        viewHolder.messageText.setVisibility(View.INVISIBLE);
        viewHolder.image.setVisibility(View.INVISIBLE);
        if (message.getType().equals("image")){
            viewHolder.sentMessageText.setVisibility(View.INVISIBLE);
            Picasso.get().load(message.getMessage()).into(viewHolder.sentImage);
            viewHolder.sentImage.setMaxHeight(50);
            viewHolder.sentImage.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Uri uri =  Uri.parse(message.getMessage());
                    Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
                    String mime = "*/*";
                    MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
                    if (mimeTypeMap.hasExtension(
                            mimeTypeMap.getFileExtensionFromUrl(uri.toString())))
                        mime = mimeTypeMap.getMimeTypeFromExtension(
                                mimeTypeMap.getFileExtensionFromUrl(uri.toString()));
                    intent.setDataAndType(uri,mime);
                    context.startActivity(intent);
                }
            });
        }else {
            viewHolder.sentImage.setVisibility(View.INVISIBLE);
            viewHolder.sentMessageText.setText(message.getMessage());
        }
    }else{
        viewHolder.sentMessageText.setVisibility(View.INVISIBLE);
        FirebaseFirestore.getInstance().collection("Users").document(message.getFrom())
                .get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
            @Override
            public void onComplete(@NonNull Task<DocumentSnapshot> task) {
                if (task.isSuccessful()){
                    DocumentSnapshot document = task.getResult();
                    if (document.exists()){
                        Picasso.get().load(document.getString("image"))
                                .networkPolicy(NetworkPolicy.OFFLINE)
                                .placeholder(R.drawable.avatar)
                                .into(viewHolder.image);
                    }
                }
            }
        });
        if (message.getType().equals("image")){
            viewHolder.messageText.setVisibility(View.INVISIBLE);
            Picasso.get().load(message.getMessage()).into(viewHolder.receivedImage);
            viewHolder.receivedImage.setMaxHeight(50);
            viewHolder.receivedImage.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Uri uri =  Uri.parse(message.getMessage());
                    Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
                    String mime = "*/*";
                    MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
                    if (mimeTypeMap.hasExtension(
                            mimeTypeMap.getFileExtensionFromUrl(uri.toString())))
                        mime = mimeTypeMap.getMimeTypeFromExtension(
                                mimeTypeMap.getFileExtensionFromUrl(uri.toString()));
                    intent.setDataAndType(uri,mime);
                    context.startActivity(intent);
                }
            });
        }else {
            viewHolder.receivedImage.setVisibility(View.INVISIBLE);
            viewHolder.messageText.setText(message.getMessage());
        }
    }
}

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

public class MessageViewHolder extends RecyclerView.ViewHolder{

    private TextView messageText, sentMessageText;
    private ImageView receivedImage, sentImage;
    private CircleImageView image;

    public MessageViewHolder(View view){
        super(view);

        messageText = (TextView) view.findViewById(R.id.messageText);
        sentMessageText = (TextView) view.findViewById(R.id.sentMessageText);
        receivedImage = (ImageView) view.findViewById(R.id.receivedImage);
        sentImage = (ImageView) view.findViewById(R.id.sentImage);
        image = (CircleImageView) view.findViewById(R.id.profile_image_message);
    }
}
}

And then, the XML that holds the message view:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="15dp">

<de.hdodenhof.circleimageview.CircleImageView
    android:id="@+id/profile_image_message"
    android:layout_width="50dp"
    android:layout_height="50dp"
    android:layout_alignParentStart="true"
    android:layout_marginStart="0dp"
    tools:src="@drawable/avatar" />

<TextView
    android:id="@+id/messageText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignTop="@id/profile_image_message"
    android:layout_marginStart="14dp"
    android:layout_toEndOf="@id/profile_image_message"
    android:background="@drawable/message_background"
    android:padding="15dp"
    android:paddingTop="6dp"
    android:paddingBottom="6dp"
    android:text="TextView"
    android:textSize="14sp" />

<TextView
    android:id="@+id/sentMessageText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentEnd="true"
    android:layout_marginEnd="0dp"
    android:background="@drawable/current_message_background"
    android:textColor="@android:color/white"
    android:padding="15dp"
    android:paddingTop="6dp"
    android:paddingBottom="6dp"
    android:text="TextView"
    android:textSize="14sp" />

<ImageView
    android:id="@+id/receivedImage"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/messageText"
    android:layout_marginStart="14dp"
    android:layout_toEndOf="@id/profile_image_message"
    android:layout_alignEnd="@id/messageText"
    android:maxHeight="50dp"
    android:maxWidth="50dp"
    android:scaleType="centerCrop" />

<ImageView
    android:id="@+id/sentImage"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/sentMessageText"
    android:layout_alignParentEnd="true"
    android:layout_marginEnd="0dp"
    android:layout_alignStart="@id/sentMessageText"
    android:maxHeight="50dp"
    android:maxWidth="50dp"
    android:scaleType="centerCrop" />

</RelativeLayout>

The XML layout is used like this: Both message displays are side-by-side. If the message was received, the stuff for a sent message is hidden and the stuff for the received message is modify. When a message is sent, the opposite happens. I've been trying to figure out for a few days why this is happening, and I just have no idea. I hoped a second set of eyes might see something I'm missing that could be causing this. Any help with this is appreciated.

Upvotes: 0

Views: 161

Answers (1)

Drew Sly
Drew Sly

Reputation: 11

Well, I figured out what my own problem was. It was the maximum size of the RecyclerView's pool. Here's the fix:

recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 0);

According to the documentation, the first int for viewType. I'm not sure what that means, but 0 seems to work. The second integer is the max count. Setting it to 0 I think disables the limit.

Upvotes: 1

Related Questions