How to create an endless Pager in Jetpack Compose - android

Box(
modifier = Modifier.size(100.dp),
contentAlignment = Alignment.Center
) {
val pagerState = rememberPagerState()
val items = listOf("A", "B", "C")
androidx.compose.foundation.pager.HorizontalPager(
state = pagerState,
pageCount = items.size,
modifier = Modifier,
verticalAlignment = Alignment.CenterVertically
) { page ->
Text(
text = items[page],
modifier = Modifier
)
}
}
In the above code, the pager stops scrolling after reaching the last item. However, I want the pager to continue scrolling endlessly after reaching the last item.

You can create it by setting pageCount to Int.MAX_VALUE and getting modulus of current page to get index for your list of items.
#Preview
#Composable
private fun Test() {
val pageCount = Int.MAX_VALUE
val items = listOf("A", "B", "C")
val pagerState = rememberPagerState(
initialPage = pageCount / 2
)
HorizontalPager(
modifier = Modifier.fillMaxWidth(),
pageCount = pageCount,
state = pagerState
) {
Text(text = items[it % 3])
}
}

Related

Auto scrolling pager not working properly in Android Jetpack Compose

I am learning jetpack compose.I am trying to implement a viewpager in jetpack compose where 5 image will be auto scrolled after 3 sec just like a carousel banner.Everything is alright before last index item image.After auto scroll to last index ,page should be scrolled to 0 index and will repeat.That's where the problem begain.The pager not working perfectly here .It's reapeting 3-4 index and sometimes stuck between to image/page after first auto scroll.
This is the img
My Code
#OptIn(ExperimentalPagerApi::class)
#Composable
fun HorizontalPagerScreen() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 5.dp)
) {
val items = createItems()
val pagerState = rememberPagerState()
HorizontalPager(
modifier = Modifier
.fillMaxWidth()
.height(250.dp),
count = items.size,
state = pagerState,
verticalAlignment = Alignment.Top,
) { currentPage ->
Image(
painter = rememberAsyncImagePainter(items[currentPage].Image),
contentDescription = items[currentPage].title,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth(),
)
//Here's the code for auto scrolling
LaunchedEffect(key1= Unit, key2= pagerState.currentPage) {
while (true) {
yield()
delay(3000)
var newPage = pagerState.currentPage + 1
if (newPage > items.lastIndex) newPage = 0
pagerState.animateScrollToPage(newPage)
}
}
}
}
}
**How to make it auto scroll for infinite times **
You can create a loopingCount variable that you increment every few seconds using a LaunchedEffect and then mod it with the max amount of pages, you also need to take into account if the user is dragging on the pager or not.
The full code sample can be found here, but added below too:
#Composable
fun HorizontalPagerLoopingIndicatorSample() {
Scaffold(
modifier = Modifier.fillMaxSize()
) { padding ->
Column(
Modifier
.fillMaxSize()
.padding(padding)
) {
// Display 10 items
val pageCount = 10
// We start the pager in the middle of the raw number of pages
val loopingCount = Int.MAX_VALUE
val startIndex = loopingCount / 2
val pagerState = rememberPagerState(initialPage = startIndex)
fun pageMapper(index: Int): Int {
return (index - startIndex).floorMod(pageCount)
}
HorizontalPager(
// Set the raw page count to a really large number
pageCount = loopingCount,
state = pagerState,
// Add 32.dp horizontal padding to 'center' the pages
contentPadding = PaddingValues(horizontal = 32.dp),
// Add some horizontal spacing between items
pageSpacing = 4.dp,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) { index ->
// We calculate the page from the given index
val page = pageMapper(index)
PagerSampleItem(
page = page,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
)
}
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp),
pageCount = pageCount,
pageIndexMapping = ::pageMapper
)
val loopState = remember {
mutableStateOf(true)
}
LoopControl(loopState, Modifier.align(Alignment.CenterHorizontally))
ActionsRow(
pagerState = pagerState,
modifier = Modifier.align(Alignment.CenterHorizontally),
infiniteLoop = true
)
var underDragging by remember {
mutableStateOf(false)
}
LaunchedEffect(key1 = Unit) {
pagerState.interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> underDragging = true
is PressInteraction.Release -> underDragging = false
is PressInteraction.Cancel -> underDragging = false
is DragInteraction.Start -> underDragging = true
is DragInteraction.Stop -> underDragging = false
is DragInteraction.Cancel -> underDragging = false
}
}
}
val looping = loopState.value
if (underDragging.not() && looping) {
LaunchedEffect(key1 = underDragging) {
try {
while (true) {
delay(1000L)
val current = pagerState.currentPage
val currentPos = pageMapper(current)
val nextPage = current + 1
if (underDragging.not()) {
val toPage = nextPage.takeIf { nextPage < pageCount } ?: (currentPos + startIndex + 1)
if (toPage > current) {
pagerState.animateScrollToPage(toPage)
} else {
pagerState.scrollToPage(toPage)
}
}
}
} catch (e: CancellationException) {
Log.i("page", "Launched paging cancelled")
}
}
}
}
}
}
#Composable
fun LoopControl(
loopState: MutableState<Boolean>,
modifier: Modifier = Modifier,
) {
IconButton(
onClick = { loopState.value = loopState.value.not() },
modifier = modifier
) {
val icon = if (loopState.value) {
Icons.Default.PauseCircle
} else {
Icons.Default.PlayCircle
}
Icon(imageVector = icon, contentDescription = null)
}
}
private fun Int.floorMod(other: Int): Int = when (other) {
0 -> this
else -> this - floorDiv(other) * other
}

scroll lags when trying to listen to scroll position in android jetpack compose

I'm using Jetpack compose in my project. I have a scrollable column. I want to show a column as the top bar when the user scrolls the screen. For this purpose, I listen to the state of the scroll in this way:
val scrollState = rememberScrollState()
Box {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(scrollState)
) {
...
...
...
}
TopBar(scrollOffset = (scrollState.value * 0.1))
}
and the TopBar is another composable:
#Composable
fun HiddenTopBar(scrollOffset: Double, onSearchListener: () -> Unit) {
val offset = if (-50 + scrollOffset < 0) (-50 + scrollOffset).dp else 0.dp
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.offset(y = offset)
.background(MaterialTheme.colors.secondary)
.padding(vertical = MaterialTheme.space.small)
) {
...
...
...
}
}
The problem is that due to constant recomposition, the scroll lags, and it is not smooth. Is there any way I can implement it more efficiently?
Yes, it's because of constant recomposition in performance documentation.
If you were checking a state derived from scroll state such as if it's scrolled you could go for derivedState but you need it on each change, nestedScrollConnection might help i guess.
This sample might help you how to implement it
#Composable
private fun NestedScrollExample() {
val density = LocalDensity.current
val statusBarTop = WindowInsets.statusBars.getTop(density)
val toolbarHeight = 100.dp
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
// our offset to collapse toolbar
val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = toolbarOffsetHeightPx.value + delta
toolbarOffsetHeightPx.value =
newOffset.coerceIn(-(2 * statusBarTop + toolbarHeightPx), 0f)
return Offset.Zero
}
}
}
Box(
Modifier
.fillMaxSize()
// attach as a parent to the nested scroll system
.nestedScroll(nestedScrollConnection)
) {
Column(
modifier = Modifier
.padding(
PaddingValues(
top = toolbarHeight + 8.dp,
start = 8.dp,
end = 8.dp,
bottom = 8.dp
)
)
.verticalScroll(rememberScrollState())
,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(modifier = Modifier
.fillMaxWidth()
.height(2000.dp))
}
TopAppBar(modifier = Modifier
.height(toolbarHeight)
.offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) },
elevation = 2.dp,
backgroundColor = Color.White,
title = { Text("toolbar offset is ${toolbarOffsetHeightPx.value}") })
}
}

How to create dot indicator (with color and size transiton) in Jetpack Compose

I want to have a horizontal dot indicator that has color transition between two dots that is scrolling and also dot's size transition while scrolling
I need to show only limited dots for a huge amount of items.
In view system, we used this library https://github.com/Tinkoff/ScrollingPagerIndicator, which is very smooth and has a very nice color and size transition effects.
I tried to implement it with scroll state rememberLazyListState(), but it is more complex than I thought.
Do you know any solution in Jetpack Compose?
Is it possible to use the current library with AndroidView? Because it needs XML view, recycler view and viewpager, I am wondering how is it possible to use it with AndroidView?
I made a sample looks similar, logic for scaling is raw but it looks similar.
#OptIn(ExperimentalPagerApi::class)
#Composable
fun PagerIndicator(
modifier: Modifier = Modifier,
pagerState: PagerState,
indicatorCount: Int = 5,
indicatorSize: Dp = 16.dp,
indicatorShape: Shape = CircleShape,
space: Dp = 8.dp,
activeColor: Color = Color(0xffEC407A),
inActiveColor: Color = Color.LightGray,
orientation: IndicatorOrientation = IndicatorOrientation.Horizontal,
onClick: ((Int) -> Unit)? = null
) {
val listState = rememberLazyListState()
val totalWidth: Dp = indicatorSize * indicatorCount + space * (indicatorCount - 1)
val widthInPx = LocalDensity.current.run { indicatorSize.toPx() }
val currentItem by remember {
derivedStateOf {
pagerState.currentPage
}
}
val itemCount = pagerState.pageCount
LaunchedEffect(key1 = currentItem) {
val viewportSize = listState.layoutInfo.viewportSize
if (orientation == IndicatorOrientation.Horizontal) {
listState.animateScrollToItem(
currentItem,
(widthInPx / 2 - viewportSize.width / 2).toInt()
)
} else {
listState.animateScrollToItem(
currentItem,
(widthInPx / 2 - viewportSize.height / 2).toInt()
)
}
}
if (orientation == IndicatorOrientation.Horizontal) {
LazyRow(
modifier = modifier.width(totalWidth),
state = listState,
contentPadding = PaddingValues(vertical = space),
horizontalArrangement = Arrangement.spacedBy(space),
userScrollEnabled = false
) {
indicatorItems(
itemCount,
currentItem,
indicatorCount,
indicatorShape,
activeColor,
inActiveColor,
indicatorSize,
onClick
)
}
} else {
LazyColumn(
modifier = modifier.height(totalWidth),
state = listState,
contentPadding = PaddingValues(horizontal = space),
verticalArrangement = Arrangement.spacedBy(space),
userScrollEnabled = false
) {
indicatorItems(
itemCount,
currentItem,
indicatorCount,
indicatorShape,
activeColor,
inActiveColor,
indicatorSize,
onClick
)
}
}
}
private fun LazyListScope.indicatorItems(
itemCount: Int,
currentItem: Int,
indicatorCount: Int,
indicatorShape: Shape,
activeColor: Color,
inActiveColor: Color,
indicatorSize: Dp,
onClick: ((Int) -> Unit)?
) {
items(itemCount) { index ->
val isSelected = (index == currentItem)
// Index of item in center when odd number of indicators are set
// for 5 indicators this is 2nd indicator place
val centerItemIndex = indicatorCount / 2
val right1 =
(currentItem < centerItemIndex &&
index >= indicatorCount - 1)
val right2 =
(currentItem >= centerItemIndex &&
index >= currentItem + centerItemIndex &&
index < itemCount - centerItemIndex + 1)
val isRightEdgeItem = right1 || right2
// Check if this item's distance to center item is smaller than half size of
// the indicator count when current indicator at the center or
// when we reach the end of list. End of the list only one item is on edge
// with 10 items and 7 indicators
// 7-3= 4th item can be the first valid left edge item and
val isLeftEdgeItem =
index <= currentItem - centerItemIndex &&
currentItem > centerItemIndex &&
index < itemCount - indicatorCount + 1
Box(
modifier = Modifier
.graphicsLayer {
val scale = if (isSelected) {
1f
} else if (isLeftEdgeItem || isRightEdgeItem) {
.5f
} else {
.8f
}
scaleX = scale
scaleY = scale
}
.clip(indicatorShape)
.size(indicatorSize)
.background(
if (isSelected) activeColor else inActiveColor,
indicatorShape
)
.then(
if (onClick != null) {
Modifier
.clickable {
onClick.invoke(index)
}
} else Modifier
)
)
}
}
enum class IndicatorOrientation {
Horizontal, Vertical
}
Usage
#Composable
private fun PagerIndicatorSample() {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(40.dp))
val pagerState1 = rememberPagerState(initialPage = 0)
val coroutineScope = rememberCoroutineScope()
PagerIndicator(pagerState = pagerState1) {
coroutineScope.launch {
pagerState1.scrollToPage(it)
}
}
HorizontalPager(
count = 10,
state = pagerState1,
) {
Box(
modifier = Modifier
.padding(10.dp)
.shadow(1.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.height(200.dp),
contentAlignment = Alignment.Center
) {
Text(
"Text $it",
fontSize = 40.sp,
color = Color.Gray
)
}
}
val pagerState2 = rememberPagerState(initialPage = 0)
PagerIndicator(
pagerState = pagerState2,
indicatorSize = 24.dp,
indicatorCount = 7,
activeColor = Color(0xffFFC107),
inActiveColor = Color(0xffFFECB3),
indicatorShape = CutCornerShape(10.dp)
)
HorizontalPager(
count = 10,
state = pagerState2,
) {
Box(
modifier = Modifier
.padding(10.dp)
.shadow(1.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.height(200.dp),
contentAlignment = Alignment.Center
) {
Text(
"Text $it",
fontSize = 40.sp,
color = Color.Gray
)
}
}
Row(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
) {
val pagerState3 = rememberPagerState(initialPage = 0)
Spacer(modifier = Modifier.width(10.dp))
PagerIndicator(
pagerState = pagerState3,
orientation = IndicatorOrientation.Vertical
)
Spacer(modifier = Modifier.width(20.dp))
VerticalPager(
count = 10,
state = pagerState3,
) {
Box(
modifier = Modifier
.padding(10.dp)
.shadow(1.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.height(200.dp),
contentAlignment = Alignment.Center
) {
Text(
"Text $it",
fontSize = 40.sp,
color = Color.Gray
)
}
}
}
}
}
Need to convert from
listState.animateScrollToItem()
to
listState.animateScrollBy()
for smooth indicator change and moving with offset change from Pager.
and do some more methodical scale and color and offset calculating this one is only a temporary solution.
I could integrate this library https://github.com/Tinkoff/ScrollingPagerIndicator easily with compose.
#Composable
fun DotIndicator(scrollState: LazyListState, modifier: Modifier = Modifier) {
AndroidViewBinding(
modifier = modifier,
factory = DotIndicatorBinding::inflate,
) {
dotIndicator.setDotCount(scrollState.layoutInfo.totalItemsCount)
dotIndicator.setCurrentPosition(scrollState.firstVisibleItemIndex)
scrollState.layoutInfo.visibleItemsInfo.firstOrNull()?.size?.let { firstItemSize ->
val firstItemOffset = scrollState.firstVisibleItemScrollOffset
val offset = (firstItemOffset.toFloat() / firstItemSize.toFloat()).coerceIn(0f, 1f)
dotIndicator.onPageScrolled(scrollState.firstVisibleItemIndex, offset)
}
}
}
For the integration I had to add XML file as well ->
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data />
<ru.tinkoff.scrollingpagerindicator.ScrollingPagerIndicator
android:id="#+id/dot_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:spi_dotColor="#color/ds_primary_25"
app:spi_dotSelectedColor="?attr/colorPrimary"
app:spi_dotSelectedSize="8dp"
app:spi_dotSize="8dp"
app:spi_dotSpacing="4dp" />
</layout>
And also adding this dependency to your Gradle file ->
api "androidx.compose.ui:ui-viewbinding:1.1.1"

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

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

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

Categories

Resources