[Jetpack Compose] animate*AsState

유민국·2024년 10월 18일

같이 알아보기
AnimationVector
Animatable

animateValueAsState

@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의 경우 targetValuetypeFloat형이고 VectorConverter또한 Float에 맞게 넣어주기때문에 신경써야할 부분은 targeValueanimationSpec을 통해 어떤 애니메이션의 속도, 지연시간등을 설정한다던가 임계값을 조정한다던가 listener를 통해 종료 후 작업 정도만 넣어주면 되는걸 확인할 수 있다

전체적으로 보면 animate*AsState의 경우 이러한 구조이기 때문에 말 그대로 type만 달라지기때문에 자신이 필요한 타입에 따라 사용하면 될 것 같다.

profile
안녕하세요 😊

0개의 댓글