Skip to content

Instantly share code, notes, and snippets.

@kliphouse
Last active March 17, 2025 18:18
Show Gist options
  • Save kliphouse/a139f27bbdebf5187dd3c3a840925495 to your computer and use it in GitHub Desktop.
Save kliphouse/a139f27bbdebf5187dd3c3a840925495 to your computer and use it in GitHub Desktop.
A horizontal pager indicator for compose that uses [PagerState](https://google.github.io/accompanist/pager/#pagerstate) (based on [IndefinitePagerIndicator](https://github.com/wching/Android-Indefinite-Pager-Indicator)
package com.rbrooks.indefinitepagerindicator
import android.view.animation.DecelerateInterpolator
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ContentAlpha
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
/**
* A horizontally laid out indicator for a [HorizontalPager], representing
* the currently active page, a default number of represented pages, and upcoming pages
* drawn using a Circle.
*
* @param pagerState the state object of your [Pager] to be used to observe the list's state.
* @param modifier the modifier to apply to this layout.
* @param selectedDotColor the color of the active Page indicator
* @param dotColor the color of page indicators that are inactive. This defaults to
* [selectedDotColor] with the alpha component set to the [ContentAlpha.disabled].
* @param dotRadius the width of each indicator in [Dp].
* @param selectedDotRadius the width of the selected indicator in [Dp].
* @param spacing the spacing between each indicator in [Dp].
* @param spacing the spacing between each indicator in [Dp].
* @param dotCount the number of dots to show.
* @param fadingDotCount the number of fading dots on either side of the dotCount dots..
*/
@OptIn(ExperimentalAnimationApi::class)
@ExperimentalPagerApi
@Composable
fun IndefinitePagerIndicator(
modifier: Modifier = Modifier,
pagerState: PagerState,
selectedDotColor: Color = colorResource(R.color.default_selected_dot_color),
dotColor: Color = colorResource(R.color.default_dot_color),
dotRadius: Dp = 4.dp,
selectedDotRadius: Dp = 5.5.dp,
dotCount: Int = 5,
fadingDotCount: Int = 1,
spacing: Dp = 10.dp,
) {
val interpolator = remember {
DecelerateInterpolator()
}
val dotRadiusPx = LocalDensity.current.run { dotRadius.roundToPx() }
val selectedDotRadiusPx = LocalDensity.current.run { selectedDotRadius.roundToPx() }
val dotSeparationDistancePx = LocalDensity.current.run { spacing.roundToPx() }
val intermediateSelectedItemPosition by remember {
derivedStateOf { pagerState.pageCount - pagerState.currentPage - 1 }
}
fun getDistanceBetweenTheCenterOfTwoDots() = 2 * dotRadiusPx + dotSeparationDistancePx
fun getDotCoordinate(pagerPosition: Int): Float =
(pagerPosition - intermediateSelectedItemPosition) * getDistanceBetweenTheCenterOfTwoDots() +
(getDistanceBetweenTheCenterOfTwoDots() * pagerState.currentPageOffset)
fun getDotYCoordinate(): Int = selectedDotRadiusPx
fun getCalculatedWidth(): Int {
val maxNumVisibleDots = dotCount + 2 * fadingDotCount
return (maxNumVisibleDots - 1) * getDistanceBetweenTheCenterOfTwoDots() + 2 * dotRadiusPx
}
fun getRadius(coordinate: Float): Float {
val coordinateAbs = Math.abs(coordinate)
// Get the coordinate where dots begin showing as fading dots (x coordinates > half of width of all large dots)
val largeDotThreshold =
dotCount.toFloat() / 2 * getDistanceBetweenTheCenterOfTwoDots()
return when {
coordinateAbs < getDistanceBetweenTheCenterOfTwoDots() / 2 -> selectedDotRadiusPx.toFloat()
coordinateAbs <= largeDotThreshold -> dotRadiusPx.toFloat()
else -> {
// Determine how close the dot is to the edge of the view for scaling the size of the dot
val percentTowardsEdge = (coordinateAbs - largeDotThreshold) /
(getCalculatedWidth() / 2.01f - largeDotThreshold)
interpolator.getInterpolation(1 - percentTowardsEdge) * dotRadiusPx
}
}
}
fun getDotColor(coordinate: Float): Color =
when {
Math.abs(coordinate) < getDistanceBetweenTheCenterOfTwoDots() / 2 -> selectedDotColor
else -> dotColor
}
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
) {
val width = size.width
(0 until pagerState.pageCount)
.map { getDotCoordinate(it) }
.forEach {
val xPosition = width / 2 - it
val yPosition = getDotYCoordinate().toFloat()
drawCircle(
color = getDotColor(it),
radius = getRadius(it),
center = Offset(xPosition, yPosition)
)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment