I'm newbie with Jetpack Compose, and I'm making a menu, inside of which there is a LazyRow which uses custom library called Snapper which allows to snap item in the middle of the row.
So, I want to make a feature - when item snaps into the middle, toast message comes out. How shoud I implement this?
Here is my code:
#OptIn(ExperimentalSnapperApi::class)
#Composable
fun MainScreen() {
val context = LocalContext.current
val nums = arrayListOf(1, 2, 3, 4, 5)
val events = arrayListOf("First", "Two", "Three", "Four", "Five")
val secondEvents = arrayListOf("Three Five Six", "Nine Eleven", "Nine Inch Nails")
val lazyListState: LazyListState = rememberLazyListState()
val layoutInfo: LazyListSnapperLayoutInfo = rememberLazyListSnapperLayoutInfo(
lazyListState = lazyListState
)
//var value by remember { mutableStateOf(layoutInfo.currentItem?.index) }
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Green)
) {
val cntxt = LocalContext.current
Row(modifier = Modifier
.background(Color.Red)
.fillMaxWidth()
.weight(1f)
) {
BoxWithConstraints {
LazyRow(
state = lazyListState,
flingBehavior = rememberSnapperFlingBehavior(lazyListState)
) {
itemsIndexed(nums) { index, num ->
Layout(
content = {
YearCard(currentYear = num)
},
measurePolicy = { measurables, constraints ->
val placeable = measurables.first().measure(constraints)
val maxWidthInPx = maxWidth.roundToPx()
val itemWidth = placeable.width
val startSpace =
if (index == 0) (maxWidthInPx - itemWidth) / 2 else 0
val endSpace =
if (index == nums.lastIndex) (maxWidthInPx - itemWidth) / 2 else 0
val width = startSpace + placeable.width + endSpace
layout(width, placeable.height) {
val x = if (index == 0) startSpace else 0
placeable.place(x, 0)
}
}
)
}
}
}
}
Column(
modifier = Modifier
.background(color = Color.Green)
.fillMaxWidth()
.weight(2f)
) {
LazyColumn {
items(events) { event ->
EventCard(event = event)
}
}
}
Button(
modifier = Modifier.padding(top = 16.dp),
onClick = {
layoutInfo.currentItem?.index?.let { itemIndex ->
Toast.makeText(
context,
"Current index is $itemIndex",
Toast.LENGTH_SHORT
).show()
}
}
) {
Text("Click")
}
}
}
Related
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
}
I have a list with 10 items one of them have this elements "rankingCurrentPlace", "rankingPastPlace" and "isUser:true".
What i need to do its an animation on the lazycolumn if the api esponse is like this
"isUser:true", "rankingPastPlace:3" , "rankingCurrentPlace:7"
i need to show an animation in the list where the row starts in the third place and descend to the seventh place
is there a way to do this?
this is what I actually have
LazyColumn(
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
) {
items(
items = leaderboard,
key = { leaderBoard ->
leaderBoard.rankingPlace
}
) { leaderBoard ->
RowComposable( modifier = Modifier
.fillMaxWidth(),
topicsItem = leaderBoard,)
}
This answer works except when swapping first item with any item even with basic swap function without animation. I think it would be better to ask a new question about why swapping first item doesn't work or if it is bug. Other than that works as expected. If you need to move to items that are not in screen you can lazyListState.layoutInfo.visibleItemsInfo and compare with initial item and scroll to it before animation
1.Have a SnapshotStateList of data to trigger recomposition when we swap 2 items
class MyData(val uuid: String, val value: String)
val items: SnapshotStateList<MyData> = remember {
mutableStateListOf<MyData>().apply {
repeat(20) {
add(MyData( uuid = UUID.randomUUID().toString(), "Row $it"))
}
}
}
2.Function to swap items
private fun swap(list: SnapshotStateList<MyData>, from: Int, to: Int) {
val size = list.size
if (from in 0 until size && to in 0 until size) {
val temp = list[from]
list[from] = list[to]
list[to] = temp
}
}
3.Function to swap items one by one. There is a bug with swapping first item. Even if it's with function above when swapping first item other one moves up without showing animation via Modififer.animateItemPlacement().
#Composable
private fun animatedSwap(
lazyListState: LazyListState,
items: SnapshotStateList<MyData>,
from: Int,
to: Int,
onFinish: () -> Unit
) {
LaunchedEffect(key1 = Unit) {
val difference = from - to
val increasing = difference < 0
var currentValue: Int = from
repeat(abs(difference)) {
val temp = currentValue
if (increasing) {
currentValue++
} else {
currentValue--
}
swap(items, temp, currentValue)
if (!increasing && currentValue == 0) {
delay(300)
lazyListState.scrollToItem(0)
}
delay(350)
}
onFinish()
}
}
4.List with items that have Modifier.animateItemPlacement()
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
state = lazyListState,
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(
items = items,
key = {
it.uuid
}
) {
Row(
modifier = Modifier
.animateItemPlacement(
tween(durationMillis = 200)
)
.shadow(1.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentScale = ContentScale.FillBounds,
contentDescription = null
)
Spacer(modifier = Modifier.width(10.dp))
Text(it.value, fontSize = 18.sp)
}
}
}
Demo
#OptIn(ExperimentalFoundationApi::class)
#Composable
private fun AnimatedList() {
Column(modifier = Modifier.fillMaxSize()) {
val items: SnapshotStateList<MyData> = remember {
mutableStateListOf<MyData>().apply {
repeat(20) {
add(MyData(uuid = UUID.randomUUID().toString(), "Row $it"))
}
}
}
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
state = lazyListState,
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(
items = items,
key = {
it.uuid
}
) {
Row(
modifier = Modifier
.animateItemPlacement(
tween(durationMillis = 200)
)
.shadow(1.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentScale = ContentScale.FillBounds,
contentDescription = null
)
Spacer(modifier = Modifier.width(10.dp))
Text(it.value, fontSize = 18.sp)
}
}
}
var fromString by remember {
mutableStateOf("7")
}
var toString by remember {
mutableStateOf("3")
}
var animate by remember { mutableStateOf(false) }
if (animate) {
val from = try {
Integer.parseInt(fromString)
} catch (e: Exception) {
0
}
val to = try {
Integer.parseInt(toString)
} catch (e: Exception) {
0
}
animatedSwap(
lazyListState = lazyListState,
items = items,
from = from,
to = to
) {
animate = false
}
}
Row(modifier = Modifier.fillMaxWidth()) {
TextField(
value = fromString,
onValueChange = {
fromString = it
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
TextField(
value = toString,
onValueChange = {
toString = it
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
Button(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
onClick = {
animate = true
}
) {
Text("Swap")
}
}
}
Edit: Animating with Animatable
Another method for animating is using Animatable with Integer vector.
val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
val coroutineScope = rememberCoroutineScope()
val animatable = remember { Animatable(0, IntToVector) }
And can be used as
private fun alternativeAnimate(
from: Int,
to: Int,
coroutineScope: CoroutineScope,
animatable: Animatable<Int, AnimationVector1D>,
items: SnapshotStateList<MyData>
) {
val difference = from - to
var currentValue: Int = from
coroutineScope.launch {
animatable.snapTo(from)
animatable.animateTo(to,
tween(350 * abs(difference), easing = LinearEasing),
block = {
val nextValue = this.value
if (abs(currentValue -nextValue) ==1) {
swap(items, currentValue, nextValue)
currentValue = nextValue
}
}
)
}
}
on button click, i'm getting values from TextField fo i convert from String
Button(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
onClick = {
val from = try {
Integer.parseInt(fromString)
} catch (e: Exception) {
0
}
val to = try {
Integer.parseInt(toString)
} catch (e: Exception) {
0
}
alternativeAnimate(from, to, coroutineScope, animatable, items)
}
) {
Text("Swap")
}
Result
I suggest you to get your items from a data class. If your other items does not contain the variables you mentioned you can make them nullable in data class and put a condition checker in your lazycolumn
Like this
data class Items(
val otherItems: Other,
val rankingCurrentPlace: Int?,
val rankingLastPlace: Int?,
val isUser: Boolean?
)
Then you can make a list from this data class and pass it to lazycolumn
LazyColumn{
items(list){
(elements with condition)
}
}
I am using Accompanist's paging indicator.
It is working fine but it is not customizable.
I want to set exact amount of dots should be visible in screen and those dots should be size configurable on scrolls.
There is another question similar to this thread but owner already accepted AndroidView approach which I want it Composable way.
I made a sample looks similar, logic for scaling is raw but it looks similar. Need to convert from
#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,
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
listState.animateScrollToItem(
currentItem,
(widthInPx / 2 - viewportSize.width / 2).toInt()
)
}
LazyRow(
modifier = modifier.width(totalWidth),
state = listState,
contentPadding = PaddingValues(vertical = space),
horizontalArrangement = Arrangement.spacedBy(space),
userScrollEnabled = false
) {
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
)
)
}
}
}
Usage
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 = 20.dp,
indicatorCount = 7,
activeColor = Color(0xff2196F3),
inActiveColor = Color(0xffBBDEFB),
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
)
}
}
}
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
I am working on recreating facebook reactions with jetpack compose. Have now an issue with how I can detect when hovering over an item in a row and scale it? To be more clear I am creating THIS. I reused popup from jetpack compose that part is fine. But dragging/hovering over the row not sure how to scale images. Any advice or help?
I did this initial implementation for you. I hope you can continue your implementation from here:
#ExperimentalComposeUiApi
#Composable
fun ReactionsComponent() {
val density = LocalDensity.current
var selectedIndex by remember {
mutableStateOf(-1)
}
val iconSize = 48.dp
val boxPadding = 8.dp
val iconSizePx = with(density) { iconSize.toPx() }
val boxPaddingPx = with(density) { boxPadding.toPx() }
val increaseSize = iconSize.times(2f)
val icons = listOf(
Icons.Default.Favorite,
Icons.Default.Star,
Icons.Default.Call,
Icons.Default.AccountBox,
Icons.Default.ThumbUp
)
Box(
Modifier
.height(increaseSize)
.width(IntrinsicSize.Min)
.pointerInteropFilter {
val selection = ((it.x - boxPaddingPx) / iconSizePx).toInt()
if (selection >= icons.size || selection < 0 || it.x < boxPaddingPx) {
selectedIndex = -1
} else if (it.action == MotionEvent.ACTION_UP) {
selectedIndex = -1 // finger released
} else {
selectedIndex = selection
}
true
}
) {
Box(
Modifier
.align(Alignment.BottomStart)
.fillMaxWidth()
.height(iconSize + boxPadding.times(2))
.background(Color.LightGray, CircleShape)
)
Row(
Modifier
.align(Alignment.BottomStart)
.width(IntrinsicSize.Min)
.padding(boxPadding),
verticalAlignment = Alignment.Bottom
) {
icons.forEachIndexed { index, icon ->
val size = if (selectedIndex == index) increaseSize else iconSize
Box(
Modifier
.border(1.dp, Color.LightGray, CircleShape)
.background(Color.White, CircleShape)
.height(animateDpAsState(size).value)
.width(animateDpAsState(size).value)
) {
Icon(
icon,
contentDescription = null,
tint = Color.Magenta,
modifier = Modifier.fillMaxSize().padding(8.dp)
)
}
}
}
}
}
Here's the result:
You can find the full code here (which includes item selection).
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"