web-dev-qa-db-fra.com

Comportement étrange des images dans RecyclerView

J'utilise Android.support.v7.widget.RecyclerView pour afficher des éléments simples contenant du texte et, dans certains cas, également des images. Fondamentalement, j'ai suivi le tutoriel à partir d'ici https://developer.Android.com/training/material/lists-cards.html et je viens de changer le type de mDataset de String [] en JSONArray et le xml de le ViewHolder. Mon RecyclerView est situé dans un simple fragment:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:orientation="vertical"
    Android:layout_width="match_parent"
    Android:layout_height="match_parent"
    Android:paddingBottom="@dimen/activity_vertical_margin"
    Android:paddingLeft="@dimen/activity_horizontal_margin"
    Android:paddingRight="@dimen/activity_horizontal_margin"
    Android:paddingTop="@dimen/activity_vertical_margin">

<!-- A RecyclerView with some commonly used attributes -->
<Android.support.v7.widget.RecyclerView
    Android:id="@+id/my_hopps_recycler_view"
    Android:scrollbars="vertical"
    Android:layout_centerInParent="true"
    Android:layout_width="200dp"
    Android:layout_height="wrap_content"/>

</RelativeLayout>

Dans ce fragment, je charge toutes les informations d'un serveur Web. Après avoir reçu les informations, j'initialise les éléments dans RecyclerView. J'utilise un LinearLayoutManager qui est défini dans la méthode onCreate de mon fragment. Après avoir ajouté l'adaptateur, j'appelle la méthode setHasFixedSize (true) sur mon RecyclerView. Ma classe d'adaptateur ressemble à ceci:

import Android.graphics.BitmapFactory;
import Android.support.v7.widget.RecyclerView;
import Android.util.Base64;
import Android.util.Log;
import Android.view.LayoutInflater;
import Android.view.View;
import Android.view.ViewGroup;
import Android.widget.ImageButton;
import Android.widget.ImageView;
import Android.widget.LinearLayout;
import Android.widget.TextView;

import org.Apache.commons.lang3.StringEscapeUtils;
import org.json.JSONArray;
import org.json.JSONObject;

import saruul.julian.MyFragment;
import saruul.julian.R;

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {

/**
* Fragment holding the RecyclerView.
*/
private MyFragment myFragment;
/**
* The data to display.
*/
private JSONArray mDataset;

public static class ViewHolder extends RecyclerView.ViewHolder {

    public TextView text;
    ImageView image;

    public ViewHolder(View v) {
        super(v);
        text = (TextView) v.findViewById(R.id.my_view_text);
        image = (ImageView) v.findViewById(R.id.my_view_image);
    }
}

public MyAdapter(MyFragment callingFragment, JSONArray myDataset) {
    myFragment = callingFragment;
    mDataset = myDataset;
}

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

// Replace the contents of a view (invoked by the layout manager)
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
    try {
        JSONObject object = mDataset.getJSONObject(position);
        if ( object.has("image") ){
            if ( !hopp.getString("image").equals("") ){
                try {
                    byte[] decodedString = Base64.decode(StringEscapeUtils.unescapeJson(object.getString("image")), Base64.DEFAULT);
                    holder.image.setImageBitmap(BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length));
                    holder.image.setVisibility(View.VISIBLE);
                } catch ( Exception e ){
                    e.printStackTrace();
                }
            } 
        }
        if ( object.has("text") ){
            holder.text.setText(object.getString("text"));
        }
    } catch ( Exception e ){
        e.printStackTrace();
    }
}

// Return the size of your dataset (invoked by the layout manager)
@Override
public int getItemCount() {
    return mDataset.length();
}
}

Et mon xml pour une vue à l'intérieur de mon RecyclerView:

<Android.support.v7.widget.CardView
xmlns:Android="http://schemas.Android.com/apk/res/Android"
xmlns:card_view="http://schemas.Android.com/apk/res-auto"
Android:id="@+id/card_view"
Android:layout_gravity="center"
Android:layout_width="match_parent"
Android:layout_height="wrap_content"
card_view:cardCornerRadius="4dp">

<LinearLayout
    Android:layout_width="match_parent"
    Android:layout_height="wrap_content"
    Android:gravity="center"
    Android:orientation="vertical"
    Android:padding="@dimen/activity_horizontal_margin">

    <TextView
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content"
        Android:gravity="center"
        Android:id="@+id/my_hopp_people_reached"/>

    <ImageView
        Android:id="@+id/my_hopp_image"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:visibility="gone"
        Android:adjustViewBounds="true"/>

    <TextView
        Android:layout_width="match_parent"
        Android:layout_height="wrap_content"
        Android:gravity="center"
        Android:id="@+id/my_hopp_text"/>

    <ImageButton
        Android:id="@+id/my_hopp_delete"
        Android:layout_width="wrap_content"
        Android:layout_height="wrap_content"
        Android:background="@null"
        Android:src="@drawable/ic_action_discard"
        />

