Skip to content

Instantly share code, notes, and snippets.

@snooplsm
Last active February 16, 2026 14:29
Show Gist options
  • Select an option

  • Save snooplsm/1f010d0919646f426400b393e35b777d to your computer and use it in GitHub Desktop.

Select an option

Save snooplsm/1f010d0919646f426400b393e35b777d to your computer and use it in GitHub Desktop.
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.RoundRect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathMeasure
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun PerimeterProgressBar(
modifier: Modifier = Modifier,
progress: Float, // 0f..1f
strokeWidth: Dp = 8.dp,
cornerRadius: Dp = 50.dp,
color: Color = MaterialTheme.colorScheme.primary,
perimeterStart: Float = 0.66f,
direction: Path.Direction = Path.Direction.Clockwise,
animate: Boolean = true
) {
val clampedProgress = progress.coerceIn(0f, 1f)
val animatedProgress by if (animate) {
animateFloatAsState(
targetValue = clampedProgress,
animationSpec = androidx.compose.animation.core.spring(),
label = "perimeterProgress"
)
} else {
rememberUpdatedState(clampedProgress)
}
// Reuse objects across recompositions to reduce allocations.
val path = remember { Path() }
val partialPath = remember { Path() }
val wrapPath = remember { Path() }
val pathMeasure = remember { PathMeasure() }
Canvas(modifier = modifier) {
val strokePx = strokeWidth.toPx()
val radiusPx = cornerRadius.toPx()
val inset = strokePx / 2f
// Rebuild the full path based on the current canvas size.
path.reset()
path.addRoundRect(
RoundRect(
left = inset,
top = inset,
right = size.width - inset,
bottom = size.height - inset,
cornerRadius = CornerRadius(radiusPx)
),
direction
)
pathMeasure.setPath(path, false)
val totalLength = pathMeasure.length
val progressLength = totalLength * animatedProgress
val startOffset = totalLength * perimeterStart
partialPath.reset()
val endOffset = startOffset + progressLength
if (endOffset <= totalLength) {
pathMeasure.getSegment(startOffset, endOffset, partialPath, true)
} else {
pathMeasure.getSegment(startOffset, totalLength, partialPath, true)
wrapPath.reset()
pathMeasure.getSegment(0f, endOffset - totalLength, wrapPath, true)
partialPath.addPath(wrapPath)
}
drawPath(
path = partialPath,
color = color,
style = Stroke(width = strokePx, cap = StrokeCap.Round)
)
}
}
@snooplsm

Copy link
Copy Markdown
Author
perimiter_white_bg_78pct_x265.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment