[Jetpack Compose] Transition

유민국·2024년 10월 22일

[Jetpack Compose] Animation

목록 보기
10/10

Transition

Transition은 상태가 변할 때 각 애니메이션을 조정하여 자연스러운 전환을 만들고, target 값이 동적으로 변경되더라도 그에 맞춰 애니메이션을 다시 실행해준다.

Transition은 여러 애니메이션을 상태별로 관리하는 역할을 한다 즉, 여러 애니메이션을 하나의 상태로 묶어서 관리할 수 있다.

updateTransition 또는 rememberTransition을 사용하여 transition을 생성할 수 있다

targetState가 변경되면, 자동으로 모든 하위 애니메이션을 새 target값으로 애니메이션들을 자동으로 실행시킨다

목표 상태에 도달한 후에도, 특정 조건에 따라 Transition이 다시 실행될 수 있다

updateTransition

updateTransition의 전체적인 동작 구조를 살펴보면,
우선 remember를 통해 targetState에 대한 Transtion을 생성하여 상태를 가지고 있게 된다. 이후 animateTo함수를 실행시켜 렌더링될 때의 시간을 켭쳐하고 각 프레임마다 등록된 애니메이션을 업데이트 해주는 상태를 만든다고 이해 하였다. 즉, 아직 애니메이션이 등록된 것이 아니며 추가로 animate* 함수를 실행하여 애니메이션을 등록하면 되고,(animateValue 내부에 createTransitionAnimation가 있기 때문에)
Transition안에 애니메이션이 포함된 상태라면 Transition안에서 렌더링될 동안 계속 캡쳐하여 애니메이션을 업데이트한다

이런 구조로 인해 하나의 transition안에 여러 애니메이션을 등록할 수 있다

처음 updateTransition
targetState에 따라 애니메이션을 업데이트하고 해당 상태를 remember,
Transition 클래스의 animateTo함수를 사용하여 목표 상태에 맞춰 애니메이션을 시작한다
DisposableEffect 내부에서 애니메이션 리소스를 정리

@Composable
fun <T> updateTransition(
    targetState: T,
    label: String? = null
): Transition<T> {
    val transition = remember { 
        Transition(targetState, label = label) 
    }
    transition.animateTo(targetState)
    DisposableEffect(transition) {
        onDispose {
            transition.onDisposed()
        }
    }
    return transition
}

Transtion.animateTo

coroutineScope내에서 애니메이션 프레임을 업데이트한다

it/AnimationDevugDurationScale(현재 프레임 시간)을 가져와 onFrame함수를 호출하여 애니메이션을 업데이트 한다

AnimationDevugDurationScale는 1로 고정된 값이며, it은 렌더링이 진행될 때 켭쳐되는 시간이라고 생각하면 될 것 같다

@OptIn(InternalAnimationApi::class)
@Suppress("ComposableNaming")
@Composable
internal fun animateTo(targetState: S) {
    if (!isSeeking) {
        updateTarget(targetState)
        if (targetState != currentState || 
            isRunning || 
            updateChildrenNeeded
        ) {
            val coroutineScope = rememberCoroutineScope()
            DisposableEffect(coroutineScope, this) {
                coroutineScope.launch(
                    start = CoroutineStart.UNDISPATCHED
                ) {
                    val durationScale = coroutineContext.durationScale
                    while (isActive) {
                        withFrameNanos {
                            if (!isSeeking) {
                                onFrame(
                                  it / AnimationDebugDurationScale,
                                  durationScale
                                )
                            }
                        }
                    }
                }
                onDispose { }
            }
        }
    }
}

inner class

TransitionAnimationState

Transition클래스 내에서 애니메이션의 현재 상태와 관련된 정보를 관리한다
애니메이션의 값 변화, 상태 조정, 중단 및 재개 처리 등 다양한 기능을 통해
애니메이션의 동작을 유연하게 제어할 수 있도록 돕는다

