Stick items to bottom inside RecyclerView with GridLayoutManager

Viewed 857

I want to have a RecyclerView that does order its items left to right and top to bottom as it does by default. But additionally I want following special behaviour like in the image below on the right side.

enter image description here

I want that the items "as a total" stick to bottom as if the RecyclerView itself would adjust its size to wrap its content if the items do not exceed the RecyclerViews height - for a better accessibility with the thumb for search results. I can't get this behaviour with a combination of LayoutManager::reverseLayout and RecyclerView::layoutDirection and wrap_content for the RecyclerView and mack it stick to the bottom does not work either because I want the whole list area to be scrollable (the RecyclerView must keep its full size)- any ideas how I can get this behaviour?

2 Answers

To achieve this behaviour to align all items to the bottom of RecyclerView you need to set the flag setStackFromEnd(true)

When stack from bottom is set to true, the list fills its content starting from the bottom of the view

but this flag is not supported by the GridLayout throwing an exception like:

GridLayoutManager does not support stack from end. Consider using reverse layout

but even using setReverseLayout(true) in GridLayoutManager it renders the items from bottom to top and from left to right which is not the result you want even reversing the data of your list.

Possible Solutions:

  1. Use GridLayoutManager and add top padding only to the first columns to fill the remaining top area or even use dummy empty columns.

  2. Use a LinearLayout with setStackFromEnd(true) which works as expected where each item is rendered from bottom to top of RecyclerView, and each item row (Item View) holding eg: 4 views (the number of columns) with equal width. Of course in that way each Item model in the list must hold 4 data one for each column.

If you choose the second option you can prepare your columns data for each row like the below code snippet:

List<ItemModel> data = new ArrayList<>();
int listCnt = 14;
for(int i=0; i<listCnt; i++){
    if(i%4 == 0) {
        String dataCol1 = i < listCnt ? String.valueOf(i) : "";
        String dataCol2 = (i + 1) < listCnt ? String.valueOf(i + 1) : "";
        String dataCol3 = (i + 2) < listCnt ? String.valueOf(i + 2) : "";
        String dataCol4 = (i + 3) < listCnt ? String.valueOf(i + 3) : "";
        data.add(new ItemModel(dataCol1, dataCol2, dataCol3, dataCol4));
    }
}

//set LinearLayoutManager
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this, RecyclerView.VERTICAL, false);
linearLayoutManager.setStackFromEnd(true);
recyclerView.setLayoutManager(linearLayoutManager);

//set adapter
RecyclerViewAdapter adapter = new RecyclerViewAdapter(data);
recyclerView.setAdapter(adapter);

where ItemModel holds the data for each row like:

public class ItemModel {

    public String dataCol1="", dataCol2="", dataCol3="", dataCol4="";

    public ItemModel(String dataCol1, String dataCol2, String dataCol3, String dataCol4) {
        this.dataCol1 = dataCol1;
        this.dataCol2 = dataCol2;
        this.dataCol3 = dataCol3;
        this.dataCol4 = dataCol4;
    }
}

And of course in your adapter you have to set the columns Visibility to View.INVISIBLE for those who don't have any data and View.VISIBLE for those who have. Example of adapter can be like below:

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

        private List<ItemModel> mData;

        public RecyclerViewAdapter(List<ItemModel> data) {
            this.mData = data;
        }

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

        @Override
        public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
           ItemModel itemModel = mData.get(position);
           if(holder instanceof ItemViewHolder) {
               ((ItemViewHolder) holder).bind(itemModel);
           }
        }

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

        public class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
            public TextView itemTextView1, itemTextView2, itemTextView3, itemTextView4;

            public ItemViewHolder(View itemView) {
                super(itemView);
                itemTextView1 = itemView.findViewById(R.id.itemTextView1);
                itemTextView2 = itemView.findViewById(R.id.itemTextView2);
                itemTextView3 = itemView.findViewById(R.id.itemTextView3);
                itemTextView4 = itemView.findViewById(R.id.itemTextView4);

                itemTextView1.setOnClickListener(this);
                itemTextView2.setOnClickListener(this);
                itemTextView3.setOnClickListener(this);
                itemTextView4.setOnClickListener(this);
            }

            public void bind(ItemModel itemModel){
                itemTextView1.setText(itemModel.dataCol1);
                itemTextView2.setText(itemModel.dataCol2);
                itemTextView3.setText(itemModel.dataCol3);
                itemTextView4.setText(itemModel.dataCol4);

                itemTextView1.setVisibility(TextUtils.isEmpty(itemModel.dataCol1) ? View.INVISIBLE : View.VISIBLE);
                itemTextView2.setVisibility(TextUtils.isEmpty(itemModel.dataCol2) ? View.INVISIBLE : View.VISIBLE);
                itemTextView3.setVisibility(TextUtils.isEmpty(itemModel.dataCol3) ? View.INVISIBLE : View.VISIBLE);
                itemTextView4.setVisibility(TextUtils.isEmpty(itemModel.dataCol4) ? View.INVISIBLE : View.VISIBLE);
            }

            @Override
            public void onClick(View v) {
                int cId = v.getId();
                if(cId == itemTextView1.getId()){
                    //get col1 data
                    String dataCol1 = mData.get(getAdapterPosition()).dataCol1;
                }
                else if(cId == itemTextView2.getId()){
                    //get col2 data
                    String dataCol2 = mData.get(getAdapterPosition()).dataCol2;
                }
                else if(cId == itemTextView3.getId()){
                    //get col3 data
                    String dataCol3 = mData.get(getAdapterPosition()).dataCol3;
                }
                else if(cId == itemTextView4.getId()){
                    //get col4 data
                    String dataCol4 = mData.get(getAdapterPosition()).dataCol4;
                }
            }
        }
    }

