Transition은 상태가 변할 때 각 애니메이션을 조정하여 자연스러운 전환을 만들고, target 값이 동적으로 변경되더라도 그에 맞춰 애니메이션을 다시 실행해준다.
Transition은 여러 애니메이션을 상태별로 관리하는 역할을 한다 즉, 여러 애니메이션을 하나의 상태로 묶어서 관리할 수 있다.
updateTransition 또는 rememberTransition을 사용하여 transition을 생성할 수 있다
targetState가 변경되면, 자동으로 모든 하위 애니메이션을 새 target값으로 애니메이션들을 자동으로 실행시킨다
목표 상태에 도달한 후에도, 특정 조건에 따라 Transition이 다시 실행될 수 있다
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
}
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 { }
}
}
}
}
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)
}
}