How to create a circular (Endless) Lazycolumn/LazyRow in Compose - android

How to achieve infinite like list in Lazycolumn/LazyRow.When scrolled to the end, I would like to views to be visible while the displaying data from the top of the list or when scrolled to the top of the list I would display data from the bottom of the list.

I think something like this can work:
#Composable
fun CircularList(
items: List<String>,
modifier: Modifier = Modifier,
onItemClick: (String) -> Unit
) {
val listState = rememberLazyListState(Int.MAX_VALUE / 2)
LazyColumn(
state = listState,
modifier = modifier
) {
items(Int.MAX_VALUE, itemContent = {
val index = it % items.size
Text(text = items[index]) // item composable
})
}
}

In addition to the previous answer, you can make it customizable to support both variants of the list.
#Composable
fun CircularList(
items: List<String>,
modifier: Modifier = Modifier,
isEndless: Boolean = false,
onItemClick: (String) -> Unit
) {
val listState = rememberLazyListState(
if (isEndless) Int.MAX_VALUE / 2 else 0
)
LazyColumn(
state = listState,
modifier = modifier
) {
items(
count = if (isEndless) Int.MAX_VALUE else items.size,
itemContent = {
val index = it % items.size
Text(text = items[index]) // item composable
}
)
}
}

Related

Jetpack Compose LazyRow - Center selected item

How can I calculate the position of the selected item in a LazyRow and use it for centering the item on the screen?
The items will have various sizes depending on their content.
If I go with lazyListState.animateScrollToItem(index) the selected item will be positioned to the left in the LazyRow.
What I want to achieve is to have the selected item centered like this
The code I have currently:
#Composable
fun Test() {
Column {
TestRow(listOf("Angola", "Bahrain", "Afghanistan", "Denmark", "Egypt", "El Salvador", "Fiji", "Japan", "Kazakhstan", "Kuwait", "Laos", "Mongolia"))
TestRow(listOf("Angola", "Bahrain", "Afghanistan"))
}
}
#OptIn(ExperimentalSnapperApi::class)
#Composable
fun TestRow(items: List<String>) {
val selectedIndex = remember { mutableStateOf(0) }
val lazyListState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyRow(
modifier = Modifier.fillMaxWidth(),
state = lazyListState,
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
contentPadding = PaddingValues(horizontal = 12.dp),
flingBehavior = rememberSnapperFlingBehavior(
lazyListState = lazyListState,
snapOffsetForItem = SnapOffsets.Start
)
) {
itemsIndexed(items) { index, item ->
TestItem(
content = item,
isSelected = index == selectedIndex.value,
onClick = {
selectedIndex.value = index
coroutineScope.launch {
lazyListState.animateScrollToItem(index)
}
}
)
}
}
}
#Composable
fun TestItem(
content: String,
isSelected: Boolean,
onClick: () -> Unit
) {
Button(
modifier = Modifier.height(40.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = if (isSelected) Color.Green else Color.Yellow,
contentColor = Color.Black
),
elevation = null,
shape = RoundedCornerShape(5.dp),
contentPadding = PaddingValues(0.dp),
onClick = onClick
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),
text = (content).uppercase(),
)
}
}
The code horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) is to make sure the LazyRow centers all the items, when there is not enough items for scrolling.
Example with 3 items:
I have tried various suggestions posted at similar questions, but with no luck in finding a solution.
jetpack-compose-lazylist-possible-to-zoom-the-center-item
how-to-focus-a-invisible-item-in-lazycolumn-on-jetpackcompose
/jetpack-compose-make-the-first-element-in-a-lazyrow-be-aligned-to-the-center-o
I found a solution that fits my needs, since I don't expect to navigate to a selected item, without being able to see it.
This solution can't center items that is not visible on the screen.
coroutineScope.launch {
val itemInfo = lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
if (itemInfo != null) {
val center = lazyListState.layoutInfo.viewportEndOffset / 2
val childCenter = itemInfo.offset + itemInfo.size / 2
lazyListState.animateScrollBy((childCenter - center).toFloat())
} else {
lazyListState.animateScrollToItem(index)
}
}
Hope t
Improving the #Markram answer
You can create an extension of "LazyListState":
fun LazyListState.animateScrollAndCentralizeItem(index: Int, scope: CoroutineScope) {
val itemInfo = this.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }
scope.launch {
if (itemInfo != null) {
val center = this#animateScrollAndCentralizeItem.layoutInfo.viewportEndOffset / 2
val childCenter = itemInfo.offset + itemInfo.size / 2
this#animateScrollAndCentralizeItem.animateScrollBy((childCenter - center).toFloat())
} else {
this#animateScrollAndCentralizeItem.animateScrollToItem(index)
}
}
}
And then just use it directly in your Composable:
val coroutineScope = rememberCoroutineScope()
coroutineScope.launch {
listState.animateScrollAndCentralizeItem(index , this)
}

Make last Item of the Compose LazyColumn fill rest of the screen

There is a regular list with some data and LazyColumn for showing it.
LazyColumn {
items (list) { item ->
ListItem(item)
}
}
Simplified ListItem looks like:
#Composable
fun ListItem(item: SomeItem) {
Row(
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)
) {
//Some widgets
}
}
I need to set the footer's item (last in list) height to fill screen if there is not enough items for that. It's easy to find last item and provide some flag to ListItem(item: SomeItem, isLast: Boolean), but I don't know how to set the item's height to achieve my goal. Did anyone faced this problem?
It's almost the same question as was asked here about RecyclerView. And the image from that question illustatrates it.
If your items are all same height and you have have info of how tall they are via static height or using Modifier.onSizeChanged{} you can do this by getting height of the LazyColumn using BoxWithConstraints maxHeight or rememberLazyListState().layoutInfo.viewportSize
#Composable
private fun ListComposable() {
val myItems = mutableListOf<String>()
repeat(4) {
myItems.add("Item $it")
}
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.border(2.dp, Color.Cyan)
) {
itemsIndexed(myItems) { index: Int, item: String ->
if (index == myItems.size - 1) {
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height((maxHeight - 50.dp * (myItems.size - 1)).coerceAtLeast(50.dp))
.background(Color.Green)
)
} else {
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Red)
)
}
}
}
}
}
If you don't know total height until last item you will need to use SubcomposeLayout with list that doesn't contain last item and pass LazyColumn height- (items.size-1) total height as your last item's height.
#Composable
fun DimensionMeasureSubcomposeLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (IntSize, Constraints) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints)
}
// Get max width and height of main component
var maxWidth = 0
var maxHeight = 0
mainPlaceables.forEach { placeable: Placeable ->
maxWidth += placeable.width
maxHeight = placeable.height
}
val maxSize = IntSize(maxWidth, maxHeight)
val dependentPlaceables = subcompose(SlotsEnum.Dependent) {
dependentContent(maxSize, constraints)
}.map {
it.measure(constraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
dependentPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
And use it as
#Composable
private fun SubcomposeExample() {
val myItems = mutableListOf<String>()
repeat(15) {
myItems.add("Item $it")
}
val subList = myItems.subList(0, myItems.size - 1)
val lasItem = myItems.last()
val density = LocalDensity.current
DimensionMeasureSubcomposeLayout(
modifier = Modifier,
mainContent = {
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
items(subList) { item: String ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Red)
)
}
}
},
dependentContent = { intSize, constraints ->
val lastItemHeight = with(density) {
(constraints.maxHeight - intSize.height).toDp().coerceAtLeast(50.dp)
}
LazyColumn(modifier = Modifier.fillMaxWidth()) {
if (myItems.size > 1) {
items(subList) { item: String ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Red)
)
}
}
item {
Text(
text = lasItem,
modifier = Modifier
.fillMaxWidth()
.height(lastItemHeight)
.background(Color.Green)
)
}
}
}
)
}

How to create vertical infinite carousel in Jetpack Compose?