@Stable
    inner class TransitionAnimationState<T, V : AnimationVector> internal constructor(
        initialValue: T,
        initialVelocityVector: V,
        val typeConverter: TwoWayConverter<T, V>,
        val label: String
    ) : State<T> {
    private var targetValue: T by mutableStateOf(initialValue)
    private val defaultSpring = spring<T>()
    var animationSpec: FiniteAnimationSpec<T> by mutableStateOf(defaultSpring)
            private set
           
    // TargetBasedAnimation 객체를 통해 애니메이션을 하기 위한 설정을 한다
    // (초기값/속도 및 타겟 값 포함)
    var animation: TargetBasedAnimation<T, V> by mutableStateOf(
            TargetBasedAnimation(
                animationSpec, typeConverter, initialValue, targetValue,
                initialVelocityVector
            )
        )
            private set
    internal var initialValueState: 
        SeekableTransitionState.SeekingAnimationState? = null
    private var initialValueAnimation: TargetBasedAnimation<T, V>? = null
    internal var isFinished: Boolean by mutableStateOf(true)
    internal var resetSnapValue by mutableFloatStateOf(NoReset)
    
    // true로 설정하면 사실상 목표 값이 무시되고 초기 값만 사용하기 때문에
    // UI상태가 즉시 반영된다
    // 이는 애니메이션이 필요하지 않은 경우에 true로 변경한다
    private var useOnlyInitialValue = false
    
    override var value by mutableStateOf(initialValue)
        internal set
    private var velocityVector: V = initialVelocityVector
    internal var durationNanos by mutableLongStateOf(animation.durationNanos)
    private var isSeeking = false
    
    // playTimeNanos에 따라 value, velocityVector, isFinished를 조정
    internal fun onPlayTimeChanged(
        playTimeNanos: Long,
        scaleToEnd: Boolean
    ) {...}
    
    internal fun seekTo(playTimeNanos: Long) {
        if (resetSnapValue != NoReset) {
            return
        }
        isSeeking = true 
        // 애니메이션이 중단된 경우를 뜻함
        if (animation.targetValue == animation.initialValue) {
            value = animation.targetValue
        } else {
        	// 주어진 playTime으로 이동
            value = animation.getValueFromNanos(playTimeNanos)
            velocityVector = animation.getVelocityVectorFromNanos(playTimeNanos)
        }
    }
    
    // initialValue를 업데이트하고,
    // 애니메이션의 진행 상태를 조정한다
    internal fun updateInitialValue() {...}
    
    // 중단될 경우 애니메이션 Spec 설정
    private val interruptionSpec: FiniteAnimationSpec<T>

    init {
      val visibilityThreshold: T? = 
        visibilityThresholdMap.get(typeConverter)?.let {
            val vector = typeConverter.convertToVector(initialValue)
            for (id in 0 until vector.size) {
                vector[id] = it
            }
            typeConverter.convertFromVector(vector)
        }
        interruptionSpec = spring(visibilityThreshold = visibilityThreshold)
    }
    
    // 애니메이션의 현재 상태를 업데이트하고 목표값과 초기값 간의 변화를 관리
    // 현재 값 업데이트, 애니메이션 재설정, 애니메이션 사양 적용
    // useOnlyInitialValue를 통해 애니메이션의 진행 여부 결졍
    private fun updateAnimation(
        initialValue: T = value,
        isInterrupted: Boolean = false,
    ){...}
    
    internal fun resetAnimation() {
        resetSnapValue = ResetNoSnap
    }
    
    // 애니메이션의 현재 값을 fraction에 따라 초기화 하고, 목표 값 및 초기값 조정
    // 애니메이션의 현재 값을 설정하며 지속 시간을 업데이트한다
    internal fun resetAnimationValue(fraction: Float) {...}
    
    // initialValue를 재설정하고, 만약 애니메이션이 중단되지 않았다면,
    // 초기값을 변경하여 애니메이션은 계속 진행된다.
    internal fun setInitialValueAnimation(
        animationState: SeekableTransitionState.SeekingAnimationState
    ){...}
    
    internal fun clearInitialAnimation() {
        initialValueAnimation = null
        initialValueState = null
        useOnlyInitialValue = false
    }
    
    // updateTargetValue과 updateInitialAndTargetValue는 
    // 컴포지션 단계에서 해당 함수가 호출된다 
    
    // 현재 애니메이션의 targetValue와 animationSpec를 업데이트
    @OptIn(InternalAnimationApi::class)
    internal fun updateTargetValue(
        targetValue: T,
        animationSpec: FiniteAnimationSpec<T>
    ){...}
    
    // 
    internal fun updateInitialAndTargetValue(
        initialValue: T,
        targetValue: T,
        animationSpec: FiniteAnimationSpec<T>
    ) {
        this.targetValue = targetValue
        this.animationSpec = animationSpec
        if (
            animation.initialValue == initialValue &&
            animation.targetValue == targetValue
        ) {
            return
        }
        updateAnimation(initialValue)
    }
}

전체 코드

profile
안녕하세요 😊

0개의 댓글