Last active
February 16, 2026 14:29
-
-
Save snooplsm/1f010d0919646f426400b393e35b777d to your computer and use it in GitHub Desktop.
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
| 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) | |
| ) | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
perimiter_white_bg_78pct_x265.mp4