I'm trying to build a scrollable column (preferably LazyColumn) that will start re-showing the first items again after I scroll to the end. For example, see this alarm clock that will cycle from 00..59 and then will smoothly keep scrolling from 0 again.
I've tried a normal LazyColumn that will show 58,59,00..59,00,01 and snap to start after I'm done scrolling (reaching 59) but it looks "cheap".
#Composable
fun CircularList(
items: List<String>,
modifier: Modifier = Modifier,
isEndless: Boolean = false,
onItemClick: (String) -> Unit
) {
val listState = rememberLazyListState(
if (isEndless) Int.MAX_VALUE / 2 else 0
)
LazyColumn(
state = listState,
modifier = modifier
) {
items(
count = if (isEndless) Int.MAX_VALUE else items.size,
itemContent = {
val index = it % items.size
Text(text = items[index]) // item composable
}
)
}
}

State hoisting for each item in LazyColumn

I've gone through this codelab. In step number 7, when clicking on single row's text it's changing its color, but function will not keep track of it, meaning it will disappear after re-composition.
I want list to remember color of single item thus I've move state hoisting to the NameList function level.
Unfortunately it's not working.
Where's the bug?
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(items = names) { name, ->
val isSelected = remember { mutableStateOf(false)}
Greeting(name = name,isSelected.value){ newSelected -> isSelected.value = newSelected}
Divider(color = Color.Black)
}
}
}
#Composable
fun Greeting(name: String,isSelected : Boolean, updateSelected : (Boolean) -> Unit) {
val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
Text(
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = { updateSelected(!isSelected)}),
text = "Hello $name",
)
}
You should hoist your selection state to the caller of NameList function.
#Composable
fun MyScreen() {
// Fake list of names
val namesList = (1..100).map { "Item $it" }
// Here, we're keeping the selected positions.
// At the beginning, all names are not selected.
val selection = remember {
mutableStateListOf(*namesList.map { false }.toTypedArray())
}
NameList(
// list of names
names = namesList,
// list of selected items
selectedItems = selection,
// this function will update the list above
onSelected = { index, selected -> selection[index] = selected },
// just to occupy the whole screen
modifier = Modifier.fillMaxSize()
)
}
Then, your NameList will look like this:
#Composable
fun NameList(
names: List<String>,
selectedItems: List<Boolean>,
onSelected: (index: Int, selected: Boolean) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
itemsIndexed(items = names) { index, name ->
Greeting(
name = name,
isSelected = selectedItems[index],
updateSelected = { onSelected(index, it) }
)
Divider(color = Color.Black)
}
}
}
Nothing changes on Greeting function.
Here is the result:

Scroll Multiple LazyRows together - LazyHorizontalGrid alike?

How to assign the same scroll state to two LazyRows, so that both row scrolls together?
Jetpack compose lists currently doesn't have LazyHorizontalGrid, So any alternative solution?
Column{
LazyRow(
modifier = Modifier.fillMaxWidth()
) {
// My sublist1
}
LazyRow(
modifier = Modifier.fillMaxWidth()
) {
// My sublist2
}
}
Trying to implement below:
Update: Google has added the component officially - LazyHorizontalGrid.
I modified the LazyVerticalGrid class, and made it work towards only GridCells.Fixed(n) horizontal grid.
Here is the complete gist code: LazyHorizontalGrid.kt
Main changes
#Composable
#ExperimentalFoundationApi
private fun FixedLazyGrid(
nRows: Int,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
scope: LazyGridScopeImpl
) {
val columns = (scope.totalSize + nRows - 1) / nRows
LazyRow(
modifier = modifier,
state = state,
contentPadding = contentPadding,
) {
items(columns) { columnIndex ->
Column {
for (rowIndex in 0 until nRows) {
val itemIndex = columnIndex * nRows + rowIndex
if (itemIndex < scope.totalSize) {
Box(
modifier = Modifier.wrapContentSize(),
propagateMinConstraints = true
) {
scope.contentFor(itemIndex, this#items).invoke()
}
} else {
Spacer(Modifier.weight(1f, fill = true))
}
}
}
}
}
}
Code Usage
LazyHorizontalGrid(
cells = GridCells.Fixed(2)
) {
items(items = restaurantsList){
RestaurantItem(r = it, modifier = Modifier.fillParentMaxWidth(0.8f))
}
}

Categories

Resources