Skip to content

Instantly share code, notes, and snippets.

@KlassenKonstantin
Created May 28, 2024 10:27
Show Gist options
  • Save KlassenKonstantin/5fe432cdb7a74e0eb5b6bbad7d64187e to your computer and use it in GitHub Desktop.
Save KlassenKonstantin/5fe432cdb7a74e0eb5b6bbad7d64187e to your computer and use it in GitHub Desktop.
Animated focus indicator
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
BorderPathTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
BorderThing()
}
}
}
}
}
}
@Composable
fun BorderThing() {
val anim = rememberInfiniteTransition()
val progress = anim.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(
modifier = Modifier.padding(16.dp)
) {
Box(
modifier = Modifier
.width(200.dp)
.height(50.dp)
.animatedBorder({ progress.value }, Color.Blue, MaterialTheme.colorScheme.outline)
)
}
}
fun Modifier.animatedBorder(provideProgress: () -> Float, colorFocused: Color, colorUnfocused: Color) = this.drawWithCache {
val width = size.width
val height = size.height
val shape = CircleShape
// Only works with RoundedCornerShape...
val outline = shape.createOutline(size, layoutDirection, this) as Outline.Rounded
// ... correction: Only works with same corner sizes everywhere
val radius = outline.roundRect.topLeftCornerRadius.x
val diameter = 2 * radius
// Clockwise path
val pathCw = Path()
// Start top center
pathCw.moveTo(width / 2, 0f)
// Line to right
pathCw.lineTo(width - radius, 0f)
// Top right corner
pathCw.arcTo(Rect(width - diameter, 0f, width, diameter), -90f, 90f, false)
// Right edge
pathCw.lineTo(width, height - radius)
// Bottom right corner
pathCw.arcTo(Rect(width - diameter, height - diameter, width, height), 0f, 90f, false)
// Line to bottom center
pathCw.lineTo(width / 2, height)
// As above, but mirrored horizontally
val pathCcw = Path()
pathCcw.moveTo(width / 2, 0f)
pathCcw.lineTo(radius, 0f)
pathCcw.arcTo(Rect(0f, 0f, diameter, diameter), -90f, -90f, false)
pathCcw.lineTo(0f, height - radius)
pathCcw.arcTo(Rect(0f, height - diameter, diameter, height), 180f, -90f, false)
pathCcw.lineTo(width / 2, height)
val pmCw = PathMeasure().apply {
setPath(pathCw, false)
}
val pmCcw = PathMeasure().apply {
setPath(pathCcw, false)
}
fun DrawScope.drawIndicator(progress: Float, pathMeasure: PathMeasure) {
val subPath = Path()
pathMeasure.getSegment(0f, pathMeasure.length * EaseOut.transform(progress), subPath)
drawPath(subPath, colorFocused, style = Stroke(3.dp.toPx(), cap = StrokeCap.Round))
}
onDrawBehind {
// Draw the shape
drawOutline(outline, colorUnfocused, style = Stroke(2.dp.toPx()))
// Draw the indicators
drawIndicator(provideProgress(), pmCw)
drawIndicator(provideProgress(), pmCcw)
}
}
@NiranjanShah
Copy link

example of using it please

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