같이 알아보기
AnimationVector
Animatable
@Composable
fun <T, V : AnimationVector> animateValueAsState(
// 이 값이 바뀔 때마다 애니메이션이 실행
targetValue: T,
// 애니메이션은 내부적으로 AnimationVector 타입을 사용하기 때문에,
// T타입과 상호 변환하기 위함
// ex) IntSize.VectorConverter 이런식으로 할당하면 된다.
typeConverter: TwoWayConverter<T, V>,
// 애니메이션의 속도, 지속 시간 등을 세밀하게 조정한다
// spring, tween, keyframes 등으로도 설정 가능하다
animationSpec: AnimationSpec<T> = remember { spring() },
// 애니메이션의 종료에 대한 임계값이라 생각하면 된다
// targetValue과 현재 진행중인 값이 임계값보다 작을 때
// 애니메이션을 중단할 수 있다
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
}
}
)
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는 간단한 상태 기반 애니메이션에 적합하며, 사용하기 쉽고 코드가 간결하기 때문에 단순히 값이 변화할 때 애니메이션을 적용하고 싶을 때 유용하게 사용할 수 있다.
만약 세밀한 애니메이션 제어(애니메이션 범위 설정, 속도 등)을
하고 싶다면Animatable사용을 고려해 보자
targetValue를 애니메이션으로 변화시키고, 그 값의 상태를 추적할 수 있는 기능을 제공한다
State<T>로 반환하기 때문에 UI에서 상태 변화에 따라 자동으로 갱신할 수 있게 한다
animate<T>AsState 같은 경우 animateValueAsState를 리턴하고 있으며 <T>에 따라 AnimationVector의 차원이 달라진다고 생각하면 될 것 같다
예시를 하나 살펴보면
@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
)
}
animateFloatAsState의 경우 targetValue의 type이 Float형이고 VectorConverter또한 Float에 맞게 넣어주기때문에 신경써야할 부분은 targeValue와 animationSpec을 통해 어떤 애니메이션의 속도, 지연시간등을 설정한다던가 임계값을 조정한다던가 listener를 통해 종료 후 작업 정도만 넣어주면 되는걸 확인할 수 있다
전체적으로 보면 animate*AsState의 경우 이러한 구조이기 때문에 말 그대로 type만 달라지기때문에 자신이 필요한 타입에 따라 사용하면 될 것 같다.