Animations
Animations are key to making an app feel modern and interactive. Doodle helps you achieve this with a powerful yet simple set of APIs that let you animate a wide range of things into your app. Everything you need to build sophisticated animations is available via the Animator
interface and its related components.
You will need to add the Animation
library to your app's dependencies.
- Kotlin
- Groovy
build.gradle.kts
dependencies {
implementation ("io.nacular.doodle:animation:$doodleVersion")
}
build.gradle
//sampleStart
dependencies {
implementation "io.nacular.doodle:animation:$doodle_version"
}
//sampleEnd
Requirements
Then you will need to include the Animator
in the list of Kodein modules your app launches with. You can do this by defining a new Module, or by including a binding for it in an existing Module you are already installing.
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.AnimatorImpl
import io.nacular.doodle.application.Application
import io.nacular.doodle.application.application
import io.nacular.doodle.core.Display
import org.kodein.di.DI.Module
import org.kodein.di.bindProvider
import org.kodein.di.instance
class MyApp(display: Display, animator: Animator): Application {
init {
// ..
}
override fun shutdown() {}
}
//sampleStart
val AnimationModule = Module(name = "AnimationModule") {
bindProvider<Animator> { AnimatorImpl(timer = instance(), animationScheduler = instance()) }
}
fun main() {
application(modules = listOf(AnimationModule)) {
MyApp(display = instance(), animator = instance())
}
}
//sampleEnd
Animating a single value
Doodle offers two main APIs to handle common animation use cases. The first allows you to animate a value from start to finish and handle each increment within a lambda. This is a very flexible API that can be used to accomplish most use cases.
These animations are of the following form. This examples animates a Float
value between 0
and 1
using a linear
easing that takes 250 ms
.
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.transition.linear
import io.nacular.doodle.animation.tweenFloat
import io.nacular.measured.units.Time.Companion.seconds
import io.nacular.measured.units.times
fun singleValue(animate: Animator) {
//sampleStart
animate(0f to 1f, using = tweenFloat(linear, 0.25 * seconds)) { value: Float ->
// ...
}
//sampleEnd
}
The lambda provided to the Animator
will be evaluated every time the animating value changes. This allows the app to take action in real time.
Notice that this API does not make any references to View
or any other Doodle concept. This allows animations like these to operate on any compatible data.
The animating ball example above uses this approach to change the ball's vertical position.
Grouped animations
You can also animate a series of properties under a single Animation
handle. This makes it easy to track and modify the behavior of several properties at once instead of having an animation for each.
This code below shows how the looping ball example animates 3 different properties. The resulting animation
will manage all the underlying items as expected.
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.RepetitionType.Reverse
import io.nacular.doodle.animation.loop
import io.nacular.doodle.animation.transition.EasingFunction
import io.nacular.doodle.animation.tweenColor
import io.nacular.doodle.animation.tweenDouble
import io.nacular.doodle.animation.tweenSize
import io.nacular.doodle.core.view
import io.nacular.doodle.drawing.Color.Companion.Blue
import io.nacular.doodle.drawing.Color.Companion.Red
import io.nacular.doodle.geometry.Size
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun groupedValues(animate: Animator, easing: EasingFunction, duration: Measure<Time>) {
val circle = view {}
//sampleStart
animate {
Blue to Red using loop(tweenColor(easing, duration), type = Reverse).invoke {
circle.backgroundColor = it
}
200.0 to 100.0 using loop(tweenDouble(easing, duration), type = Reverse).invoke {
circle.y = it
}
Size(100) to Size(75) using loop(tweenSize(easing, duration), type = Reverse).invoke {
circle.size = it
}
}
//sampleEnd
}
There is no requirement that the animations define within the block have the same duration or are even of the same type.
Chaining animations
Animations within a block can be chained together to create new sequential animations that have nice cancellation properties. This is helpful for cases where you'd like to start a follow-up animation after a previous one completes, while keeping the ability to cancel the chain if needed. Manually doing this via onComplete
results in creating additional, independent animations, which requires more bookkeeping to cancel everything.
This app shows an example of animation chaining. The wheel bounces, then slides and rolls down the hill. The bounce animates first, then the slide and roll are done as a single chained block that begins when the bounce completes.
package io.nacular.doodle.docs.apps
import io.nacular.doodle.animation.Animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.transition.easeOutBounce
import io.nacular.doodle.animation.tween
import io.nacular.doodle.animation.tweenDouble
import io.nacular.doodle.animation.tweenPoint
import io.nacular.doodle.application.Application
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.View
import io.nacular.doodle.core.height
import io.nacular.doodle.core.renderProperty
import io.nacular.doodle.core.view
import io.nacular.doodle.core.width
import io.nacular.doodle.docs.utils.controlBackgroundColor
import io.nacular.doodle.drawing.AffineTransform.Companion.Identity
import io.nacular.doodle.drawing.Canvas
import io.nacular.doodle.drawing.Color
import io.nacular.doodle.drawing.Color.Companion.Black
import io.nacular.doodle.drawing.Color.Companion.Lightgray
import io.nacular.doodle.drawing.Color.Companion.White
import io.nacular.doodle.drawing.Stroke
import io.nacular.doodle.drawing.opacity
import io.nacular.doodle.drawing.paint
import io.nacular.doodle.event.PointerListener.Companion.on
import io.nacular.doodle.geometry.Circle
import io.nacular.doodle.geometry.Point
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.Size
import io.nacular.doodle.geometry.Vector3D
import io.nacular.doodle.geometry.circumference
import io.nacular.doodle.geometry.inset
import io.nacular.doodle.geometry.lineTo
import io.nacular.doodle.geometry.path
import io.nacular.doodle.layout.constraints.constrain
import io.nacular.doodle.layout.constraints.fill
import io.nacular.doodle.utils.autoCanceling
import io.nacular.measured.units.Angle.Companion.acos
import io.nacular.measured.units.Angle.Companion.degrees
import io.nacular.measured.units.Time.Companion.seconds
import io.nacular.measured.units.times
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sqrt
class AnimationChainingApp(private val display: Display, private val animate: Animator): Application {
private class Wheel: View() {
val radius get() = circle.radius + thickness / 2
val diameter get() = 2 * radius
val circumference get() = 2 * PI * radius
var bottom get() = position + Point(radius, 2 * radius); set(new) {
position = new - Point(radius, 2 * radius)
}
val center get() = position + Point(radius, radius)
var rotation by renderProperty(0 * degrees)
private val thickness = 20.0
private val dashLength get() = circle.circumference / 8
private val circle get() = Circle(
center = Point(width, height) / 2.0,
radius = minOf(width/2, height/2) - thickness / 2
)
override fun contains(point: Point) = false
override fun render(canvas: Canvas) {
val c = circle
val d = dashLength
canvas.circle(c.inset(-thickness / 2 + 0.5), Stroke(Lightgray))
canvas.circle(c.inset( thickness / 2 ), Stroke(Lightgray))
canvas.rotate(around = Point(radius, radius), by = rotation) {
circle(
c,
Stroke(
color = Color.Blue,
dashes = doubleArrayOf(d, d),
thickness = thickness,
)
)}
}
}
private var animation: Animation<*>? by autoCanceling()
private val wheel = Wheel().apply { size = Size(100) }
private var rampBounds = calculateRampBounds()
private val rollBounce = easeOutBounce(0.15f)
private val fallDuration = 1 * seconds
private val rollDuration = 3 * seconds
init {
display += view {
+ wheel
render = {
outerShadow(
color = Black opacity 0.05f,
vertical = -10.0,
blurRadius = 10.0
) {
path(
path(0.0, rampBounds.y)
.lineTo(rampBounds.position )
.lineTo(rampBounds.right, rampBounds.bottom)
.lineTo(0.0, rampBounds.bottom)
.finish(),
White.paint
)
}
}
pointerChanged += on(
entered = { startAnimation() },
pressed = { startAnimation() }
)
}
display.fill(controlBackgroundColor.paint)
display.layout = constrain(display.first(), fill)
display.sizeChanged += { _,_,_ ->
rampBounds = calculateRampBounds()
animation?.cancel()
resetWheel()
}
}
private fun resetWheel() {
wheel.bottom = Point(wheel.radius, 2 * wheel.radius)
wheel.rotation = 0 * degrees
wheel.transform = Identity
}
private fun startAnimation() {
resetWheel()
animation = animate {
val rampLength = sqrt(rampBounds.width * rampBounds.width + rampBounds.height * rampBounds.height)
val radiusLengthRatio = wheel.radius / rampLength
val bottomYOffsetFromCenter = rampBounds.width * radiusLengthRatio
val bottomXOffsetFromCenter = rampBounds.height * radiusLengthRatio
val bottomXOffsetWall = wheel.radius + bottomXOffsetFromCenter
val bottomYOffsetFloor = bottomXOffsetWall * rampBounds.height / rampBounds.width
val tippingPoint = Identity.rotate(
around = Point(wheel.radius, rampBounds.y),
by = acos(rampBounds.width / rampLength)
)(Point(wheel.radius, rampBounds.y - wheel.radius))
val wheelBottomEnd = Point(
rampBounds.right - bottomXOffsetWall + bottomXOffsetFromCenter,
rampBounds.bottom - bottomYOffsetFloor - bottomYOffsetFromCenter + wheel.radius
)
val rollLength = wheelBottomEnd distanceFrom wheel.bottom
val rotations = rollLength / wheel.circumference
//sampleStart
wheel.bottom to Point(wheel.radius, rampBounds.y) using (tweenPoint(easeOutBounce, fallDuration)) {
// (1) Ball falls and bounces
wheel.bottom = it
} then {
// (2) Then it slides down the hill and bounces off the wall, by animating x and deriving y
wheel.bottom.x to rampBounds.width using (tweenDouble(rollBounce, rollDuration)) { x ->
wheel.bottom = Point(x, wheelBottomY(x, tippingPoint))
}
// (2) While rolling at the same time
0 * degrees to 360 * degrees * rotations using (tween(degrees, rollBounce, rollDuration)) {
wheel.rotation = it
}
}
//sampleEnd
}
}
/**
* Calculate the ball's bottom position to keep it on the ramp as it moves horizontally.
*
* The first phase of the move is the portion where the ball is rolling over the edge.
* Then it transitions to normal linear motion.
*/
private fun wheelBottomY(centerX: Double, tippingPoint: Vector3D) = when {
centerX < tippingPoint.x -> rampBounds.y - wheel.radius * cos((centerX - wheel.radius) / wheel.radius)
else -> tippingPoint.y + (centerX - tippingPoint.x) * rampBounds.height / rampBounds.width
} + wheel.radius
private fun calculateRampBounds() = Rectangle(
x = wheel.radius,
y = display.height / 2,
width = display.width - wheel.radius,
height = display.height / 2
)
override fun shutdown() {
animation?.cancel()
}
}
Tweens
Tween animations let you interpolate data between two values using a curve (or easing function). These animations work with data that can be converted to numeric (Double
) representation. Doodle has several tween*
functions, all with the form:
tween*(
easing : EasingFunction,
duration: Measure<Time>,
delay : Measure<Time>
)
Numeric data
Doodle supports tweens with any Number
type and includes APIs for working with Float
, Double
and Int
. This shows how you can animate a Float
from 0
to 100
using a linear
tween that lasts 250 ms
, and starts immediately (no delay
).
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.transition.linear
import io.nacular.doodle.animation.tweenFloat
import io.nacular.measured.units.Time.Companion.milliseconds
import io.nacular.measured.units.times
fun numeric(animate: Animator) {
//sampleStart
animate(0f to 1f, using = tweenFloat(linear, 250 * milliseconds)) {
println(it)
}
//sampleEnd
}
You can also use tweenDouble
and tweenInt
to work with other number types.
Colors, Sizes, ...
Doodle also allows you to tween non-numeric data, as long as it can be converted to a numeric representation. This works really well for things like Point
, Size
, Rectangle
, and Color
. All of these data types are supported by built-in tween variants. So you can do any of the following.
- Point
- Size
- Rect
- Color
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.transition.EasingFunction
import io.nacular.doodle.animation.tweenPoint
import io.nacular.doodle.geometry.Point
import io.nacular.doodle.geometry.Point.Companion.Origin
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun point(animate: Animator, easing: EasingFunction, duration: Measure<Time>) {
//sampleStart
animate(Origin to Point(100, 200), using = tweenPoint(easing, duration)) {
// ...
}
//sampleEnd
}
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.transition.EasingFunction
import io.nacular.doodle.animation.tweenSize
import io.nacular.doodle.geometry.Size
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun size(animate: Animator, easing: EasingFunction, duration: Measure<Time>) {
//sampleStart
animate(Size(100) to Size(100, 200), using = tweenSize(easing, duration)) {
// ...
}
//sampleEnd
}
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.transition.EasingFunction
import io.nacular.doodle.animation.tweenRect
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.Rectangle.Companion.Empty
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun rect(animate: Animator, easing: EasingFunction, duration: Measure<Time>) {
//sampleStart
animate(Empty to Rectangle(100, 200), using = tweenRect(easing, duration)) {
// ...
}
//sampleEnd
}
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.transition.EasingFunction
import io.nacular.doodle.animation.tweenColor
import io.nacular.doodle.drawing.Color.Companion.Green
import io.nacular.doodle.drawing.Color.Companion.Red
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun color(animate: Animator, easing: EasingFunction, duration: Measure<Time>) {
//sampleStart
animate(Red to Green, using = tweenColor(easing, duration)) {
// ...
}
//sampleEnd
}
This is an example of a Switch with a custom Behavior
that draws a heart and animates between selection state using an arbitrary easing function.
Easing functions
The tween*
functions work with one or more EasingFunction
, a duration
of (Measure<Time>
), and an optional delay
(also a Measure<Time>
). EasingFunction
is simply a function that takes a Float
between 0
and 1
and returns another Float
scaled to 0
and 1
. Doodle includes many common easing functions already.
But it is just as simple to create a custom one as well. Simply provide a function of the following form that maps inputs between 0
and 1
to a normalized output in (or slightly outside) that range.
CustomEasing
(Float) -> Float
Key frames
Doodle also supports key-frame animations. These let you specify intermediate values the animated property will have at specific times, and the easing curves between these values. The following example animates the ball's y position so it takes 1/3 of the total duration to get from one level to the next. But it provides 3 separate curves to animate between each level.
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.keyFramesDouble
import io.nacular.doodle.animation.transition.easeInElastic
import io.nacular.doodle.animation.transition.easeOutBack
import io.nacular.doodle.animation.transition.linear
import io.nacular.doodle.core.View
import io.nacular.measured.units.Time.Companion.seconds
import io.nacular.measured.units.times
fun size(animate: Animator, graph: View) {
//sampleStart
val duration = 1 * seconds
animate(0.0 to graph.height, keyFramesDouble(duration) {
// value | at what time | easing to next value
// --------------------------------------------------------------
0.0 at duration * 0 then easeInElastic
graph.height * 1/3 at duration * 1/3 then linear
graph.height * 2/3 at duration * 2/3 then easeOutBack
}) {
// ...
}
//sampleEnd
}
The default ease from the start
to the first value in the key frames is linear
. The example above overrides this by specifying the first key frame as the start
, then it is able to specify how to ease from the beginning.
Key-frame animations are defined using the keyFrame*
(i.e. keyFramesFloat
) functions that work on data that can be mapped to numeric values like tween*
. Similarly, Doodle provides built-in support for the same set of data types here as it does for tween*
.
- Point
- Size
- Rect
- Color
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.keyFramesPoint
import io.nacular.doodle.geometry.Point
import io.nacular.doodle.geometry.Point.Companion.Origin
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun pointKeyframe(animate: Animator, duration: Measure<Time>) {
//sampleStart
animate(Origin to Point(100, 200), using = keyFramesPoint(duration) {
// key frame definition
}) {
// animation block
}
//sampleEnd
}
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.keyFramesSize
import io.nacular.doodle.geometry.Size
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun sizeKeyframe(animate: Animator, duration: Measure<Time>) {
//sampleStart
animate(Size(100) to Size(100, 200), using = keyFramesSize(duration) {
// key frame definition
}) {
// animation block
}
//sampleEnd
}
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.keyFramesRect
import io.nacular.doodle.geometry.Rectangle
import io.nacular.doodle.geometry.Rectangle.Companion.Empty
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun rectKeyframe(animate: Animator, duration: Measure<Time>) {
//sampleStart
animate(Empty to Rectangle(100, 200), using = keyFramesRect(duration) {
// key frame definition
}) {
// animation block
}
//sampleEnd
}
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.keyFramesColor
import io.nacular.doodle.drawing.Color.Companion.Green
import io.nacular.doodle.drawing.Color.Companion.Red
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun colorKeyframe(animate: Animator, duration: Measure<Time>) {
//sampleStart
animate(Red to Green, using = keyFramesColor(duration) {
// key frame definition
}) {
// animation block
}
//sampleEnd
}
Repetition
Finite repeating
You can repeat animations like tween*
or keyFrame*
using the repeat
wrapper. This wrapper takes an animationPlan
, the number of times
it should be repeated and the type of repetition (Restart
or Reverse
).
For example, you can repeat
a linear
tween from 0
to 1
as follows.
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.RepetitionType.Reverse
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.repeat
import io.nacular.doodle.animation.transition.EasingFunction
import io.nacular.doodle.animation.tweenFloat
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun repeat(animate: Animator, easing: EasingFunction, duration: Measure<Time>) {
//sampleStart
animate(0f to 1f, repeat(tweenFloat(easing, duration), times = 2, type = Reverse)) {
// ...
}
animate {
0f to 1f using (repeat(tweenFloat(easing, duration), times = 2, type = Reverse)) {
// ...
}
}
//sampleEnd
}
The type
determines what the animation will do at each repetition boundary. Restart
will run the animation again as though it were just beginning. While Reverse
will run the animation from its end to start.
Loops
Sometimes you'd like to run an animation that repeats indefinitely. The loop
function makes this easy. It is just like repeat
, except it takes no times
value and continues "forever". The following app runs a looping animation (that reverses) to change the ball's y
, color
, and size
based on an easing.
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.RepetitionType.Reverse
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.loop
import io.nacular.doodle.animation.transition.EasingFunction
import io.nacular.doodle.animation.tweenFloat
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun loop(animate: Animator, easing: EasingFunction, duration: Measure<Time>) {
//sampleStart
animate.invoke(0f to 1f, loop(tweenFloat(easing, duration), type = Reverse)) {
// ...
}
animate {
0f to 1f using (loop(tweenFloat(easing, duration), type = Reverse)) {
// ...
}
}
//sampleEnd
}
Delays
All animations can be delayed using the after
function. It takes a time to delay and executes the given animation after that delay has passed.
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.after
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.transition.EasingFunction
import io.nacular.doodle.animation.tweenFloat
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun delay(animate: Animator, easing: EasingFunction, duration: Measure<Time>, delay: Measure<Time>) {
//sampleStart
animate(0f to 1f, after(delay, tweenFloat(easing, duration))) {
// ...
}
animate {
0f to 1f using (after(delay, tweenFloat(easing, duration))) {
// ...
}
}
//sampleEnd
}
Animating properties
An Animator
can also be used to create animatable properties for a class. These properties will then animate from their current value to a new one whenever they are changed. This is done as follows:
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.transition.linear
import io.nacular.doodle.animation.tweenColor
import io.nacular.doodle.core.View
import io.nacular.doodle.drawing.Color.Companion.Red
import io.nacular.measured.units.Time.Companion.milliseconds
import io.nacular.measured.units.times
//sampleStart
class MyView(animate: Animator): View() {
// ..
var color by animate(default = Red, tweenColor(linear, 250 * milliseconds)) { old, new ->
rerenderNow()
}
// ..
}
//sampleEnd
Doodle manages the animation lifecycle of these properties for you and optionally notifies you of changes throughout the animation. This makes it easy to react as the value changes. An existing animation will be canceled if a new value is set for the property, and the property will begin animating towards that new value.
An interruption in the animation will reset the elapsed time. So setting color
in the above example to something new while it was in the middle of animating would require 250ms to get from the intermediate value to the new value specified.
Animation lifecycle
All animations result in an Animation
instance being created to track their lifecycle. This type implements Completable
, which means it notifies listeners when it is completed
or canceled
. You can register to be notified of these state changes as follows:
package animation
import io.nacular.doodle.animation.Animation
fun lifeCycle(animation: Animation<*>) {
//sampleStart
animation.completed += { /*...*/ }
animation.canceled += { /*...*/ }
//sampleEnd
}
Doodle provides the autoCanceling
property delegate to automatically cancel Completable
instances when a new value is assigned to them. This is useful for animations as well.
package animation
import io.nacular.doodle.animation.Animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.transition.easeInElastic
import io.nacular.doodle.animation.transition.linear
import io.nacular.doodle.animation.tweenFloat
import io.nacular.doodle.utils.autoCanceling
import io.nacular.measured.units.Time.Companion.seconds
import io.nacular.measured.units.times
fun autoCancel(animate: Animator) {
//sampleStart
// using autoCanceling ensures existing animation is canceled when a new one is assigned
var someAnimation: Animation<Float>? by autoCanceling()
someAnimation = animate.invoke(0f to 1f, using = tweenFloat(linear, 1 * seconds)) {
// ...
}
// ...
// Any ongoing animation is canceled with this assignment
someAnimation = animate.invoke(1f to 0f, using = tweenFloat(easeInElastic, 1 * seconds)) {
// ...
}
//sampleEnd
}
Animating custom data
It is possible to animate custom data in addition to the built-in types that Doodle offers. The simplest case is when you have some data that can be represented numerically. These data types can be used directly in tween, keyFrame and other existing animation types as long as you provide logic to convert them to and from their numeric form.
Numeric conversion
You can animate data that is convertible to and from numbers using either SingleDataConverter
or MultiDataConverter
. These support single and multi-dimensional data respectively. These converters define how a type is mapped to and from a Double
or Array<Double>
.
Simply implement a custom converter and provide it to one of the generic animation builders as follows:
package animation
import io.nacular.doodle.animation.Animator
import io.nacular.doodle.animation.SingleDataConverter
import io.nacular.doodle.animation.invoke
import io.nacular.doodle.animation.transition.EasingFunction
import io.nacular.doodle.animation.tween
import io.nacular.measured.units.Measure
import io.nacular.measured.units.Time
fun customData(animate: Animator, easing: EasingFunction, duration: Measure<Time>) {
//sampleStart
data class Foo(val value: String)
val customDataConverter = object: SingleDataConverter<Foo> {
override val zero : Foo = Foo("")
override fun serialize (value: Foo ): Double = value.value.toDouble()
override fun deserialize(value: Double): Foo = Foo("$value")
}
animate(Foo("1") to Foo("2"), tween(customDataConverter, easing, duration)) { value: Foo ->
// ...
}
//sampleEnd
}
Doodle has several built-in converters
that use this technique to animate several common data types.
Non-numeric data
The Animator
interface actually works with a lower-level definition of an animation than those used for numeric data. This interface is the entry point for truly custom data that cannot be converted to numeric form. Doodle currently does not have any use cases like this, but the API is there in case applications have a need.
See the AnimationPlan
for more details.