Странное поведение изображений в RecyclerView

Я использую android.support.v7.widget.RecyclerView для отображения простых элементов, содержащих текст, а в некоторых случаях и изображения. В основном я следовал руководству отсюда https://developer.android.com/training/material/lists-cards.html и просто изменил тип mDataset со String[] на JSONArray и xml ViewHolder. Мой RecyclerView находится в простом фрагменте:

<?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>

В этом фрагменте я загружаю всю информацию с веб-сервера. После получения информации я инициализирую элементы в RecyclerView. Я использую LinearLayoutManager, который установлен в методе onCreate моего фрагмента. После добавления адаптера я вызываю метод setHasFixedSize(true) в моем RecyclerView. Мой класс адаптера выглядит так:

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();
}
}

И мой xml для представления внутри моего 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>

Итак, вот моя проблема: все работает нормально. Текст и изображение отображаются правильно в моем RecyclerView. Однако, если я дойду до конца списка, прокручивая вниз и снова прокручивая вверх, изображения отображаются в элементах, которые не должны содержать изображений. На данный момент у меня девять предметов. Последние два из них содержат картинку. Поэтому я прокручиваю вниз и вижу только текст в первых семи элементах. Достижение конца списка показывает мне две картинки, как и ожидалось. Но если я снова прокрутлю вверх, эти изображения появятся в других элементах.

Прокрутка вверх и вниз изменяет изображения всегда в одном и том же порядке:

  • Первая прокрутка вверх (после одиночного спуска) создает картинку во втором элементе.
  • Прокрутка вниз создает картинку в 7-м пункте.
  • Прокрутка вверх создает изображение в первом элементе.
  • Прокрутка вниз ничего не дает.
  • Прокрутка вверх создает изображение в третьем элементе, а изображение во втором элементе было перезаписано другим изображением.
  • Прокрутка вниз создает изображение в 6-м элементе и позволяет исчезнуть изображению в 7-м элементе.
  • И так далее ...

Я уже обнаружил, что метод onBindViewHolder(ViewHolderholder, int position) вызывается каждый раз, когда элемент снова появляется на экране. Я проверил позицию и предоставленную информацию в своем mDataset. Здесь все работает нормально: позиция правильная и предоставленная информация тоже. Текст во всех пунктах не меняется и всегда отображается корректно. У кого-нибудь есть такая проблема и знает какое-то решение?


person Jason Saruulo    schedule 02.01.2015    source источник


Ответы (3)


Я думаю, вам нужно добавить предложение else к оператору if ( object.has("image") ) в методе onBindViewHolder(ViewHolder holder, int position), установив для изображения значение null:

holder.image.setImageDrawable(null);

Я думаю, что документация для onBindViewHolder опиши лучше зачем это нужно:

Вызывается RecyclerView для отображения данных в указанной позиции. Этот метод должен обновить содержимое itemView, чтобы отразить элемент в заданной позиции.

Вы всегда должны предполагать, что переданный ViewHolder необходимо полностью обновить :)

person Joe    schedule 02.01.2015
comment
Кажется, это помогает! Я поместил предложение else в свой оператор if (!hopp.getString(image).equals()), потому что в моих объектах JSON всегда будет поле изображения. Я пробовал то же самое, но использовалholder.image.setImageBitmap(null), что, очевидно, не сработало, или я также поместил его в неправильное выражение if. Это не имеет значения, потому что вы спасли мой день. :) - person Jason Saruulo; 02.01.2015
comment
Спасибо. Я изменил на holder.image.setImageResource(R.drawable.image); - person zackygaurav; 15.07.2015

Не уверен, что у людей все еще есть проблемы с этим, но оператор if не сработал для меня. Что сработало для меня, так это:

Использование переопределения этого метода в адаптере

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

с последующим вызовом оператора if на основе этого для самого изображения в onBindViewHolder, например:

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);
            }

Я использую Picasso, но он должен работать и в любой другой ситуации. Надеюсь, это поможет кому-то!

person Lion789    schedule 18.03.2016
comment
Добавление getItemViewType решило мою проблему! - person ziniestro; 04.08.2017
comment
Это нельзя рассматривать как решение. По сути, вы говорите RecyclerView вообще не перерабатывать. - person skywall; 28.08.2018

На основе ответа @ Lion789. Я использую Google Volley для загрузки изображений и json. .

Это позволяет нам получить позицию в представлении, которая будет повторно использована позже.

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

Делая запрос, установите позицию как 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);
}

И когда вид будет переработан, мы отменим запрос и удалим текущее изображение.

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

Залп гарантирует, что отмененные запросы не будут доставлены.

person Marcola Carr    schedule 28.02.2017
comment
не будет ли это вызывать изображения каждый раз, когда представление возвращается в поле зрения? - person x10sion; 16.03.2017
comment
Да, но так как Volley все кеширует, образ уже был бы в памяти - person Marcola Carr; 19.03.2017