Design a scrollable list of multiple textviews per line - android

I'm developing a music collection app, and I'm struggling with my current goal :Designing a scrollable list of 3 textviews - track number, track title and track duration - which will represent songs in an album. I want the list's layout to be composed of 3 columns with fixed width,aligned as one would expect, without the title textview for a particular track, for example, stretching over above the duration textview of the track below.
I have considered the following options:
Using one RecyclerView with its ViewHolder layout composed of a horizontal LinearLayout of 3 textviews. The problem here is that I don't know how to keep the columns aligned this way. Should I set fixed widths for the textviews using definite dps? How then can I set the entire row to fill the device's entire width?
Using 3 RecyclerViews for each column. I've tried this idea halfway, and the columns are aligned nicely, but scrolling one RV doesn't scroll the others, obviously, and to fix that - as I've seen in another question here - I'll need to mess with the scrolling mechanism, and it seems it can still be very prone to errors with the RVs still might manage to get out of sync or the app crashing.
I know there is GridLayout (which I'm not familiar with and don't know if it can solve my problem), but since it's now considered legacy, I would rather refrain from using it.
Above all, I would like to know what way is considered "best practice", assuming there is one, to tackling such a problem.
Thanks!

This is an alternative solution inspired by Farshad Tahmasbi's answer I found here.
In this screenshot you can see the result. (The duration column is empty right now).
mTracksRecyclerView.setAdapter(new TrackListAdapter(getContext(), musicItem.getTracks()));
GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 12);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
int type = position % 3 ;
if (type == 0) {
// Position
return 2;
} else if (type == 1) {
// Title
return 8;
} else {
// Duration
return 2;
}
}
});
mTracksRecyclerView.setLayoutManager(gridLayoutManager);

I tried to create a scrollable list of 3 TextViews for a row.
Like you have mentioned, we can use a RecyclerView as the scrollable list.
activity_main.xml
<ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</ConstraintLayout>
For a list item to have 3 TextViews in a row, I used a GridLayout as follows.
list_item.xml
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3"
android:rowCount="1" >
<TextView
android:id="#+id/trackNumber"
android:layout_columnWeight="1"
android:layout_width="0dp"
android:background="#android:color/holo_red_light" />
<TextView
android:id="#+id/trackTitle"
android:layout_columnWeight="6"
android:layout_width="0dp"
android:ellipsize="end"
android:background="#android:color/holo_green_light" />
<TextView
android:id="#+id/trackDuration"
android:layout_columnWeight="1"
android:layout_width="0dp"
android:background="#android:color/holo_blue_light" />
</GridLayout>
For each TextView a layout_columnWeight value is set to specify the relative proportion of horizontal space that should be allocated for it.
Each TextView's layout_width value is set to 0dp for it to take full width of the allocated space.
But instead of GridLayout, we can use a ConstraintLayout and achieve the same result.
As mentioned above in the question, GridLayout is marked as legacy.
It better to use ConstraintLayout.
list_item.xml
<ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="#+id/trackNumber"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="1"
android:background="#android:color/holo_red_light"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="#id/trackTitle"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/trackTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="6"
android:ellipsize="end"
android:background="#android:color/holo_green_light"
app:layout_constraintStart_toEndOf="#id/trackNumber"
app:layout_constraintEnd_toStartOf="#id/trackDuration"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/trackDuration"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintHorizontal_weight="1"
android:background="#android:color/holo_blue_light"
app:layout_constraintStart_toEndOf="#id/trackTitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ConstraintLayout>
For each TextView a layout_constraintHorizontal_weight value is set to specify the relative proportion of horizontal space that should be allocated for it.
Each TextView's layout_width value is set to 0dp for it to take full width of the allocated space.
This is just a Kotlin class to store track information.
class TrackInfo(
var trackNumber: Int,
var trackTitle: String?,
var durationMinutes: Short,
var durationSeconds: Short
)
This is the Adapter class and ViewHolder class for the RecyclerView.
class TrackInfoAdapter(private val items: List<TrackInfo>, private val mContext: Context) :
RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false)
)
}
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val trackInfo = items[position]
holder.trackNumber.text = trackInfo.trackNumber.toString()
holder.trackTitle.text = trackInfo.trackTitle
// Use String.format() to add leading zero for single digit durationSeconds value
val formattedDurationSeconds = "%02d".format(trackInfo.durationSeconds)
holder.trackDuration.text = "${trackInfo.durationMinutes}:$formattedDurationSeconds"
}
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val trackNumber = view.trackNumber
val trackTitle = view.trackTitle
val trackDuration = view.trackDuration
}
This is the MainActivity.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Prepare list of track information
val trackInfoList = mutableListOf<TrackInfo>().apply {
// track 1
add(
TrackInfo(1, "Lady GaGa - Poker Face",
4, 4)
)
// track 2
add(
TrackInfo(2, "T.I. featuring Rihanna - Live Your Life",
4, 1)
)
// track 3
add(
TrackInfo(3, "Kanye West - Stronger",
5, 11)
)
// track 4
add(
TrackInfo(4, "Leon Haywood - I Wanna Do Something Freaky To You",
6, 0)
)
// track 5
add(
TrackInfo(5, "Hilary Duff - Reach Out",
4, 16)
)
}
recyclerView.layoutManager = LinearLayoutManager(baseContext)
// Set track information list to Adapter of RecyclerView
recyclerView.adapter = TrackInfoAdapter(trackInfoList, baseContext)
}
}
Result :
I hope this answers the first part of your question.
You have mentioned about 'stretching over above the duration textview, when there's a particular track without a title'.
For that, children of a GridLayout can be configured to span multiple cells using android:layout_columnSpan attribute.
But to handle trackTitle null cases and set properties like layout_columnSpan of TextViews accordingly, I think it would be better to write a custom view class for list item.

the best solution to this is using RecyclerView.
1 - you can set the width of the TextViews to match_parent if you want them to fill the width of the screen.
2 - you need only one recycler view that shows a list of a view.
if you can show me a picture of what you want to achieve

Related

RecyclerView - Continuous Columns Layout

I am trying to create a layout where items would follow one another in columns (see image below) but I am not getting there yet. I have tried GridLayoutManager and StaggeredGridLayoutManager - the problem with both neither provides the feature of item flowing into another column and following each other this way. With my current attempt I am trying FlexboxLayoutManager but the result I am getting is always columns with single items instead of the items flowing one after another.
The desired behavior is that the items are located one after another and when the high of the recycler doesn't allow for the full item view it should be broken down to the next column.
Here is what I am trying right now:
mBinding?.activeRecycler?.layoutManager = FlexboxLayoutManager(context).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
alignItems = AlignItems.STRETCH
}
And this is getting me one item per column.
Trying to achieve this:
I highly doubt this is possible.
The RecyclerView, its adapters and its layout managers all are not designed to alter the fundamental form of a view.
Meaning that "splitting" one would not be possible.
The RecyclerView is designed to understand how many views are in sight at the same time, create that many views only and then bind the underlying objects to the views respectively.
Meaning the RecyclerView doesn't "Cut a View in half and displays its halves in different places".
The only way in which a constellation like yours would be possible, was if the layout manager is specifically designed to display one item in multiple views and thereby multiple positions. Which would then allow it to be displayed as you described. However, as I said, that would mean the view 3 in the middle and the view 3 in the last column would be two views being bound to the same object or a copy of it. (Or someone went completely crazy and actually split the view, which I doubt).
I don't believe that any of the standard layout managers are capable of it and I doubt that you can even achieve this without also altering the adapter accordingly, at the very least. Because the adapter basically does the binding so without its help the standard layout managers wouldn't be able to do the double binding as described above.
That being said, this is just a very good guess, going by the principles of the view and its components. I have not read the source code or full description of every layout manager.
The way I understand your problem is like this: You have your current list of data that contains the text fields and you want to show them on the normal way, one list item one view item in recycler view.
But based on your design requirements this is not possible.
My idea to achieve that is like this:
You have to create a new list which will separate one item of the previous list into 2,3 or more items to fit in your columns.
private fun demo() {
val originalList = listOf<String>()
val newScreenSpecificList = mutableListOf<String>()
val columnHeight = 3//example number of lines
val columnWidth = 10//example number of chars
var columnsIndex = 0//index of column
var currentColumnHeight = 0 // current column filled height
originalList.forEach {
if (currentColumnHeight + getTextHeight(it, columnWidth) <= columnHeight) {
newScreenSpecificList.add(it)
currentColumnHeight = currentColumnHeight + getTextHeight(it, columnWidth)
} else {
//here is the part where your text is bigger then your column height so you need to divide it
val textForSpaceLeft = getTextForSpaceLeft(it, columnHeight - currentColumnHeight)
newScreenSpecificList.add(textForSpaceLeft)
currentColumnHeight = currentColumnHeight + getTextHeight(textForSpaceLeft, columnWidth)
if (currentColumnHeight >= columnHeight) {
columnsIndex++
}
if (getTextForNewSpaceLeft(it, columnHeight - currentColumnHeight)){
//continue to repeat logic for new column
//...
}
}
if (currentColumnHeight >= columnHeight) {
columnsIndex++
}
}
}
private fun getTextForSpaceLeft(it: String, spaceLeft: Int): String {
return "it"// return text for the available space
}
private fun getTextForNewSpaceLeft(it: String, spaceLeft: Int): String {
return "new column also"// return text left for the new available space
}
private fun getTextHeight(text: String, columnWidth: Int): Int {
return 2//todo your logic to convert text length to number of lines needed for a specific width of the column
}
Now you need to continue this logic it is not complete, I hope it helps you.
I guess your problem is with the LayoutParams of items which are being created in your adapter. probably the height is set to match_parent in items. You can try to change the LayoutParams of itemViews in your adapter's onCreateViewHolder/onBindViewHolder. Or if the items' heights are kinda tricky to calculate, you can create a customView and try calculate the height in onMeasure and set the height to wrap_content
try to set items' height to wrap_content or if you want to do it in code, something like this:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlexItemViewHolder {
val infatedView = ...
infatedView.layoutParams = FlexboxLayoutManager.LayoutParams(FlexboxLayoutManager.LayoutParams.WRAP_CONTENT, FlexboxLayoutManager.LayoutParams.WRAP_CONTENT)
infatedView.addView(textView)
return FlexItemViewHolder(f)
}

