この記事ははてなエンジニア Advent Calendar 2025 19日目の記事です。昨日の記事は
id:utgwkkのあなたの手元のGoプロダクトにひっそり佇んでいるコード片のことを、ほんの少しでいいから思い出してみませんかでした。
PathMeasureというPathを取り扱うClassがあって、これを使うとセットしたPathの一部区間やPath上の位置を取得できる。
val path = remember { Path() } val pathMeasure = remember { PathMeasure() } val destinationPath = remember { Path() } Box( modifier = Modifier.drawWithCache { path.addPath( RoundedPolygon( numVertices = 6, radius = size.minDimension / 2, centerX = size.width / 2, centerY = size.height / 2 ).toPath().asComposePath() ) pathMeasure.setPath(path, forceClosed = false) destinationPath.reset() // 始点から半分までの区間がdestinationへ返される pathMeasure.getSegment( startDistance = 0f, stopDistance = pathMeasure.length * 0.5f, destination = destinationPath, ) onDrawBehind { rotate(270f) { drawPath(path, color = Color.DarkGray) drawPath(destinationPath, color = Color.Green) } } } .fillMaxSize(), )

抽出する区間を随時変えてやることでちょっとしたPathのアニメーションが作れる。
val infiniteTransition = rememberInfiniteTransition(label = "infinite") val progress by infiniteTransition.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Restart ), label = "progress" ) // in drawWithCache pathMeasure.getSegment( startDistance = 0f, stopDistance = pathMeasure.length * progress, destination = destinationPath, )

あくまでPathの区間をそのまま抽出しているだけなので、アニメーションがどうなるかは元のPathがどのように作成されているかによる。
中心から扇のように広がるアニメーションを作りたいなら円弧を描画して各々の図形でクリップするほうが正直早いと思う。
FillではなくStrokeで描画するとPathSegmentの真の実力が発揮される。
// in drawWithCache onDrawBehind { rotate(270f) { drawPath(path, color = Color.DarkGray) drawPath( destinationPath, color = Color.Green, style = Stroke(width = 8.dp.toPx()), ) } }

これの良いところは、StrokeCapを設定することでぶつ切り感のない線が伸びていくようなアニメーションに見えるところ。
// in onDrawBehind drawPath( destinationPath, color = Color.Green, style = Stroke( width = 8.dp.toPx(), cap = StrokeCap.Round, ), )

筆跡をPathにしてサイン風のアニメーションにするととても映えそう。今回は面倒なのでしないが。
他にはModifier.borderの代わりにアニメーションする枠線を描画するのもよさそう。
// in drawWithCache val strokeWidth = 8.dp.toPx() val offset = strokeWidth / 2f val radius = 8.dp.toPx() path.addRoundRect( RoundRect( Rect( offset = Offset(x = offset, y = offset), size = Size( width = size.width - strokeWidth, height = size.height - strokeWidth, ), ), cornerRadius = CornerRadius(radius), ), ) pathMeasure.setPath(path, forceClosed = false) destinationPath.reset() pathMeasure.getSegment( startDistance = 0f, stopDistance = pathMeasure.length * progress, destination = destinationPath, ) onDrawBehind { drawPath( path = destinationPath, color = Color.Green, style = Stroke( width = strokeWidth, cap = StrokeCap.Round, ), ) }

ループするアニメーションのように区間が0(もしくはlength)を跨ぐ場合は、取得する区間の範囲指定が0 < x < length かつ start < stopとなっていることに注意する。
// in drawWithCache val strokeLength = 100.dp.toPx() val start = pathMeasure.length * progress val end = start + strokeLength pathMeasure.getSegment( startDistance = start, stopDistance = minOf(end, pathMeasure.length), destination = destinationPath, ) pathMeasure.getSegment( startDistance = 0f, stopDistance = maxOf(0f, end - pathMeasure.length), destination = destinationPath, startWithMoveTo = false )

発想次第で面白いアニメーションがいくつもできそうなのでぜひ俺の考えた最強のPathアニメーションを作ってほしい。

はてなエンジニア Advent Calendar 2025 明日の担当は
id:fxwx23です。