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
}
)
}
}
Related
I have a lazyRow and I want to show list of indicators:
what I want: I want to show 6 items and when user scrolls other indicators get visible.
#Composable
private fun ImagesDotsIndicator(
modifier: Modifier,
totalDots: Int,
selectedIndex: Int
) {
LazyRow(
modifier = modifier,
horizontalArrangement = Arrangement.Center,
reverseLayout = true,
verticalAlignment = Alignment.CenterVertically
) {
if (totalDots == 1) return#LazyRow
items(totalDots) { index ->
if (index == selectedIndex) {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(color = Color.White)
)
} else {
Box(
modifier = Modifier
.size(6.dp)
.clip(CircleShape)
.background(color = Color.LightGray)
)
}
Spacer(modifier = Modifier.padding(horizontal = 2.dp))
}
}
}
how can I make this indicator?
I would suggest you use Google's Accompanist HorizontalPager and HorizontalPagerIndicator if you want to swipe pages and show the dots. This is a layout that lays out items in a horizontal row, and allows the user to horizontally swipe between pages and also show the page indicator.
You need to add these 2 lines to your app build gradle file to add the dependencies.
// Horizontal Pager and Indicators - Accompanist
implementation "com.google.accompanist:accompanist-pager:0.24.7-alpha"
implementation "com.google.accompanist:accompanist-pager-indicators:0.24.7-alpha"
On your composable file, you can add a simple Sealed class to hold the data that you want to display e.g. text.
sealed class CustomDisplayItem(val text1:String, val text2: String){
object FirstItem: CustomDisplayItem("Hi", "World")
object SecondItem: CustomDisplayItem("Hello", "I'm John")
}
Thereafter make a template of the composable element or page that you want to show if the user swipes left or right.
#Composable
fun DisplayItemTemplate(item: CustomDisplayItem) {
Column() {
Text(text = item.text2 )
Spacer(modifier = Modifier.height(4.dp))
Text(text = item.text2)
}
}
Lastly use HorizontalPager and HorizontalPageIndicator composables to display the corresponding page when a user swipes back and forth.
#OptIn(ExperimentalPagerApi::class)
#Composable
fun ImagesDotsIndicator(
modifier: Modifier,
) {
//list of pages to display
val displayItems = listOf(CustomDisplayItem.FirstItem, CustomDisplayItem.SecondItem)
val state = rememberPagerState()
Column(modifier = modifier.fillMaxSize()) {
//A horizontally scrolling layout that allows users to
// flip between items to the left and right.
HorizontalPager(
count = 6,
state = state,
) {
/*whenever we scroll sideways the page variable changes
displaying the corresponding page */
item ->
//call template item and add the data
DisplayItemTemplate(item = displayItems[item])
}
//HorizontalPagerIndicator dots
HorizontalPagerIndicator(
pagerState = state,
activeColor = MaterialTheme.colors.primary,
inactiveColor = Color.Gray,
indicatorWidth = 16.dp,
indicatorShape = CircleShape,
spacing = 8.dp,
modifier = Modifier
.weight(.1f)
.align(CenterHorizontally)
)
}
}
Please see the above links to read more on how you can customize your composables to work in your case.
Actually it is preaty straight forward without any additional library:
val list = (0..100).toList()
val state = rememberLazyListState()
val visibleIndex by remember {
derivedStateOf {
state.firstVisibleItemIndex
}
}
Text(text = visibleIndex.toString())
LazyColumn(state = state) {
items(list) { item ->
Text(text = item.toString())
}
}
Create scroll state and use it on your list, and on created scroll state observe first visible item.
I am trying to create a infinite scrollable carousel of cards using modified version of this solution
#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(
count = Int.MAX_VALUE,
itemContent = {
val index = it % items.size // line X
Text(text = items[index])
}
)
}
}
However, this crashes with NPE:
java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter it
at androidx.compose.foundation.lazy.LazyListItemProviderImplKt$generateKeyToIndexMap$1$1.invoke(Unknown Source:2)
This will also crash, if i use a smaller index of 500 for example, but it doesn't crash, if i use 50
val listState = rememberLazyListState(500) // Crashes
val listState = rememberLazyListState(50) // Doesn't crash
What could possible cause this?
EDIT1: I have tried debugging it with breakpoints and it crashes before it stops on line X. Items list is not empty, usual test size is 2-6 objects.
EDIT2: Also tried using scrollToItem() instead to set the initial index, but that returned the same exception
I never did understand why this problem occurred, but here's a solution (or workaround/hack) how to solve it:
#Composable
fun CircularList(
items: List<String>,
modifier: Modifier = Modifier,
onItemClick: (String) -> Unit
) {
val listState = rememberLazyListState(Int.MAX_VALUE / 4) // Use smaller index for smaller count
LazyColumn(
state = listState,
modifier = modifier
) {
items(
count = Int.MAX_VALUE / 2, // Use smaller count, somewhere around 1B
itemContent = {
val index = it % items.size
Text(text = items[index])
}
)
}
}
For some reason, this worked without a problem.
I have been trying to make it so that the first item on a LazyRow is centered in the middle of the screen as soon as the app is opened. The snapping behavior works once it is scrolled, but when the app is initially opened, the first item is not centered on the page.
LazyRow with snapper and infinite scrolling
#OptIn(ExperimentalSnapperApi::class)
#Composable
fun CircularList(
data: List<SingleBox>,
modifier: Modifier = Modifier,
isEndless: Boolean = true
) {
val listState = rememberLazyListState(
if (isEndless) Int.MAX_VALUE / 2 else 0
)
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
val contentPadding = PaddingValues(horizontal = screenWidth / 2) //This moves starting point of item horizontally
BoxWithConstraints {
LazyRow(
state = listState,
modifier = modifier,
contentPadding = contentPadding,
flingBehavior = rememberSnapperFlingBehavior(listState, SnapOffsets.Center, snapIndex = { _, startIndex, targetIndex ->
targetIndex.coerceIn(startIndex - 7, startIndex + 7) //This snaps item to center of page when LazyRow stops moving
})
)
{
items(
count = if (isEndless) Int.MAX_VALUE else data.size, //This makes it scroll infinitly
itemContent = {
val index = it % data.size
CustomItem(data[index]) // item composable
},
)
}
}
}
How it looks when opened
How it should look when app is opened
I think the best option here would be the Accompanist Pager Library.
My jetpack compose app uses a LazyColumn to display data from history. When I am at the beginning of the LazyColumn and data is entered into the database, it will automatically be displayed on top with scrollState.scrollToItem(0, 0) each time. In this case, if I scroll down the list, exit the screen, and then return to it, then I will remain in the same place (at the bottom of the list). I need to fix this so that every time I enter the screen, I end up at the beginning of the list (LazyColumn).
#Composable
fun HistoryTableList(
viewModel: HistoryViewModel = viewModel()
) {
val scrollState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
LazyColumn(
state = scrollState,
modifier = Modifier
.padding(
HistoryListHorizontalPadding,
0.dp,
HistoryListHorizontalPadding,
HistoryListPaddingBottom
)
.fillMaxWidth(),
) {
items(
items = historyItems,
key = { historyRecord ->
historyRecord.uid
}
) { historyRecord ->
historyRecord?.let {
viewModel.onListScrolled(scrollState.firstVisibleItemScrollOffset)
if (scrollState.firstVisibleItemIndex <= 1) {
coroutineScope.launch {
scrollState.scrollToItem(0, 0)
}
}
HistoryTableItem(history = historyRecord)
}
}
}
}
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
}
)
}
}