Update layout_margin inside kotlin class for specific elements

So, this should be pretty basic, but I am new to Kotlin. So I basically have the following ImageView inside a RelativeLayout view.
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:srcCompat="#drawable/moon_0"
android:id="#+id/imagen_luna"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_alignParentTop="true" />
The thing is that under certain condition this ImageView can collide visually with another element, so I have to modify the marginStart and the marginEnd properties. I saw in some tutorials that first I must obtain the current layout margins or something like that, but I am not sure why it's so complicated, in Swift I just modify the properties of each margin.
Is there an easy way to achieve this or in case there isn't what would be the easiest way to do this?
UPDATE - Added code in the Adapter
This is the code I have in my adapter for this specific ImageView:
override fun onBindViewHolder(holder: AnoViewHolder, position: Int) {
...
if(respec.nombre_icono_signo != "" && respec.imagen_luna != "") {
val layoutParamsLuna = holder.view?.imagen_luna.layoutParams as RelativeLayout.LayoutParams
layoutParamsLuna.setMargins(1, 8, 15, 8)
holder.view?.imagen_luna.layoutParams = layoutParamsLuna
}
...
}
The thing is that this does nothing and my theory is that the setMargins function accepts left and right margins, not start and end margins.
You can update margin in layoutParams of view
Ex:
val layoutParams= imagen_luna.layoutParams as RelativeLayout.LayoutParams
layoutParams.marginEnd=resources.getDimension(your_dimension).toInt()
layoutParams.marginStart=resources.getDimension(your_dimension).toInt()
layoutParams.bottomMargin=resources.getDimension(your_dimension).toInt()
layoutParams.topMargin=resources.getDimension(your_dimension).toInt()
//or
layoutParams.setMargins(start,top,end,bottom)
imagen_luna.layoutParams=layoutParams
You have to add value for margins in dimension file.because if you set setMargins(1, 8, 15, 8) like this,It's will margin in pixel not in dp.
So use margin values like below.
<dimen name="spacing_normal">16dp</dimen>
<dimen name="spacing_small">8dp</dimen>
<dimen name="spacing_tiny">4dp</dimen>
Update your dapter class like belo
override fun onBindViewHolder(holder: AnoViewHolder, position: Int) {
...
if(respec.nombre_icono_signo != "" && respec.imagen_luna != "") {
val layoutParamsLuna = holder.view?.imagen_luna.layoutParams as RelativeLayout.LayoutParams
layoutParamsLuna.setMargins(context.resources.getDimension(R.dimen.spacing_normal).toInt(),
context.resources.getDimension(R.dimen.spacing_normal).toInt(),
context.resources.getDimension(R.dimen.spacing_normal).toInt(),
context.resources.getDimension(R.dimen.spacing_normal).toInt())
holder.view?.imagen_luna.layoutParams = layoutParamsLuna
}else{
//reset margin for else case because recylerview reuse views so it's will keep previous states
}
...
}

