Search code examples
androidimageviewandroid-recyclerviewporter-duff

Incorrect image recycling in RecyclerView


I have a RecyclerView with an icon in each row. The icon should be colored dynamically, so I use a white icon and apply a color filter when binding the view. Oddly enough, I end up with a lot of discrepancies in the resulting view.

In this example I'm attempting to make every third row red and the rest green (note #18):

enter image description here

Below is the adapter. As you can see, I'm applying the altered icon to the ImageView every time I rebind the holder, presumably leaving no room for the old recycled image.

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

    private final Context context;

    public TestAdapter(Context context) {
        this.context = context;
    }

    @Override
    public TestAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false);
        return new ViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(TestAdapter.ViewHolder holder, int position) {
        holder.bindItem(position);
    }

    @Override
    public int getItemCount() {
        return 200;
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

        private final ImageView image;
        private final TextView text;

        public ViewHolder(View itemView) {
            super(itemView);

            image = (ImageView) itemView.findViewById(R.id.image);
            text = (TextView) itemView.findViewById(R.id.text);
        }

        public void bindItem(int position) {
            // pick color depending on the position
            final int color = ContextCompat.getColor(context,
                    position%3 == 0 ? android.R.color.holo_red_light : android.R.color.holo_green_light
            );

            // set text content and color
            text.setText("#" + position);
            text.setTextColor(color);

            // create icon from the resource and set filter
            final Drawable icon = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_brightness, null);
            icon.setColorFilter(color, PorterDuff.Mode.MULTIPLY);

            image.setImageDrawable(icon);
        }
    }

}

item_layout.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/image"
        android:layout_width="18dp"
        android:layout_height="wrap_content"
        android:scaleType="fitStart"
        android:adjustViewBounds="true" />

    <TextView
        android:id="@+id/text"
        android:textSize="18sp"
        android:textColor="@android:color/black"
        android:layout_marginLeft="48dp"
        android:layout_marginStart="48dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        tools:text="text" />

</LinearLayout>

What gives?

Thanks.


Solution

  • Having read your comments, I'm not sure whether this is suitable to your requirements. However, I was able to replicate the expected behaviour by setting the "brightness" drawable with the android:src element in the layout file, then applying the filter to the ImageView (rather than the icon):

    public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder> {
    
        public RecyclerViewAdapter() {
        }
    
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
            View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.rv_row, viewGroup, false);
            return new ViewHolder(v);
        }
    
        @Override
        public void onBindViewHolder(ViewHolder viewHolder, int i) {
            viewHolder.bindItem(i);
        }
    
        @Override
        public int getItemCount() {
            return 200;
        }
    
        public static class ViewHolder extends RecyclerView.ViewHolder {
    
            private final ImageView image;
            private final TextView text;
    
            public ViewHolder(View itemView) {
                super(itemView);
    
                image = (ImageView) itemView.findViewById(R.id.image);
                text = (TextView) itemView.findViewById(R.id.text);
            }
    
            public void bindItem(int position) {
    
                final int color = ContextCompat.getColor(itemView.getContext(),
                        position % 3 == 0 ? android.R.color.holo_red_light : android.R.color.holo_green_light
                );
    
                text.setText("#" + position);
                text.setTextColor(color);
    
                image.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
            }
        }
    }
    

    EDIT:

    It also works if you set the icon dynamically, but apply the filter to the ImageView:

    public static class ViewHolder extends RecyclerView.ViewHolder {
    
        private final ImageView image;
        private final TextView text;
    
        public ViewHolder(View itemView) {
            super(itemView);
    
            image = (ImageView) itemView.findViewById(R.id.image);
            text = (TextView) itemView.findViewById(R.id.text);
        }
    
        public void bindItem(int position) {
    
            final int color = ContextCompat.getColor(itemView.getContext(),
                    position % 3 == 0 ? android.R.color.holo_red_light : android.R.color.holo_green_light
            );
    
            text.setText("#" + position);
            text.setTextColor(color);
    
            Drawable icon = getIcon();
    
            image.setImageDrawable(icon);
            image.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
        }
    
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        private Drawable getIcon() {
            return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
                    ? image.getContext().getResources().getDrawable(R.drawable.ic_brightness_1_white_24dp, null)
                    : image.getContext().getResources().getDrawable(R.drawable.ic_brightness_1_white_24dp);
        }
    }