Jetpack Composeが提供するアニメーションAPIは非常に充実しています。
「どのAPIを使えば最適なのか?」と、その選択を迷うくらいに数が多いです。ドキュメントは、「〇〇占い」に登場するようなYes/Noの設問ツリーを掲載(アニメーションAPIを選択する)して、選択の手助け行っています。
ここでは、アニメーションAPIの中から「animate*AsState(*は型名が入る)」を取りあげて、まとめます。
animate*AsStateは、APIの中で最も汎用性があります。まず始めに抑えておくべきアニメーションAPIです。
※環境:Android Studio Iguana | 2023.2.1
    Kotlin 1.9.0
    Compose Compiler 1.5.1
    androidx.compose.animation:* 1.5.0
目次
animate*AsStateとは
Composable関数(UI要素)の状態を時々刻々と変更することで、表示されるコンテンツをアニメーション化できます。
animation*AsStateは、このアニメーション化するための状態(value)を作り出します。
例えば、下図はアイコンのx座標を変更することで、左から右へ移動するアニメーションです。

関数の動作
初回ComposeでtargetValueへ値(サンプルは20.dp)を新規設定しても、アニメーションは行われません。
イベント(サンプルはクリック)等によりtargetValueの値(サンプルは260.dp)が変更されると、前targetValueと現targetvalueの間でアニメーションが行われます。
そして、targetValueが変更される毎に、これを繰り返します。