How to Inflate Custom XML Layout into Custom ItemDecoration?

I wish to add a Custom ItemDecoration in my RecyclerView that is a layout defined in a XML file.
So far I was able to inflate the XML and position using canvas.translate (yet, without understanding everything).
Currently I have this code to draw:
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
val left = child.marginLeft
val top = child.top
context?.let {
//Inflate the Layout and set the Values (a text in this case)
val view = LayoutInflater.from(it).inflate(R.layout.my_decoration_layout, parent, false)
val textView = view.findViewById<TextView>(R.id.textView)
textView.text = "This is an ItemDecoration"
//Calculate the Size. Im using "hardcoded" values, but this does not seems to change how the View is rendered
view.measure(1000,1000)
view.layout(0, 0, 1000, 1000)
//Draw. I had to translate the canvas to apply the offset for each "ViewHolder"
canvas.save()
canvas.translate(left.toFloat(), top.toFloat())
view.draw(canvas)
canvas.restore()
}
}
}
The XML is (note the background colors to see the Rendered Area):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#android:color/holo_blue_light">
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/holo_red_light"/>
</LinearLayout>
With this, I could inflate and position my XML between each ViewHolder, but I still have two big issues:
My TextView is drawed over the ViewHolder, so I get here an overlap.
Would be great if this "Draw" operation pushes the ViewHolder.
Even my custom XML layout has it's width as "match_parent", it only wraps the text view.
If possible, Id like to know which exactly what "measure" and "layout" means and how it affect my View "area".
And how to prevent the overlap.
I "solved" the Overlap issue drawing a Text on the Canvas, using
Paint().apply {
color = Color.BLACK
style = Paint.Style.FILL
textSize = 40f
canvas.drawText(year.toString(), left.toFloat(), top.toFloat(), this)
}
But since my layout is a bit more complex, would be nice to understand how to do it with XML layouts.
Thanks

Is there a way to make RecyclerView requiresFadingEdge unaffected by paddingTop and paddingBottom

