net.peanuuutz.fork.ui.animation.Transition.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fork-ui Show documentation
Show all versions of fork-ui Show documentation
Comprehensive API designed for Minecraft modders
The newest version!
/*
* Copyright 2020 The Android Open Source Project
* Modifications Copyright 2022 Peanuuutz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.peanuuutz.fork.ui.animation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameMillis
import net.peanuuutz.fork.ui.animation.TimeConstants.UnspecifiedTimeMillis
import net.peanuuutz.fork.ui.animation.spec.target.composite.AnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.composite.FiniteAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.composite.SpringSpec
import net.peanuuutz.fork.ui.animation.vector.AnimationVector
import net.peanuuutz.fork.ui.animation.vector.VectorConvertor
import net.peanuuutz.fork.ui.animation.vector.newVector
import kotlin.math.max
@Composable
fun rememberUpdatedTransition(
targetValue: T
): Transition {
val transition = remember {
Transition(TransitionState(targetValue))
}.apply {
animateTo(targetValue)
}
DisposableEffect(Unit) {
onDispose {
transition.onTransitionEnd()
}
}
return transition
}
@Composable
fun rememberUpdatedTransition(
transitionState: TransitionState
): Transition {
val transition = remember {
Transition(transitionState)
}.apply {
animateTo(transitionState.targetValue)
}
DisposableEffect(Unit) {
onDispose {
transition.onTransitionEnd()
}
}
return transition
}
@Stable
class Transition(transitionState: TransitionState) {
val state: T
get() = _transitionState.value
val targetState: T
get() = _transitionState.targetValue
val segment: Segment
get() = _segment
inner class Segment(
val initialState: T,
val targetState: T
) {
infix fun T.to(targetState: T): Boolean {
return this == initialState && targetState == [email protected]
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Transition<*>.Segment) return false
if (initialState != other.initialState) return false
if (targetState != other.targetState) return false
return true
}
override fun hashCode(): Int {
var result = initialState?.hashCode() ?: 0
result = 31 * result + (targetState?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "Transition.Segment(initialState=$initialState, targetState=$targetState)"
}
}
val animationStates: List>
get() = _animationStates
@Stable
abstract inner class AnimationState> : State {
abstract val convertor: VectorConvertor
abstract override val value: S
val valueVector: V
get() = convertor.convertToVector(value)
abstract val velocityVector: V
val velocity: S
get() = convertor.convertFromVector(velocityVector)
abstract val isRunning: Boolean
abstract val animationSpec: AnimationSpec
abstract val animation: TargetBasedAnimation
}
val children: List>
get() = _children
val totalDurationMillis: Int by derivedStateOf {
var durationMillis = 0
animationStates.forEach { animationState ->
durationMillis = max(durationMillis, animationState.animation.durationMillis)
}
children.forEach { transition ->
durationMillis = max(durationMillis, transition.totalDurationMillis)
}
durationMillis
}
val isRunning: Boolean
get() = _transitionState.isRunning
abstract inner class DeferredAnimation> {
abstract val convertor: VectorConvertor
abstract fun animate(
transitionSpec: Segment.() -> FiniteAnimationSpec = { SpringSpec() },
targetValueByState: (state: T) -> S
): State
}
// ======== Internal ========
private val _transitionState: TransitionStateImpl = transitionState as TransitionStateImpl
private var _segment: Segment by mutableStateOf(Segment(state, state))
private val _animationStates: MutableList> = mutableStateListOf()
private val _children: MutableList> = mutableStateListOf()
private var shouldUpdate: Boolean by mutableStateOf(true)
private var startTimeMillis: Int by mutableStateOf(UnspecifiedTimeMillis)
private var playTimeMillis: Int by mutableStateOf(0)
internal fun addAnimationState(animationState: AnimationStateImpl<*, *>) {
_animationStates.add(animationState)
}
internal fun removeAnimationState(animationState: AnimationStateImpl<*, *>) {
_animationStates.remove(animationState)
}
internal fun addChild(transition: Transition<*>) {
_children.add(transition)
}
internal fun removeChild(transition: Transition<*>) {
_children.remove(transition)
}
@Composable
internal fun animateTo(targetState: T) {
updateTargetState(targetState)
if (state != targetState || isRunning || shouldUpdate) {
LaunchedEffect(this) {
do {
withFrameMillis { frameTimeMillisLong ->
val frameTimeMillis = frameTimeMillisLong.toInt()
update(frameTimeMillis)
}
} while (isRunning)
}
}
}
internal fun updateTargetState(targetState: T) {
if (this.targetState != targetState) {
_segment = Segment(this.targetState, targetState)
_transitionState.value = this.targetState
_transitionState.targetValue = targetState
if (!isRunning) {
shouldUpdate = true
}
_animationStates.forEach { animation ->
animation.shouldReset = true
}
}
}
private fun update(frameTimeMillis: Int) {
if (startTimeMillis == UnspecifiedTimeMillis) {
onTransitionStart(frameTimeMillis)
}
shouldUpdate = false
playTimeMillis = frameTimeMillis - startTimeMillis
var allFinished = true
_animationStates.forEach { state ->
if (state.isRunning) {
state.update(playTimeMillis)
}
if (state.isRunning) {
allFinished = false
}
}
_children.forEach { transition ->
if (transition.state != transition.targetState) {
transition.update(playTimeMillis)
}
if (transition.state != transition.targetState) {
allFinished = false
}
}
if (allFinished) {
onTransitionEnd()
}
}
private fun onTransitionStart(frameTimeMillis: Int) {
startTimeMillis = frameTimeMillis
_transitionState.isRunning = true
}
internal fun onTransitionEnd() {
_transitionState.isRunning = false
startTimeMillis = UnspecifiedTimeMillis
_transitionState.value = targetState
playTimeMillis = 0
}
@Stable
internal inner class AnimationStateImpl>(
override val convertor: VectorConvertor,
initialValue: S,
initialVelocityVector: V
) : AnimationState() {
override var value: S by mutableStateOf(initialValue)
private var targetValue: S by mutableStateOf(initialValue)
override var velocityVector: V by mutableStateOf(initialVelocityVector)
override var animationSpec: FiniteAnimationSpec by mutableStateOf(SpringSpec())
override var animation: TargetBasedAnimation by mutableStateOf(
value = TargetBasedAnimation(
vectorizedAnimationSpec = animationSpec.vectorize(convertor),
convertor = convertor,
initialValue = initialValue,
targetValue = targetValue,
initialVelocityVector = initialVelocityVector
)
)
override var isRunning: Boolean by mutableStateOf(false)
var shouldReset: Boolean by mutableStateOf(false)
private var offsetFrameTimeMillis: Int by mutableStateOf(0)
fun updateAnimation(
animationSpec: FiniteAnimationSpec,
targetValue: S
) {
if (this.targetValue != targetValue || shouldReset) {
this.targetValue = targetValue
this.animationSpec = animationSpec
animation = TargetBasedAnimation(
vectorizedAnimationSpec = animationSpec.vectorize(convertor),
convertor = convertor,
initialValue = value,
targetValue = targetValue,
initialVelocityVector = velocityVector
)
isRunning = true
shouldReset = false
offsetFrameTimeMillis = playTimeMillis
shouldUpdate = true
}
}
fun update(playTimeMillis: Int) {
val actualPlayTimeMillis = playTimeMillis - offsetFrameTimeMillis
value = animation.getValueFromMillis(actualPlayTimeMillis)
velocityVector = animation.getVelocityVectorFromMillis(actualPlayTimeMillis)
if (actualPlayTimeMillis >= animation.durationMillis) {
isRunning = false
offsetFrameTimeMillis = 0
}
}
}
internal inner class DeferredAnimationImpl>(
override val convertor: VectorConvertor
) : DeferredAnimation() {
var deferredState: DeferredState? = null
internal inner class DeferredState>(
val animationState: AnimationStateImpl
) : State {
lateinit var transitionSpec: Segment.() -> FiniteAnimationSpec
lateinit var targetValueByState: (T) -> S
override val value: S
get() {
update()
return animationState.value
}
private fun update() {
val segment = segment
val transitionSpec = segment.transitionSpec()
val targetValue = targetValueByState(segment.targetState)
animationState.updateAnimation(transitionSpec, targetValue)
}
}
override fun animate(
transitionSpec: Segment.() -> FiniteAnimationSpec,
targetValueByState: (state: T) -> S
): State {
val deferredState = deferredState ?: kotlin.run {
val initialValue = targetValueByState(state)
val animationState = AnimationStateImpl(
convertor = convertor,
initialValue = initialValue,
initialVelocityVector = convertor.newVector(initialValue)
)
addAnimationState(animationState)
DeferredState(animationState).also { deferredState = it }
}
return deferredState.apply {
this.transitionSpec = transitionSpec
this.targetValueByState = targetValueByState
}
}
}
}
@Composable
inline fun Transition.derive(
crossinline stateTransformer: @Composable (parentState: T) -> S
): Transition {
// This will only be used once (to create child transition)
val initialParentState = remember(this) { state }
return derive(
initialState = stateTransformer(initialParentState),
targetState = stateTransformer(targetState)
)
}
// ======== Internal ========
@PublishedApi
@Composable
internal fun Transition.derive(
initialState: S,
targetState: S
): Transition {
val child = remember(this) {
Transition(TransitionState(initialState))
}.apply {
updateTargetState(targetState)
}
DisposableEffect(this) {
addChild(child)
onDispose {
removeChild(child)
}
}
return child
}