where the list item R.layout.item_list_row which holds the 4 column views can be like below:

<?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:layout_width="match_parent"
    android:layout_height="50dp"
    android:orientation="horizontal">

    <TextView
        android:id="@+id/itemTextView1"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_weight="1"
        android:background="@android:color/holo_blue_light"
        android:textColor="@android:color/black"
        tools:text="1"/>

    <TextView
        android:id="@+id/itemTextView2"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_weight="1"
        android:background="@android:color/holo_blue_light"
        android:textColor="@android:color/black"
        tools:text="2"/>

    <TextView
        android:id="@+id/itemTextView3"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_weight="1"
        android:background="@android:color/holo_blue_light"
        android:textColor="@android:color/black"
        tools:text="3"/>

    <TextView
        android:id="@+id/itemTextView4"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:gravity="center"
        android:layout_weight="1"
        android:background="@android:color/holo_blue_light"
        android:textColor="@android:color/black"
        tools:text="4"/>

</LinearLayout>

And of course if you have a different span count number you can make different ViewHolder types in the RecyclerView Adapter.

Results for option 2 with RecyclerView height to be match_parent, 300dp and wrap_content respectively will be like below. RecyclerView has the grey background:

match_parent 300dp wrap_content

Here is my understanding of what you are trying to do.

  1. The RecyclerView has some real estate on the screen that is constant in width and height for any given device/orientation.

  2. If the item count is such that its display fills the RecyclerView's on-screen area, then the RecyclerView just fills the area and scrolls as it will.

  3. If the item count is such that the RecyclerView's on-screen area is not filled, then the whole display of the items should drop to the bottom of the RecyclerView's on-screen area leaving empty space at the top.

One way to do this is to programmatically set top padding to the RecyclerView that is the difference between the height of the RecyclerView and the total height of the displayed items if they don't fill the RecyclerView. This is easier if you can compute the height difference before layout, but you could also compute it after layout with a global layout listener.

LinearLayoutManager#findFirstCompletelyVisibleItemPosition() will tell you which item is on top of the display.

LinearLayoutManager#findLastCompletelyVisibleItemPosition() will tell you the last position.

If all positions are shown, you will know to introduce top padding.

I am unclear if you would want the items to be able to scroll to the top or not if they don't fill the RecyclerView. Introduce bottom padding if they should scroll up leaving empty space at the bottom.

You will also have to specify android:clipToPadding="false" on the RecyclerView to get the padding to work as explained above.

Here is a sample activity to show how this can be done:

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private final List<String> mItems = new ArrayList<>();
    private RecyclerView mRecycler;
    private final int mCount = 24;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        for (int i = 0; i < mCount; i++) {
            mItems.add("[" + i + "] " + getString(R.string.lorem));
        }

        mRecycler = findViewById(R.id.recyclerView);
        RecyclerViewAdapter mAdapter = new RecyclerViewAdapter(mItems);
        mRecycler.setAdapter(mAdapter);
        mRecycler.setLayoutManager(new GridLayoutManager(this, 5));

        mRecycler.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mRecycler.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                GridLayoutManager glm = (GridLayoutManager) mRecycler.getLayoutManager();
                if (glm == null) return;
                int lastPosition = glm.findLastCompletelyVisibleItemPosition();
                if (lastPosition == (mCount - 1)) {
                    View lastView = glm.findViewByPosition(lastPosition);
                    if (lastView == null) return;
                    int newPadding = mRecycler.getHeight() - lastView.getBottom();
                    // Set bottom padding to newPadding to allow items to scroll to the top
                    // of the RecyclerView.
                    mRecycler.setPadding(0, newPadding, 0, newPadding);
                    mRecycler.setClipToPadding(false);
                }
            }
        });
    }
}

enter image description here

Related