Currently, I need to use paddingTop and paddingBottom of RecyclerView, as I want to avoid complex space calculation, in my first RecyclerView item and last item.
However, I notice that, requiresFadingEdge effect will be affected as well.
This is my XML
<androidx.recyclerview.widget.RecyclerView
android:requiresFadingEdge="vertical"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:overScrollMode="always"
android:background="?attr/recyclerViewBackground"
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
When paddingTop and paddingBottom is 40dp
As you can see, the fading effect shift down by 40dp, which is not what I want.
When paddingTop and paddingBottom is 0dp
Fading effect looks fine. But, I need to have non-zero paddingTop and paddingBottom, for my RecyclerView.
Is there a way to make RecyclerView's requiresFadingEdge unaffected by paddingTop and paddingBottom?
I found the best and kind of official solution for this. Override this 3 methods of RecyclerView. Which play most important role for fading edge.
First create your recyclerView.
public class MyRecyclerView extends RecyclerView {
// ... required constructor
#Override
protected boolean isPaddingOffsetRequired() {
return true;
}
#Override
protected int getTopPaddingOffset() {
return -getPaddingTop();
}
#Override
protected int getBottomPaddingOffset() {
return getPaddingBottom();
}
}
That's it. Use this recyclerview and you will see fadding edge unaffected.
Following content is just for explanation. If you want to know behind the scene.
To understand how this edge effect is working I dig into the class where android:requiresFadingEdge is used, And I found that it's not handled by RecyclerView instead It's handled by View class which is parent for all view.
In onDraw method of View class I found the code for drawing fade edge by using help of this method isPaddingOffsetRequired. Which used only for handling the fade effect.
According to documentation this method should be overridden by child class If you want to change the behaviour of fading edge. Bydefault It return false. So By returning true we are asking view to apply some offset for edge at the time of view drawing.
Look following snippet of onDraw method of View class to understand the calculation.
final boolean offsetRequired = isPaddingOffsetRequired();
if (offsetRequired) {
paddingLeft += getLeftPaddingOffset();
}
int left = mScrollX + paddingLeft;
int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
int top = mScrollY + getFadeTop(offsetRequired);
int bottom = top + getFadeHeight(offsetRequired);
if (offsetRequired) {
right += getRightPaddingOffset();
bottom += getBottomPaddingOffset();
}
As we can see top variable is initialize using getFadeTop(offsetRequired).
protected int getFadeTop(boolean offsetRequired) {
int top = mPaddingTop;
if (offsetRequired) top += getTopPaddingOffset();
return top;
}
In this method, top is calculated by adding value of topOffSet when offset is needed. So to reverse the effect we need to pass negative value of padding which you are passing. so we need to return -getPaddingTop().
Now for bottom we are not passing negative value because bottom is working on top + height. So passing negative value make fade more shorter from the bottom so we need to add bottom padding to make it proper visible.
You can override this 4 method to play with it. getLeftPaddingOffset(), getRightPaddingOffset(), getTopPaddingOffset(), getBottomPaddingOffset()
I suggest a few solutions
, I hope to be helpful
1- RecyclerView.ItemDecoration (keep requiresFadingEdge)
class ItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration(){
override fun getItemOffsets(outRect: Rect, view: View, parent:
RecyclerView, state: RecyclerView.State?) {
val position = parent.getChildAdapterPosition(view)
when(position)
0 -> { outRect.top = spacing /*40dp*/ }
parent.adapter.itemCount-1 -> { outRect.bottom = spacing /*40dp*/ }
}
2- Multiple ViewHolders (depending on the ViewHolder either keep or remove requiresFadingEdge)
https://stackoverflow.com/a/26245463/5255963
3- Gradient (remove requiresFadingEdge)
Make two gradients with drawables like this:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="linear"
android:angle="90"
android:startColor="#000"
android:endColor="#FFF" />
</shape>
And set background to two views at the top and bottom of the Recyclerview.
There are many ways to achieve that but I preferred below solution that works for me.
You can use a third-party library called Android-FadingEdgeLayout checkout here
Here is a dependency.
implementation 'com.github.bosphere.android-fadingedgelayout:fadingedgelayout:1.0.0'
In yours.xml
<com.bosphere.fadingedgelayout.FadingEdgeLayout
android:id="#+id/fading_edge_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fel_edge="top|left|bottom|right"
app:fel_size_top="40dp"
app:fel_size_bottom="40dp"
app:fel_size_left="0dp"
app:fel_size_right="0dp">
<androidx.recyclerview.widget.RecyclerView
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:overScrollMode="always"
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
</com.bosphere.fadingedgelayout.FadingEdgeLayout>
Change your ReacyclerView property according to your need. below is example image with all sided sades. I hope that will help you.
Credits Android-FadingEdgeLayout

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

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
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).
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)
}
}
}
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.
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>

Categories

Resources