Last active
March 17, 2025 18:18
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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