Why does my StaggeredGrid RecyclerView layout every item, not only visible ones?
Asked Answered
P

2

0

I have a Staggered grid containing 370 items, with images.

I want to make sure the items are recycled quickly to be careful with memory, but a ViewHolder is created and then bound for every single item in my adapter and pays no attention to whether children are visible

I've tried the following

StaggeredGridLayoutManager lm = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);
rv.setLayoutManager(lm);

rv.setItemViewCacheSize(20); //Has no effect

RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool();
pool.setMaxRecycledViews(0, 20);
rv.setRecycledViewPool(pool); //also has no effect

Logging shows onCreateViewHolder and onBindViewHolder are called 185 times each. Then onViewRecycled is called 185 times before resuming calls to onCreateViewHolder until we reach the full 370.

This could be an understanding problem on my part, but I think the RecyclerView should bind only those view that are visible, or to honor having only 20 views, or 20 in pool + however many fit on screen. How can I make this happen with the StaggeredGridLayoutManager?

If I listen to scroll changes and use findFirstCompletelyVisibleItemPositions, and findLastCompletelyVisibleItemPositions this still spans every single item in the adapter, not just the 6 that fit on screen

My adapter code

class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {

    static final int NUM_COLS = 3;
    private final LayoutInflater mInflater;
    private final List<GridItem> mEntries;
    private int mLastExpanded; //stores where the last expanded item was
    private OnCardClickListener mOnItemClick;

    MyAdapter(Context context) {
        super();
        mEntries = new ArrayList<>();
        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    void setOnTileClickListener(@Nullable OnCardClickListener listener) {
        mOnItemClick = listener;
        notifyDataSetChanged(); //recall bind logic
    }

    void setItems(Collection<GridItem> items) {
        mEntries.clear();
        mEntries.addAll(items);
        sort();
    }

    @WorkerThread
    private void sort() {
        Collections.sort(mEntries, (thisEntry, otherEntry) -> {
            int ret;
            if (otherEntry == null || thisEntry.getCreated() == otherEntry.getCreated()) {
                ret = 0;
            } else if (thisEntry.getCreated() > otherEntry.getCreated()) {
                ret = -1;
            } else {
                ret = 1;
            }
            return ret;
        });
    }

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

    private GridItem getItem(int position) {
        return mEntries.get(position);
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new MyViewHolder(mInflater.inflate(R.layout.li_grid_item, parent, false));
    }

    @Override
    public void onViewRecycled(MyViewHolder holder) {
        super.onViewRecycled(holder);
        holder.onViewRecycled(); //clears bitmap reference
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        determineTileSize(holder, position);
        holder.bind(getItem(position),  mOnItemClick);
    }

    private void determineTileSize(MyViewHolder holder, int position) {
        ViewGroup.LayoutParams cardParams = holder.getCardLayout().getLayoutParams();
        StaggeredGridLayoutManager.LayoutParams gridItemParams = (StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
        if (shouldBeExpanded(position)) {
            cardParams.height = (int) holder.getCard().getResources().getDimension(R.dimen.spacing_card_large);
            mLastExpanded = position;
            gridItemParams.setFullSpan(true);
        }
        holder.getCardLayout().setLayoutParams(cardParams);
    }

    private boolean shouldBeExpanded(int position) {
        return position > (mLastExpanded + NUM_COLS);  //minimum 1 row between enlarged
    }

}

My Activity Layout Structure

<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" ...>
    <android.support.design.widget.AppBarLayout ...>
        <android.support.design.widget.CollapsingToolbarLayout ...>
            <android.support.v7.widget.Toolbar
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:layout_collapseMode="pin" ... />
            <android.support.design.widget.TabLayout ...
                app:layout_collapseMode="pin"
                android:layout_width="wrap_content"
                android:layout_height="?attr/actionBarSize" />
        </android.support.design.widget.CollapsingToolbarLayout>
    </android.support.design.widget.AppBarLayout>
    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />
    <FrameLayout
        android:id="@+id/bottom_sheet"
        android:layout_width="match_parent"
        android:layout_height="@dimen/height_backdrop"
        android:minHeight="@dimen/height_backdrop"
        android:background="@color/colorAccent"
        android:visibility="gone"
        app:elevation="@dimen/spacing_narrow"
        app:behavior_peekHeight="0dp"
        app:behavior_hideable="true"
        app:layout_behavior="android.support.design.widget.BottomSheetBehavior" />
</android.support.design.widget.CoordinatorLayout>

Fragment Layout

<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior" ...>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/grid_recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical" />

            <!-- Empty and loading views -->

    </RelativeLayout>

</android.support.v4.widget.NestedScrollView>
Pisci answered 1/11, 2016 at 0:8 Comment(8)
Could you show the adapter code? do you have different types of views?Franchescafranchise
No, I use only one view type, but some rows are set to full spanPisci
RecyclerView in your layout file is direct child or under some NestedScrollView? Can you post the layout structure?Pinworm
@MahmoudElmorabea added adapter, see if there is something obvious pleasePisci
@Pinworm its nested, I'll post it in a momentPisci
Why have you added RecyclerView in NestedScrollView when not required? Add it without nested scroll and I believe it'll solve your issue.Pinworm
Oh, how embarrassing. That solved it, such a simpel bug. I though the NSV was required for the collapsing header, it isn't! :/ Please make a clear answer so I can give you the reputationPisci
@NickCardoso Added :) and I think Collapsing header can be achieved by any view who has implemented NestedScrollingChildPinworm
P
7

Problem:

The reason you are facing this is issue is because you have added RecyclerView in NestedScrollView.

Reason:

It's not first time I have heard of this issue, me and probably everyone who has tried to put RecyclerView in NestedScrollView has faced this issue (if noticed).

As far as I could figure the reason, it is because when you place RecyclerView in NestedScrollView, it is unable to identify exact height required for RecyclerView. What normally a developer assumes for this (in simple words) is RecyclerView height should be match_parent once all above views have gone off screen. But unfortunately, this is NOT the case.

It makes RecyclerView somehow wrap_content adding all its views and then measuring its height (correct me if I am wrong). Not sure a possible bug or expected behaviour, but I believe NestedScrollView should be able to handle this case explicitly, otherwise, adding RecyclerView in NestedScrollView is completely useless, as it does not recycle views, completely destroying the RecyclerView concept and thus consuming a lot of memory.

Temporary Solution:

Just remove the RecyclerView from NestedScrollView so that it can properly reuse the views.

NOTE: The answer may not be 100% right as it is based completely on my personal observation and experience. Any better solution or improvements in the answer is appreciated.

Pinworm answered 9/11, 2016 at 18:58 Comment(0)
C
-1

The issue is in NestedScrollview the space available to the recyclerview is not determined.

You can use android:fillViewport="true" to make NestedScrollView measure the RecyclerView. The RecyclerView will fill the remaining height. so if you want to scroll the NestScrollView, you can set the RecyclerView's minHeight.

Cookbook answered 17/7, 2018 at 9:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.