User experience plays a very crucial role in letting users interact with your application more which results in greater user retention. Today we are going to discuss about very often used UI design called overlapping list.
These types of lists are widely used in most applications and have a variety of use cases. And we will build this using both the available design systems, i.e.; using the Recycler View way and the Jetpack Compose way.
First, let’s explore the Recycler View way
As we can see we need to do two steps to achieve the overlapping behaviour,
1. Create a horizontal list
2. Shift each item (starting from the second position) up to a certain percentage to the left.
Creating a list is pretty straightforward, so we will focus on the second point. So we will shift each item by some percentage using RecyclerView Item Decoration. For that, we need to create a custom item decoration class.
class OverlappingDecoration: RecyclerView.ItemDecoration() {
private val overlapPercentage = 15
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val width = view.layoutParams.width
val widthToShift = (overlapPercentage * width * -1) / 100
val position = parent.getChildAdapterPosition(view)
val isReversed = (parent.layoutManager as? LinearLayoutManager)?.reverseLayout ?: false
if (position == 0) {
return
} else {
if (isReversed) {
outRect.set(0, 0, widthToShift, 0)
} else {
outRect.set(widthToShift, 0, 0, 0)
}
}
}
For each view we are first calculating its width and then the widthToShift which is the pixels of offsets that needed to be shifted to overlap on the previous item.
outRect.set(widthToShift, 0, 0, 0) is used to for shifting view to left.
Similarly, in case stacking items is reversed, we are shifting the view to the right side with outRect.set(0, 0, widthToShift, 0). Also, we don’t need to do this for the first item in the list as it doesn’t need to be overlapped.
Now we can add this decoration to our recylerview and it will work like a charm.
Job Offers
Now let’s see the Compose way
In compose, we have Lazy Row/Column as an alternate for RecyclerView. We play with Arrangement.spacedBy((-1 *x).dp) but this will apply spacing to every item of the list but we want to skip for the first.
Also, we might play with some if-else to achieve the same but here I preferred to go with creating a Custom Composable.
@Composable
fun OverlappingRow(
modifier: Modifier = Modifier,
overlappingPercentage: Float,
content: @Composable () -> Unit
) {
val factor = (1 - overlappingPercentage)
Layout(
modifier = modifier,
content = content,
measurePolicy = { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val widthsExceptFirst = placeables.subList(1, placeables.size).sumOf { it.width }
val firstWidth = placeables.getOrNull(0)?.width ?: 0
val width = (widthsExceptFirst * factor + firstWidth).toInt()
val height = placeables.maxOf { it.height }
layout(width, height) {
var x = 0
for (placeable in placeables) {
placeable.placeRelative(x, 0, 0f)
x += (placeable.width * factor).toInt()
}
}
}
)
}
Here we have defined a factor which determines the width of the item after the next item will overlap on itself, that’s why we have to subtract it from 1. Now for a custom layout in compose, we need to define a measurePolicy which determines the size and coordinates of the composables.
The measure policy has two properties measurables and constraints. As the name suggests measurables are a list of compositions that can be measured and constraints define the range in pixels in which the measured layout would choose a size to render the composition.
Using measurables and constraints, we get the placeables which are the children’s layouts that can be positioned by the parent layout.
A measurePolicy requires a width and height to determine the size and position of each placeable.
In our case, getting the height is straightforward, i.e.; it will be the height of the tallest placeable.
To find the width, we need to first get the sum of the width of all items except the first item. Then we need to multiply it with the factor that we calculated (since that will affect the overall width of the layout) and then we will add the width of the first item to it.
Now while placing the placeables on the layout we need to make sure that the x-position of each placeable is adjacent and for overlapping we need to adjust the width with factor as well.
We can now use our OverlappingRow composable inside a LazyRow to provide the scroll behaviour to our layout.
LazyRow {
item {
Spacer(modifier = modifier.width(16.dp))
}
item {
OverlappingRow(overlappingPercentage = 0.20f) {
for (i in images) {
Image(
painter = painterResource(id = i),
contentDescription = "image_$i",
contentScale = ContentScale.Crop,
modifier = modifier
.size(100.dp)
.clip(CircleShape)
.border(4.dp, Color(0xFFFFA0A0), CircleShape)
)
}
}
}
item {
Spacer(modifier = modifier.width(16.dp))
}
}
Let’s see a demo for both ways
Using these approaches we can define much more use cases and enhance the user experience in a better way.
That’s it for this blog. Let’s connect on LinkedIn and Twitter
You can find the source code in the following repository
https://github.com/raystatic/OverlappingLists?source=post_page—–9d0655c5a20e——————————–
This article is previously published on proandroiddev.com