Michael Svit Michael Svit - 4 months ago 32
Android Question

RecyclerView creating and binding all ViewHolders when scrolling

I have a weird issue which I cannot pinpoint. My app fetches movie data from a website, and specifically, a poster image. I created a RecyclerView that is supposed to show all of the posters in a 2-column grid.

Scrolling experience is extremely laggy, and for some reason the items are swapping places mid-scroll even though there are no changes in the data list.

I tried debugging the app, and found out that on the first time I call notifyDataSetChanged() my adapter would call onCreateViewHolder(...) for every item in my list (around 90 times, when only 6 posters are visible at any given moment), and same goes for onBindViewAdapter(...).
Then, when scrolling, it would recreate and rebind all ViewHolders again!

I have checked my code again and again and haven't been able to determine the cause. Your help will be greatly appreciated.

RecyclerView.adapter:

public class MovieRecyclerViewAdapter extends RecyclerView.Adapter<MovieRecyclerViewAdapter.ViewHolder> {
private final Cinema cinema;
private final List<Movie> movies;
private final Context context;
private final DisplayMetrics displayMetrics;
private final Picasso picasso;

public MovieRecyclerViewAdapter(Context context, Cinema cinema, List<Movie> movies) {
this.context = context;
this.cinema = cinema;
this.movies = movies;

WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
displayMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(displayMetrics);

// Set up Picasso with okHttp3
okhttp3.OkHttpClient okHttp3Client = new okhttp3.OkHttpClient();
OkHttp3Downloader okHttp3Downloader = new OkHttp3Downloader(okHttp3Client);
picasso = new Picasso.Builder(context)
.downloader(okHttp3Downloader)
.build();
}

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

@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
Movie movie = movies.get(position);
int imgViewWidth = displayMetrics.widthPixels / 2;
int imgViewHeight = (int)(imgViewWidth * 1.4);
picasso.with(context)
.load(cinema.getPosterUrl(movie))
.resize(imgViewWidth, imgViewHeight)
.into(holder.imageView);
}

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

public class ViewHolder extends RecyclerView.ViewHolder {
public ImageView imageView;

public ViewHolder(View view) {
super(view);
this.imageView = (ImageView) view.findViewById(R.id.fragment_movies_poster);
}
}
}


RecyclerView item layout:

<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_movies_poster"
android:layout_width="match_parent"
android:layout_height="wrap_content">

</ImageView>


RecyclerView XML definition:

<android.support.v7.widget.RecyclerView
android:id="@+id/fragment_movies_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/fragment_movies" />


RecyclerView configuration, setting adapter:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_movies_grid, container, false);

Context context = view.getContext();
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.fragment_movies_recycler_view);
recyclerView.setLayoutManager(new GridLayoutManager(context, columnCount));
movies = new ArrayList<>();
adapter = new MovieRecyclerViewAdapter(context, cinema, movies);
recyclerView.setAdapter(adapter);
recyclerView.setHasFixedSize(true);

fetchMovies(view); // Populates movies list

return view;
}





EDIT

Since posting this question I found a similar question: Android RecyclerView Creates and Binds all the views on Dataset change

Indeed, once I set the item layout_height to a fixed value rather than "wrap_content" the issue is fixed. Apparently the RecyclerView tries to create all its views in order to determine its height.

However, I tried the solution offered in that question, calling the LayoutManager's
lm.setAutoMeasureEnabled(false)
with layout_height set to "wrap_content" and the issue persisted. Are there any suggestions how would I have an adaptive layout_height without this issue occurring?

Answer

I ended up avoiding this issue by using a custom ImageView subclass for the item, overriding onMeasure() and giving it a fixed aspect ratio that determines height according to width.


FixedAspectRatioImageView:

public class FixedAspectRatioImageView extends ImageView{
    public FixedAspectRatioImageView(Context context) {
        super(context);
    }

    public FixedAspectRatioImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FixedAspectRatioImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = (int) (widthSize * 1.4);
        super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY));
    }
}

RecyclerView item as defined in XML:

<?xml version="1.0" encoding="utf-8"?>
<com.michaelsvit.kolnoa.FixedAspectRatioImageView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_movies_poster"
    android:layout_width="match_parent"
    android:layout_height="288dp"> // Value here does not matter, as it is dynamically set
                                   // to be 1.4 times width

</com.michaelsvit.kolnoa.FixedAspectRatioImageView>