アニメーションの動きに特殊な効果を加えることができます。
特殊な効果とは、動きが加速したり、減速したり、弾んだり、または向きを変えたり、などです。
上記のような特殊な効果は、動きの軌道がそれぞれ異なる形になるため、ここでは「アニメーションの形状」と表現しています。
AnimationSpecはアニメーションの形状を定義するインターフェースです。そして、Jetpack Composeは実装済みの形状を4つ提供しています。
この中から、「spring」と取りあげて、まとめます。
※環境:Android Studio Iguana | 2023.2.1 Patch 1
Kotlin 1.9.0
Compose Compiler 1.5.1
androidx.compose.animation:* 1.5.0
アニメーションの形状
AnimationSpecはアニメーションの形状を定義するインターフェースです。
interface AnimationSpec<T> {
fun <V : AnimationVector> vectorize(
converter: TwoWayConverter<T, V>
): VectorizedAnimationSpec<V>
}
Jetpack Composeは実装済みの形状を4つ提供しています。
形状 | 概要 | 補足 |
tween | 滑らかな曲線を描いて変化 | 3次のベジェ曲線 |
spring | バネが振動するように変化 | バネの減衰比(振動の振幅)を選択可能
バネの硬さ(振動の速さ)を選択可能 |
keyframes | 折れ線を描いて変化 | |
snap | 指定時間に即座に切り替え | |
この中のspringは、バネガ振動するように変化するアニメーションを提供します。springの実態はAnimationSpecを実装したSpringSpecクラスです。
@Stable
fun <T> spring(
dampingRatio: Float = Spring.DampingRatioNoBouncy,
stiffness: Float = Spring.StiffnessMedium,
visibilityThreshold: T? = null
): SpringSpec<T> =
SpringSpec(dampingRatio, stiffness, visibilityThreshold)
private val StartX = 20.dp
private val EndX = 260.dp
@Preview
@Composable
private fun AnimationSpecSample() {
Column {
val _toggle = remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(20.dp))
// ----------------------------------------------------------
...
Text(text = "[ spring ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMediumLow
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
...
// ----------------------------------------------------------
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { _toggle.value = !_toggle.value }) {
Text(text = "Toggle")
}
}
}
関数の引数
spring関数は次のような引数を持ち、形状を調整できます。
引数 | 概要 |
dampingRatio | Float | バネの減衰比(振動の振幅)
・定義済みパラメータ
DampingRatioHighBouncy(0.2f)
DampingRatioMediumBouncy(0.5f)
DampingRatioLowBouncy(0.75f)
DampingRatioNoBouncy(1.0f)
※()内は設定されている値 |
stiffness | Float | バネの硬さ(振動の速さ)
・設定済みパラメータ
StiffnessHigh(10_000f)
StiffnessMedium(1500f)
StiffnessMediumLow(400f)
StiffnessLow(200f)
StiffnessVeryLow(50f)
※()内は設定されている値 |
visibilityThreshold | T? | アニメーション終了の閾値 |
バネの減衰比(dampingRatio)
dampingRatioはバネの減衰比を指定します。
ドキュメントには「減衰比」と記載されているのですが、「振動の振幅」と言った方が的を得ていると思います。
Valueが途切れているのは、visibilityThreshold(デフォルト:0.01f)により、出力が打ち切られているためです。
dampingRatioのサンプルコード
private val StartX = 20.dp
private val EndX = 260.dp
@Preview_mdpi320x480
@Composable
private fun DampingRatioSample() {
Column {
val _toggle = remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(20.dp))
// ----------------------------------------------------------
Text(text = "[ HighBouncy ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMediumLow
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ MediumBouncy ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMediumLow
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ LowBouncy ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessMediumLow
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ NoBouncy ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMediumLow
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
// ----------------------------------------------------------
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { _toggle.value = !_toggle.value }) {
Text(text = "Toggle")
}
}
}
バネの硬さ(stiffness)
stiffnessはバネの硬さを指定します。
ドキュメントには「硬さ」と記載されているのですが、「振動の速さ」と言った方が的を得ていると思います。
振幅は変わりませんが、 Valueの変化が速ければ、早くtartgetValueへ収束します。
stiffnessのサンプルコード
private val StartX = 20.dp
private val EndX = 260.dp
@Preview_mdpi320x480
@Composable
private fun StiffnessSample() {
Column {
val _toggle = remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(20.dp))
// ----------------------------------------------------------
Text(text = "[ StiffnessHigh ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessHigh
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ StiffnessMedium ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ StiffnessMediumLow ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMediumLow
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ StiffnessLow ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessLow
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ StiffnessVeryLow ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 50.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _posX = animateDpAsState(
targetValue = if(_toggle.value) EndX else StartX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessVeryLow
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = _posX.value)
)
}
// ----------------------------------------------------------
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = { _toggle.value = !_toggle.value }) {
Text(text = "Toggle")
}
}
}
アニメーション終了の閾値(visibilityThreshold)
visibilityThresholdはアニメーションの終了を判断するための閾値です。
閾値の役割
値(value)が「| valueの振幅 | ≦ visibilityThreshold」になると確定した時点で、アニメエンジンの動作をキャンセルし、value出力を停止させます。※「| |」は絶対値
プラスα(アルファ)?
キャンセル後にプラスα(アルファ)のvalue出力あります。期間はvisibilityThresholdに関係なく、まちまちです。
アニメエンジンは別スレッドで動作しています。ですので、キャンセルにより直ちに止められないためであると考えられます。
#コードが複雑で、解析を断念しました。申し訳ありません!#
閾値の違い(サンプル)
visibilityThresholdのサンプルコード
private val StartX = 20.dp
private val EndX = 260.dp
@Preview_mdpi320x480
@Composable
private fun VisibilityThresholdSample() {
Column {
val _toggle = remember { mutableStateOf(false) }
Spacer(modifier = Modifier.height(10.dp))
// ----------------------------------------------------------
Text(text = "[ th = 0.01 ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 40.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _ratioX = animateValueAsState(
targetValue = if(_toggle.value) 1.0f else 0.0f,
typeConverter = Float.VectorConverter,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = 0.01f
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = StartX + (EndX - StartX) * _ratioX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ th = 0.02 ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 40.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _ratioX = animateValueAsState(
targetValue = if(_toggle.value) 1.0f else 0.0f,
typeConverter = Float.VectorConverter,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = 0.02f
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = StartX + (EndX - StartX) * _ratioX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ th = 0.05 ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 40.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _ratioX = animateValueAsState(
targetValue = if(_toggle.value) 1.0f else 0.0f,
typeConverter = Float.VectorConverter,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = 0.05f
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = StartX + (EndX - StartX) * _ratioX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ th = 0.10 ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 40.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _ratioX = animateValueAsState(
targetValue = if(_toggle.value) 1.0f else 0.0f,
typeConverter = Float.VectorConverter,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = 0.10f
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = StartX + (EndX - StartX) * _ratioX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ th = 0.20 ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 40.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _ratioX = animateValueAsState(
targetValue = if(_toggle.value) 1.0f else 0.0f,
typeConverter = Float.VectorConverter,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = 0.20f
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = StartX + (EndX - StartX) * _ratioX.value)
)
}
// ----------------------------------------------------------
Text(text = "[ th = 0.50 ]")
Box(
modifier = Modifier
.size(width = 320.dp, height = 40.dp)
.background(color = Color(0xFFF0F0FF)),
contentAlignment = Alignment.CenterStart
) {
val _ratioX = animateValueAsState(
targetValue = if(_toggle.value) 1.0f else 0.0f,
typeConverter = Float.VectorConverter,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = 0.50f
)
)
Image(
painter = painterResource(R.drawable.baseline_toys_black_36),
contentDescription = null,
modifier = Modifier
.size(36.dp)
.offset(x = StartX + (EndX - StartX) * _ratioX.value)
)
}
// ----------------------------------------------------------
Spacer(modifier = Modifier.height(10.dp))
Button(onClick = { _toggle.value = !_toggle.value }) {
Text(text = "Toggle")
}
}
}
targetValueの大きさ
targetValueの大きさを2倍にすると、それに伴ってspringの振幅も2倍になります。つまり、両者は比例します。
また、振幅が2倍になると、1倍の場合よりも、振動の収束(targetValueに落ち着く)に時間がかかります。
これに対して、visibilityThreshold(th)は絶対的な値なので、引数に与えた値のままです。
ですので、visibilityThresholdが同じである場合、targetValueが大きくなれば打ち切りの時刻は遅れます。
下図はその例です。
打ち切りの遅れは、アニメーション期間の拡大を意味します。
アニメーションは端末のリソース(CPU能力、メモリー、消費電力など)を多く消費する処理です。
リソースを浪費しないように、適切なvisibilityThresholdを設定して、アニメーション期間は必要最小にすべきです。
注意:animateFloatAsStateの場合
animateFloatAsStateは引数にvisibilityThresholdを持ちます。
※animateFloatAsStateについては「Compose Animation:animate*AsState」を参照
visibilityThresholdはanimateFloatAsState側が優先されて、spring側は無視されるので、注意してください。
この原因は、animateValueAsStateの呼び出しで、「visibilityThreshold = null」に出来ないためです。
@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 <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 toolingOverride = remember { mutableStateOf<State<T>?>(null) }
val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) }
val listener by rememberUpdatedState(finishedListener)
val animSpec: AnimationSpec<T> by rememberUpdatedState(
animationSpec.run {
if (visibilityThreshold != null && this is SpringSpec &&
this.visibilityThreshold != visibilityThreshold
) {
spring(dampingRatio, stiffness, visibilityThreshold)
} else {
this
}
}
)
...
return toolingOverride.value ?: animatable.asState()
}
関連記事:
animate*AsState関数は制御する値のタイプにより、数種類が準備されています。 どの関数も、animateValueAsStateが原型のラッパー関数であり、動作は同じです。 ここでは、各々の関数について、サンプルを示します。 また、animateValueAsStateのサンプルは、独自の関数を作る方法を紹介しています。 ※環境:Android Studio Iguana | 2023.2.1 Kotlin 1.9.0 Compose Compiler 1.5.1 androidx.compose.animation:* 1.5.0 ...
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 ...
アニメーションの動きに特殊な効果を加えることができます。 特殊な効果とは、動きが加速したり、減速したり、弾んだり、または向きを変えたり、などです。 上記のような特殊な効果は、動きの軌道がそれぞれ異なる形になるため、ここでは「アニメーションの形状」と表現しています。 AnimationSpecはアニメーションの形状を定義するインターフェースです。そして、Jetpack Composeは実装済みの形状を4つ提供しています。 この中から、「tween」と取りあげて、まとめます。 ※環境:Android Studio Iguana | 2023.2.1 Patch 1 Kotlin 1.9.0 Compose Compiler 1.5.1 androidx.compose.animation:* 1.5.0 ...
アニメーションの動きに特殊な効果を加えることができます。 特殊な効果とは、動きが加速したり、減速したり、弾んだり、または向きを変えたり、などです。 上記のような特殊な効果は、動きの軌道がそれぞれ異なる形になるため、ここでは「アニメーションの形状」と表現しています。 AnimationSpecはアニメーションの形状を定義するインターフェースです。そして、Jetpack Composeは実装済みの形状を4つ提供しています。 この中から、「keyframes」と取りあげて、まとめます。 ※環境:Android Studio Iguana | 2023.2.1 Patch 1 Kotlin 1.9.0 Compose Compiler 1.5.1 androidx.compose.animation:* 1.5.0 ...
アニメーションの動きに特殊な効果を加えることができます。 特殊な効果とは、動きが加速したり、減速したり、弾んだり、または向きを変えたり、などです。 上記のような特殊な効果は、動きの軌道がそれぞれ異なる形になるため、ここでは「アニメーションの形状」と表現しています。 AnimationSpecはアニメーションの形状を定義するインターフェースです。そして、Jetpack Composeは実装済みの形状を4つ提供しています。 この中から、「snap」と取りあげて、まとめます。 ※環境:Android Studio Iguana | 2023.2.1 Patch 1 Kotlin 1.9.0 Compose Compiler 1.5.1 androidx.compose.animation:* 1.5.0 ...