I am new with android Compose
Can Anyone told me if there any way to implement Page indcater in compose without third library ?
I am using material design 3
I have tried many solutions
But its Dublicated
I try a way with state pager but now its Dublicated 🙂
After many attempts to find an effective solution
I found two ways
The first:
Jetpack Compose animated pager dots indicator?
The Second :
Pager indicator for compose
but you will face some issue
about PagerState Class which now is duplicated
so you Can use rememberPagerState
fun PageIndicatorSample() {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(40.dp))
val pagerState1 = rememberPagerState(initialPage = 0)
val coroutineScope = rememberCoroutineScope()
PagerIndicatornew(pagerState = pagerState1, indicatorCount = getAllData.size) {
coroutineScope.launch {
pagerState1.scrollToPage(it)
}
}
HorizontalPager(
pageCount = getAllData.size,
state = pagerState1,
) {
// Here You Add what compose You Want... this is just example
Card(getAllData[it])
}
}
}
For Pager indicator
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun PagerIndicatornew(
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 = indicatorCount
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
)
)
}
}
}
NOTE: the first solution has some issues with indexing
so I prefer the Second one
Look at accompanist source code and customize it if you need or better use third-party lib
Related
This is a share your knowledge, Q&A-style question about using Layout, different Constraints and placing based on size and other Composables' positions to achieve a grid that changes Composable size, position and adds a number if number of Composables are greater than 4.
To layout Composables based on their count Layout is required and need to select Constraints, a detailed answer about Constraints is available in this link, based on number of items and place them accordingly.
For Constraints selection when there are 2 items we need to pick half width and full height to have result in question. When there are 4 items we need to pick half width and half height.
When item count is 3 we need to use 2 constraints, 1 for measuring first 2 items, another one measuring the one that covers whole width
#Composable
private fun ImageDrawLayout(
modifier: Modifier = Modifier,
itemCount: Int,
divider: Dp,
content: #Composable () -> Unit
) {
val spacePx = LocalDensity.current.run { (divider).roundToPx() }
val measurePolicy = remember(itemCount, spacePx) {
MeasurePolicy { measurables, constraints ->
val newConstraints = when (itemCount) {
1 -> constraints
2 -> Constraints.fixed(
width = constraints.maxWidth / 2 - spacePx / 2,
height = constraints.maxHeight
)
else -> Constraints.fixed(
width = constraints.maxWidth / 2 - spacePx / 2,
height = constraints.maxHeight / 2 - spacePx / 2
)
}
val gridMeasurables = if (itemCount < 5) {
measurables
} else {
measurables.take(3) + measurables.first { it.layoutId == "Text" }
}
val placeables: List<Placeable> = if (measurables.size != 3) {
gridMeasurables.map { measurable: Measurable ->
measurable.measure(constraints = newConstraints)
}
} else {
gridMeasurables
.take(2)
.map { measurable: Measurable ->
measurable.measure(constraints = newConstraints)
} +
gridMeasurables
.last()
.measure(
constraints = Constraints.fixed(
constraints.maxWidth,
constraints.maxHeight / 2 - spacePx
)
)
}
layout(constraints.maxWidth, constraints.maxHeight) {
when (itemCount) {
1 -> {
placeables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
2 -> {
var xPos = 0
placeables.forEach { placeable: Placeable ->
placeable.placeRelative(xPos, 0)
xPos += placeable.width + spacePx
}
}
else -> {
var xPos = 0
var yPos = 0
placeables.forEachIndexed { index: Int, placeable: Placeable ->
placeable.placeRelative(xPos, yPos)
if (index % 2 == 0) {
xPos += placeable.width + spacePx
} else {
xPos = 0
}
if (index % 2 == 1) {
yPos += placeable.height + spacePx
}
}
}
}
}
}
}
Layout(
modifier = modifier,
content = content,
measurePolicy = measurePolicy
)
}
Another thing to note here is we need to find find Composable that contains Text. It's possible to find it from index since it's 4th item but i used Modifier.layoutId() for demonstration. This Modifier helps finding Composables when you don't know in which order they are placed inside a Composaable.
val gridMeasurables = if (size < 5) {
measurables
} else {
measurables.take(3) + measurables.first { it.layoutId == "Text" }
}
And place items based on item count and we add space only after first item on each row.
Usage
#Composable
fun GridImageLayout(
modifier: Modifier = Modifier,
thumbnails: List<Int>,
divider: Dp = 2.dp,
onClick: ((List<Int>) -> Unit)? = null
) {
if (thumbnails.isNotEmpty()) {
ImageDrawLayout(
modifier = modifier
.clickable {
onClick?.invoke(thumbnails)
},
divider = divider,
itemCount = thumbnails.size
) {
thumbnails.forEach {
Image(
modifier = Modifier.layoutId("Icon"),
painter = painterResource(id = it),
contentDescription = "Icon",
contentScale = ContentScale.Crop,
)
}
if (thumbnails.size > 4) {
val carry = thumbnails.size - 3
Box(
modifier = Modifier.layoutId("Text"),
contentAlignment = Alignment.Center
) {
Text(text = "+$carry", fontSize = 20.sp)
}
}
}
}
}
And using GridImageLayout
Column {
val icons5 = listOf(
R.drawable.landscape1,
R.drawable.landscape2,
R.drawable.landscape3,
R.drawable.landscape4,
R.drawable.landscape5,
R.drawable.landscape6,
R.drawable.landscape7,
)
GridImageLayout(
modifier = Modifier
.border(1.dp, Color.Red, RoundedCornerShape(10))
.clip(RoundedCornerShape(10))
.size(150.dp),
thumbnails = icons5
)
}
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 want to create a Compose view that looks something like this:
|-------------------------------------------|
| "Dynamic text" Image "Text " |
|-------------------------------------------|
| "Dynamic text 2" Image "Text" |
|-------------------------------------------|
Logical way to do it would be to add two Rows inside a Column. Problematic part is that Images inside those Rows must always align while the first text elements length can change.
|-------------------------------------------|
| "Dynamic longer text" Image "Text " |
|-------------------------------------------|
| "Text" Image "Text" |
|-------------------------------------------|
What is the best option for it? I've thought about using ConstraintLayout but that seems like an overkill for this simple scenario. I have also considered using Columns inside a Row, but that just doesn't feel natural in this case. All ideas welcome!
If aligning Image and Text with static text end of your Composable is okay you can set Modifier.weight(1f) for Text with dynamic text so it will cover the space that is not used by Image and Text with static text.
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
) {
Text("Dynamic text", modifier = Modifier.weight(1f))
Image(
modifier = Modifier.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = ""
)
Text("text")
}
Row(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
) {
Text("Dynamic longer text", modifier = Modifier.weight(1f))
Image(
modifier = Modifier.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = ""
)
Text("text")
}
}
If you want a layout like in first table you should use a Layout. If you don't have any experience with Layout it might be complicated.
Steps to accomplish what's needed
1- First measure your every child Composable with Constraints and get placeable
val placeables: List<Placeable> = measurables.map { measurable ->
measurable.measure(constraints)
}
2- Get maximum width of dynamic texts, this will be our threshold for each row's second element x position, i added a padding but you can add padding to Text if you want to
// Get the maximum with of first Text on each Row
val maxDynamicWidth = placeables.filterIndexed { index, _ ->
index % 3 == 0
}.maxOf { it.width } + padding
3- Get height of each row. We will use row height for placing every 3 Composables
val rowHeights = mutableListOf<Int>()
var maxHeight = 0
// Get height for each row
placeables.forEachIndexed { index, placeable ->
maxHeight = if (index % 3 == 2) {
rowHeights.add(maxHeight)
0
} else {
placeable.height.coerceAtLeast(maxHeight)
}
}
4- get total height of our Layout, you can use constraints.maxHeight if you want to and then layout every 3 Composables on each row
val totalHeight = rowHeights.sum()
var y = 0
var x = 0
layout(constraints.maxWidth, totalHeight) {
placeables.forEachIndexed { index, placeable ->
if (index % 3 == 0) {
if (index > 0) y += rowHeights[index / 3]
placeable.placeRelative(0, y)
x = maxDynamicWidth
} else {
placeable.placeRelative(x, y)
x += placeable.width
}
}
}
Full implementation
#Composable
private fun MyLayout(
modifier: Modifier = Modifier,
paddingAfterDynamicText: Dp = 0.dp,
content: #Composable () -> Unit
) {
val padding = with(LocalDensity.current) {
paddingAfterDynamicText.roundToPx()
}
Layout(
modifier = modifier,
content = content
) { measurables: List<Measurable>, constraints: Constraints ->
require(measurables.size % 3 == 0)
val placeables: List<Placeable> = measurables.map { measurable ->
measurable.measure(constraints)
}
// Get the maximum with of first Text on each Row
val maxDynamicWidth = placeables.filterIndexed { index, _ ->
index % 3 == 0
}.maxOf { it.width } + padding
val rowHeights = mutableListOf<Int>()
var maxHeight = 0
// Get height for each row
placeables.forEachIndexed { index, placeable ->
maxHeight = if (index % 3 == 2) {
rowHeights.add(maxHeight)
0
} else {
placeable.height.coerceAtLeast(maxHeight)
}
}
val totalHeight = rowHeights.sum()
var y = 0
var x = 0
// i put Composable on each row to top of the Row
// (y-placeable.height)/2 places them to center of row
layout(constraints.maxWidth, totalHeight) {
placeables.forEachIndexed { index, placeable ->
if (index % 3 == 0) {
if (index > 0) y += rowHeights[index / 3]
placeable.placeRelative(0, y)
x = maxDynamicWidth
} else {
placeable.placeRelative(x, y)
x += placeable.width
}
}
}
}
}
You can use it as
MyLayout(
modifier = Modifier.border(3.dp, Color.Red),
paddingAfterDynamicText = 15.dp
) {
Text("Dynamic text")
Image(
modifier = Modifier.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = ""
)
Text("text")
Text("Dynamic longer text")
Image(
modifier = Modifier.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = ""
)
Text("text")
}
Result
I want to make a smooth transition for scaling text inside the lazy column. Currently, I am using the graphics layer to animate the text scale based on the first visible item index from the list state. But it does not provide smooth and continuous animation. I want to make it as an Animated Flat list in React native. Here is an example of what I want to achieve.
Here is my code for scaling text based on the selected items.
val animateSizeText by animateFloatAsState(
targetValue = if (item == selectedItem) {
1f
}
else if (item == selectedItem- 1 || item == selectedItem+ 1) {
0.9f
}
else if (item == selectedItem- 2 || item == selectedItem+ 2) {
0.7f
}
else {
0.5f
},
animationSpec = tween(100, easing = LinearOutSlowInEasing)
)
Modifier for scaling text:
modifier = Modifier
.graphicsLayer {
scaleY = animateSizeText
scaleX = animateSizeText
}
Comparing to related question, you need to enable non default opacity value for other items using firstOrNull block and control how it depends on scroll position with a multiplier. It's pretty simple math, change this formula according to the scale effect you need.
val items = remember {
('A'..'Z').map { it.toString() }
}
val listState = rememberLazyListState()
val horizontalContentPadding = 16.dp
val boxSize = 50.dp
BoxWithConstraints {
val halfRowWidth = constraints.maxWidth / 2
LazyRow(
state = listState,
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(horizontal = horizontalContentPadding, vertical = 8.dp),
modifier = Modifier
.fillMaxWidth()
) {
itemsIndexed(items) { i, item ->
val opacity by remember {
derivedStateOf {
val currentItemInfo = listState.layoutInfo.visibleItemsInfo
.firstOrNull { it.index == i }
?: return#derivedStateOf 0.5f
val itemHalfSize = currentItemInfo.size / 2
(1f - minOf(1f, abs(currentItemInfo.offset + itemHalfSize - halfRowWidth).toFloat() / halfRowWidth) * 0.5f)
}
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.scale(opacity)
.alpha(opacity)
.size(boxSize)
.background(Color.Blue)
) {
Text(item, color = Color.White)
}
}
}
}
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"