private val StartX = 20.dp
private val EndX = 260.dp
@Preview_mdpi
@Composable
private fun AnimateDpAsStateSample() {
    Box(
        modifier = Modifier
            .size(width = 320.dp, height = 100.dp)
            .background(color = Color(0xFFF0F0FF)),
        contentAlignment = Alignment.CenterStart
    ) {
        val _toggle = remember { mutableStateOf(false) }
        val _posX = animateDpAsState(
            targetValue = if(_toggle.value) EndX else StartX,
            animationSpec = tween(durationMillis = 1000)
        )
        Image(
            painter = painterResource(R.drawable.baseline_toys_black_36),
            contentDescription = null,
            modifier = Modifier
                .size(36.dp)
                .offset(x = _posX.value)
                .clickable { _toggle.value = !_toggle.value }
        )
    }
}
アニメーション中は状態(value:サンプルは_posX)が時々刻々と変更されるで、再Composeが行われて表示が更新されます。
つまり、アニメーション中は最小間隔(約1/60[s])で再Composeが連続実行される点に注意してください。
関数の種類
animate*AsState関数は制御する値のタイプにより、次のような種類が準備されています。※サンプルは「Compose Animation:animate*AsState関数のサンプル」を参照
| アニメーション関数 | タイプ | コメント | 
|---|---|---|
| animateFloatAsState | Float | |
| animateIntAsState | Int | |
| animateDpAsState | Dp | |
| animateOffsetAsState | Offset | x、yはFloat型 x、yは独立して制御可能 | 
| animateIntOffsetAsState | IntOffset | x、yはInt型 x、yは独立して制御可能 | 
| animateSizeAsState | Size | width、heightはFloat型 width、heightは独立して制御可能 | 
| animateIntSizeAsState | IntSize | width、heightはInt型 width、heightは独立して制御可能 | 
| animateColorAsState | Color | a、r、g、bは独立して制御可能 | 
| animateRectAsState | Rect | left、top、right、bottomはFloat型 left、top、right、bottomは独立して制御可能 | 
| animateValueAsState | Value | 上記9つの原型になる関数 | 
この中で、animateValueAsStateは原型になる関数です。他の関数はanimateValueAsStateのラッパー関数になります。
@Composable
fun animateFloatAsState(
    targetValue: Float,
    animationSpec: AnimationSpec<Float> = defaultAnimation,
    visibilityThreshold: Float = 0.01f,
    label: String = "FloatAnimation",
    finishedListener: ((Float) -> Unit)? = null
): State<Float> {
    val resolvedAnimSpec =
        if (animationSpec === defaultAnimation) {
            remember(visibilityThreshold) { spring(visibilityThreshold = visibilityThreshold) }
        } else {
            animationSpec
        }
    return animateValueAsState(
        targetValue,
        Float.VectorConverter,
        resolvedAnimSpec,
        visibilityThreshold,
        label,
        finishedListener
    )
}
@Composable
fun animateIntAsState(
    targetValue: Int,
    animationSpec: AnimationSpec<Int> = intDefaultSpring,
    label: String = "IntAnimation",
    finishedListener: ((Int) -> Unit)? = null
): State<Int> {
    return animateValueAsState(
        targetValue,
        Int.VectorConverter,
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}
private val intDefaultSpring = spring(visibilityThreshold = Int.VisibilityThreshold)
@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    label: String = "DpAnimation",
    finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
    return animateValueAsState(
        targetValue,
        Dp.VectorConverter,
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}
private val dpDefaultSpring = spring<Dp>(visibilityThreshold = Dp.VisibilityThreshold)
@Composable
fun animateOffsetAsState(
    targetValue: Offset,
    animationSpec: AnimationSpec<Offset> = offsetDefaultSpring,
    label: String = "OffsetAnimation",
    finishedListener: ((Offset) -> Unit)? = null
): State<Offset> {
    return animateValueAsState(
        targetValue,
        Offset.VectorConverter,
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}
private val offsetDefaultSpring = spring(visibilityThreshold = Offset.VisibilityThreshold)
@Composable
fun animateIntOffsetAsState(
    targetValue: IntOffset,
    animationSpec: AnimationSpec<IntOffset> = intOffsetDefaultSpring,
    label: String = "IntOffsetAnimation",
    finishedListener: ((IntOffset) -> Unit)? = null
): State<IntOffset> {
    return animateValueAsState(
        targetValue,
        IntOffset.VectorConverter,
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}
private val intOffsetDefaultSpring = spring(visibilityThreshold = IntOffset.VisibilityThreshold)
@Composable
fun animateSizeAsState(
    targetValue: Size,
    animationSpec: AnimationSpec<Size> = sizeDefaultSpring,
    label: String = "SizeAnimation",
    finishedListener: ((Size) -> Unit)? = null
): State<Size> {
    return animateValueAsState(
        targetValue,
        Size.VectorConverter,
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}
private val sizeDefaultSpring = spring(visibilityThreshold = Size.VisibilityThreshold)
@Composable
fun animateIntSizeAsState(
    targetValue: IntSize,
    animationSpec: AnimationSpec<IntSize> = intSizeDefaultSpring,
    label: String = "IntSizeAnimation",
    finishedListener: ((IntSize) -> Unit)? = null
): State<IntSize> {
    return animateValueAsState(
        targetValue,
        IntSize.VectorConverter,
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}
private val intSizeDefaultSpring = spring(visibilityThreshold = IntSize.VisibilityThreshold)
@Composable
fun animateColorAsState(
    targetValue: Color,
    animationSpec: AnimationSpec<Color> = colorDefaultSpring,
    label: String = "ColorAnimation",
    finishedListener: ((Color) -> Unit)? = null
): State<Color> {
    val converter = remember(targetValue.colorSpace) {
        (Color.VectorConverter)(targetValue.colorSpace)
    }
    return animateValueAsState(
        targetValue, converter, animationSpec, label = label, finishedListener = finishedListener
    )
}
private val colorDefaultSpring = spring<Color>()
@Composable
fun animateRectAsState(
    targetValue: Rect,
    animationSpec: AnimationSpec<Rect> = rectDefaultSpring,
    label: String = "RectAnimation",
    finishedListener: ((Rect) -> Unit)? = null
): State<Rect> {
    return animateValueAsState(
        targetValue,
        Rect.VectorConverter,
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}
private val rectDefaultSpring = spring(visibilityThreshold = Rect.VisibilityThreshold)
@Composable
fun <T, V : AnimationVector> animateValueAsState(
    targetValue: T,
    typeConverter: TwoWayConverter<T, V>,
    animationSpec: AnimationSpec<T> = remember { spring() },
    visibilityThreshold: T? = null,
    label: String = "ValueAnimation",
    finishedListener: ((T) -> Unit)? = null
): State<T> {
    ...
    val channel = remember { Channel<T>(Channel.CONFLATED) }
    SideEffect {
        channel.trySend(targetValue)
    }
    LaunchedEffect(channel) {
        for (target in channel) {
            ...
            val newTarget = channel.tryReceive().getOrNull() ?: target
            launch {
                if (newTarget != animatable.targetValue) {
                    animatable.animateTo(newTarget, animSpec)
                    listener?.invoke(animatable.value)
                }
            }
        }
    }
    return toolingOverride.value ?: animatable.asState()
}
関数の引数
animate*AsState関数は次のような引数を持ちます。
| 引数 | 概要 | |
|---|---|---|
| targetValue | T | 最終の到達値 | 
| animationSpec | AnimationSpec | アニメーションの形状 | 
| label | String | |
| finishedListener | (Int) -> Unit)? = null | アニメーション終了時に実行される 関数オブジェクト(ラムダ式) | 
| visibilityThreshold(※1) | T? | アニメーション終了の閾値 | 
| typeConverter(※2) | TwoWayConverter | アニメエンジン出力(Float) ⇒制御する値のタイプ(T)の変換器 | 
| ※T:Float/Int/Dp/Offset/IntOffset/Size/IntSize/Color/Rect ※1:animateFloatAsStateとanimateValueAsStateのみ ※2:animateValueAsStateのみ | ||
アニメーションの形状(animationSpec)
animationSpecはアニメーションの形状を指定します。
形状の種類
4種類の形状が準備されています。
| 形状 | 概要 | 補足 | 
|---|---|---|
| tween | 滑らかな曲線を描いて変化 | 3次のベジェ曲線 | 
| spring | バネが振動するように変化 | バネの減衰比(振動の振幅)を選択可能 バネの硬さ(振動の速さ)を選択可能 | 
| keyframes | 折れ線を描いて変化 | |
| snap | 指定時間に即座に切り替え | 




※詳細はリンクを参照
tween:「AnimationSpecでアニメの形状を指定(tween編)」
spring:「AnimationSpecでアニメの形状を指定(spring編)」
keyframes:「AnimationSpecでアニメの形状を指定(keyframes編)」
snap:「AnimationSpecでアニメの形状を指定(snap編)」
形状の違い(サンプル)
アニメーション終了の閾値(visibilityThreshold)
visibilityThresholdはアニメーションの終了を判断するための閾値です。
AnimationSpecがspringの時のみ有効です。
閾値の役割
値(value)が|value|≦visibilityThresholdになると確定した時点で、アニメエンジンの動作をキャンセルし、value出力を停止させます。

アニメエンジンは別スレッドで動作しています。ですので、キャンセルにより直ちに止められないためであると考えられます。
#コードが複雑で、解析を断念しました。申し訳ありません!#
閾値の違い(サンプル)

targetValueの大きさ
targetValueの大きさを2倍にすると、それに伴ってspringの振幅も2倍になります。つまり、両者は比例します。
また、振幅が2倍になると、1倍の場合よりも、振動の収束(targetValueに落ち着く)に時間がかかります。
これに対して、visibilityThreshold(th)は絶対的な値なので、引数に与えた値のままです。

ですので、visibilityThresholdが同じである場合、targetValueが大きくなれば打ち切りの時刻は遅れます。
下図はその例です。

打ち切りの遅れは、アニメーション期間の拡大を意味します。
アニメーションは端末のリソース(CPU能力、メモリー、消費電力など)を多く消費する処理です。
リソースを浪費しないように、適切なvisibilityThresholdを設定して、アニメーション期間は必要最小にすべきです。
関連記事:
