以下の内容はhttps://blog.r4wxii.com/entry/2025-12-19より取得しました。


【Jetpack Compose】PathMeasureを使ったPathのアニメーション

この記事ははてなエンジニア 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が灰色、抽出されたPathが緑色で表示されている

抽出する区間を随時変えてやることでちょっとした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です。




以上の内容はhttps://blog.r4wxii.com/entry/2025-12-19より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14