Make last Item / ViewHolder in RecyclerView fill rest of the screen or have a min height
Asked Answered
S

4

21

I'm struggeling with the RecyclerView. I use a recycler view to display the details of my model class.

//My model class
MyModel {
    String name;
    Double latitude;
    Double longitude;
    Boolean isOnline;
    ...
}

Since some of the values might not be present, I use the RecyclerView with custom view types (one representing each value of my model).

//Inside my custom adapter
public void setModel(T model) {
    //Reset values
    itemCount = 0;
    deviceOfflineViewPosition = -1;
    mapViewPosition = -1;

    //If device is offline, add device offline item
    if (device.isOnline() == null || !device.isOnline()) {
        deviceOfflineViewPosition = itemCount;
        itemCount++;
    }

    //Add additional items if necessary
    ...

    //Always add the map as the last item
    mapViewPosition = itemCount;
    itemCount++;

    notifyDataSetChanged();
}

@Override
public int getItemViewType(int position) {
    if (position == deviceOfflineViewPosition) {
        return ITEM_VIEW_TYPE_OFFLINE;
    } else if (position == mapViewPosition) {
        return ITEM_VIEW_TYPE_MAP;
    } else if (...) {
        //Check for other view types
    }
}

With the RecyclerView I can easily determine at runtime which values are available and add corresponding items to the RecyclerView datasource. I simplyfied the code but my model has a lot more values and I have a lot more view types.

The last item in the RecyclerView is always a map and it is always present. Even if there is no value at all in my model, there will at least be one item, the map.

PROBLEM: How can I make the last item in RecyclerView fill the remaining space on screen and also have a min heigh. The size shall be what ever value is lager: the remaining space or the min height. For example:

  • Model has a few values, which in sum take up 100dp of a 600dp screen -> map heigh should be 500dp
  • Model has a lot of values, which in sum take up 500dp of a 600dp screen -> map heigh should be a min value of 200dp
  • Model has no values -> map fills whole screen

enter image description here

Senility answered 21/12, 2016 at 1:30 Comment(3)
The concept of ‘rest of the space’ doesn’t make so much sense in the context of a scrolling view like RecyclerView. You can, however, set a min height using View.setMinimumHeight() and set height as match parent using View.setLayoutParams(). That must be done during the binding process, depending on how many items you have.Huzzah
Well it does and thats why I also have my minHeight. See, if the conent of the recyclerview make up less space than the screen, I want to strech the map to cover the empty space. If there is more content than can fit on the screen, oviously there is not rest to fill and therefore the minHeight should be usedSenility
This may help you #36887839Fortyniner
P
10

You can find the remaining space in RecyclerView after laying out the last item and add that remaining space to the minHeight of the last item.

val isLastItem = getItemCount() - 1 == position
if (isLastItem) {
    val lastItemView = holder.itemView
    lastItemView.doOnLayout {
        val recyclerViewHeight = recyclerView.height
        val lastItemBottom = lastItemView.bottom
        val heightDifference = recyclerViewHeight - lastItemBottom
        if (heightDifference > 0) {
            lastItemView.minimumHeight = lastItemView.height + heightDifference
        }
    }
}

In onBindHolder check if the item is last item using getItemCount() - 1 == position. If it is the last item, find the height difference by subtracting recyclerView height with lastItem bottom (getBottom() gives you the bottom most pixel of the view relative to it's parent. In this case, our parent is RecyclerView).

If the difference is greater than 0, then add that to the current height of the last view and set it as minHeight. We are setting this as minHeight instead of setting directly as height to support dynamic content change for the last view.

Note: This code is Kotlin, and doOnLayout function is from Android KTx. Also your RecyclerView height should be match_parent for this to work (I guess that's obvious).

Potable answered 12/12, 2018 at 7:16 Comment(4)
So maybe i'm missing something, but for me setting the minimum height is not causing the cell to redraw. Because its being rendered first so that it can get the appropriate values to calculate, when it actually sets minimum height its already drawn once. Is there a way I can then force it to do a redraw?Blockhouse
@Blockhouse That's weird. If you look at the setMinimumHeight source from View.java, it actually calls requestLayout() which will invalidate the layout and layout pass will be done again. Maybe you can try calling requestLayout() manually after setting height like this lastItemView.post{ requestLayout() } . If that doesn't work, try to call invalidate() too. requestLayout will force the View to measure and lay again. invalidate will force to redraw it's content.Potable
doOnLayout didn't work for me. I used post instead.Molybdic
As @SoroushLotfi mentions, in my case is working using post but when the item is outside the screen bottom is zero and the calculation is wrong so the item is taking the parent height as its minimunHeight. I solved it checking if heightDifference > 0 && heightDifference < recyclerViewHeightCoralyn
A
5

You can extend LinearLayoutManager to layout the last item yourself.

This is a FooterLinearLayoutManager that will move the last item of the list to the bottom of the screen (if it isn't already there). By overriding layoutDecoratedWithMargins the LinearLayouyManager calls us with where the item should go, but we can match this against the parent height.

Note: This will not "resize" the view, so fancy backgrounds or similar probably won't work, it will just move the last item to the bottom of the screen.

/**
 * Moves the last list item to the bottom of the screen.
 */
class FooterLinearLayoutManager(context: Context) : LinearLayoutManager(context) {

    override fun layoutDecoratedWithMargins(child: View, left: Int, top: Int, right: Int, bottom: Int) {
        val lp = child.layoutParams as RecyclerView.LayoutParams
        if (lp.viewAdapterPosition < itemCount - 1)
            return super.layoutDecoratedWithMargins(child, left, top, right, bottom)

        val parentBottom = height - paddingBottom
        return if (bottom < parentBottom) {
            val offset = parentBottom - bottom
            super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset)
        } else {
            super.layoutDecoratedWithMargins(child, left, top, right, bottom)
        }
    }
}
Ammoniate answered 13/11, 2019 at 14:18 Comment(0)
A
1

I used muthuraj solution to solve a similar problem, I wanted the last item to be shown at the last normally if previous items fill up the height of the page or more height but in case the previous item doesn't fill up the height, I wanted last item to be shown on the bottom of the page.

When previous item + specialItem take all the place.
--------------
item
item
item
specialItem
--------------

When previous item + specialItem take more than the height
--------------
item
item
item
item
item
item
specialItem
--------------

When previous item + specialItem take less than the height
--------------



specialItem
--------------
or
--------------
item


specialItem
--------------

to archive this I use this code

val lastItemView = holder.itemView
            //TODO use a better option instead of waiting for 200ms
            Handler().postDelayed({
                val lastItemTop = lastItemView.top
                val remainingSpace = recyclerViewHeight() - lastItemTop
                val heightToSet = Math.max(remainingSpace, minHeight)

                if (lastItemView.height != heightToSet) {
                    val layoutParams = lastItemView.layoutParams
                    layoutParams.height = heightToSet
                    lastItemView.layoutParams = layoutParams
                }
            }, 200)

the reason I use Handler().postDelayed is that doOnLayout never gets called for me and I couldn't figure out why so instead run the code with 200ms delay until I found something better to work with.

Anthropologist answered 11/3, 2019 at 22:47 Comment(1)
In order to avoid the postDelayed, you can use lastItemView.post { /* Code inside post delayed*/}. And replace view.height instances with view.measuredHeight.Absent
C
1

This worked for me, let the recycler view with layout_height="0dp", and put the top constraint to the bottom of the corresponding above item, and the bottom constraint to the parent, then it will be sized with the remaining space:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ToggleButton
        android:id="@+id/toggleUpcomingButton"
        style="?attr/materialButtonOutlinedStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:gravity="top"
        android:text="@string/upcoming"
        app:layout_constraintEnd_toStartOf="@+id/togglePupularButton"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ToggleButton
        android:id="@+id/togglePupularButton"
        style="?attr/materialButtonOutlinedStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="top"
        android:text="@string/popular"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/toggleUpcomingButton"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/text_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="@string/popular_movies"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toggleUpcomingButton" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginTop="16dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
Chrominance answered 4/9, 2020 at 2:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.