</LinearLayout>

</Android.support.v7.widget.CardView>

Alors maintenant, voici mon problème: tout fonctionne bien. Le texte et l'image sont affichés correctement dans ma RecyclerView. Cependant, si j'atteins la fin de la liste en faisant défiler vers le bas et à nouveau vers le haut, les images sont affichées dans des éléments qui ne doivent pas contenir d'images. Pour le moment, j'ai neuf articles. Les deux derniers contiennent une image. Je fais donc défiler vers le bas et je vois juste du texte dans les sept premiers éléments. Atteindre la fin de la liste me montre deux photos comme prévu. Mais si je fais défiler à nouveau ces images apparaissent dans d'autres éléments.

Faire défiler vers le haut et vers le bas change les images toujours dans le même ordre:

  • Le premier défilement vers le haut (après avoir été abaissé une fois) crée une image dans le deuxième élément.
  • Faire défiler vers le bas crée une image dans le 7e élément.
  • Faire défiler vers le haut crée une image dans le premier élément.
  • Faire défiler vers le bas ne crée rien.
  • Faire défiler vers le haut crée une image dans le troisième élément et l'image dans le deuxième élément a été remplacée par l'autre image.
  • Faire défiler vers le bas crée une image dans le 6ème élément et laisse l'image dans le 7ème élément disparaître.
  • Etc ...

J'ai déjà découvert que la méthode onBindViewHolder (ViewHolder holder, int position) est appelée chaque fois qu'un élément réapparaît à l'écran. J'ai vérifié la position et les informations fournies dans mon mDataset. Tout fonctionne bien ici: la position est correcte et les informations fournies aussi. Le texte de tous les éléments ne change pas et s'affiche toujours correctement. Quelqu'un at-il une sorte de ce problème et connaît une solution?

34
Jason Saruulo

Je pense que vous devez ajouter une clause else à votre instruction if ( object.has("image") ) dans la méthode onBindViewHolder(ViewHolder holder, int position), en définissant l'image sur null:

holder.image.setImageDrawable(null);

Je pense la documentation pour onBindViewHolder décrivez mieux pourquoi c'est nécessaire:

Appelé par RecyclerView pour afficher les données à la position spécifiée. Cette méthode doit mettre à jour le contenu de l'itemView pour refléter l'item à la position donnée.

Vous devez toujours supposer que le ViewHolder transmis doit être complètement mis à jour :)

47
Joe

Je ne sais pas si les gens ont encore des problèmes avec cela, mais la déclaration if n'a pas fonctionné pour moi. Ce qui a fonctionné pour moi, c'est ceci:

Utilisation d'un remplacement sur cette méthode dans l'adaptateur

@Override
    public int getItemViewType(int position) {
        return position;
    }

suivi en appelant l'instruction if basée sur ceci pour l'image elle-même dans le onBindViewHolder, comme ceci:

int pos = getItemViewType(position);
            if(theList.get(pos).getPicture() == null) {

                holder.picture.setVisibility(View.GONE);
            } else {

                Picasso.with(context).load(R.drawable.robot).into(holder.picture);
            }

J'utilise Picasso mais cela devrait fonctionner avec toute autre situation. J'espère que cela aide quelqu'un!

14
Lion789

Basé sur la réponse de @ Lion789. J'utilise Google Volley pour le chargement des images et json.

Cela nous permet de récupérer la position sur la vue qui va être recyclée plus tard.

@Override
public int getItemViewType(int position) {
    return position;
}

Lors de la demande, définissez la position comme TAG

@Override
public void onBindViewHolder(final ItemHolder holder, int position) {
        ImageRequest thumbnailRequest = new ImageRequest(url,
                new Response.Listener<Bitmap>() {
                    @Override
                    public void onResponse(Bitmap response) {                          
                        holder.itemThumbnail.setImageBitmap(response);
                    }
                },[...]);

        thumbnailRequest.setTag(String.valueOf(position)); //setting the TAG
        mQueue.addToRequestQueue(thumbnailRequest);
}

Et lorsque la vue est recyclée, nous annulons la demande et supprimons l'image actuelle

@Override
public void onViewRecycled(ItemHolder holder) {
    super.onViewRecycled(holder);
    mQueue.cancelAll(String.valueOf(holder.getItemViewType()));
    holder.itemThumbnail.setImageDrawable(null);
}

Volley garantit que les demandes annulées ne seront pas livrées.

3
Marcola