Description
This PR provides an engine to drive animations from common code.
It supports all the basic types of animations:
- Ease In
- Ease Out
- Ease In/Out
- Custom Cubic Bezier
- Spring
This PR also:
- Refactor sample's Home Screen to include sections
- Adds a SwiftUI utility to use
ForEach
directly with VMDIdentifiableContent
- Adds
AtomicStackReference
in trikot.foundation
Motivation and Context
Using the MVVM pattern it is pretty hard for a view to perform a change animated because the view layer don't have any insight on the type and context of a change. To alleviate that, we introduce the concept of animations in the view model layer.
There is basically two ways to perform an animated change:
Closure based
A closure based version suitable when the change occur as a result of the press of a button or a specific lifecycle event. All the published properties updated within that block will be animated.
withAnimation(VMDAnimation.Tween(duration = 1.seconds, easing = VMDAnimationEasing.Standard.EaseInEaseOut)) {
// Perform the change here
}
Binding based
If the change is introduced as part of a binding on a reactive chain, we also have the possibility to bind a value animated. Instead of providing only the value, we have to supply a pair of the value and an optional animation object. The exemple bellow animates the alpha value but does not animate the initial value and animates only the visible
-> hidden
transition while performing the hidden
-> visible
change without animation.
bindHiddenAnimated(
hiddenPublisher.withPreviousValue().map {
val previousValue = it.first
val value = it.second
if (previousValue == null) { // No animation if its the first value received
Pair(value, null)
} else { // Optionally animate only some changes
Pair(value, if (value) VMDAnimation.Tween() else null)
}
}
)
Usage in native code
SwiftUI
Since SwiftUI is able to automatically infer the updated properties and interpolate between them, there is no further step required for the animation to be effective. It is the equivalent of performing a change for a @State
or an @ObservedObject
within a withAnimation
closure.
Jetpack Compose
It is a bit more effort on the Jetpack Compose side because the animation engine requires that we specifically declare an interpolator for a property to be animated.
Example for animation of the alpha value
val animatedHiddenProperty by viewModel.textField.observeAnimatedPropertyAsState(
property = viewModel.textField::isHidden,
transform = { hidden -> if (hidden) 0f else 1f }
)
val alphaAnimationProgress by animateFloatAsState(
targetValue = animatedHiddenProperty.value,
animationSpec = animatedHiddenProperty.animationSpec()
)
Notes
The previous animation mechanism that was iOS only and that would animate all the changes no mater the type of change have been completely removed as it is nearly unused internally and it has just been introduced.
Result
iOS
https://user-images.githubusercontent.com/291573/158255503-7b985480-776b-4c66-9726-54281c2c8c48.mp4
Android
https://user-images.githubusercontent.com/291573/158255555-3de5c3e2-fd39-43fd-b163-4797747620f5.mp4
How Has This Been Tested?
An animation showcase screen was added in the sample app.
Types of changes
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)