3

Animations | Doodle

 1 year ago
source link: https://nacular.github.io/doodle/docs/animations
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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.

Requirements

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")
}

// ...

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.

val AnimationModule = Module(name = "AnimationModule") {
bindProvider<Animator> { AnimatorImpl(instance(), instance()) }
}

application(modules = listOf(..., AnimationModule, ...)) {
MyApp(...)
}

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 form:

val animate: Animator

animate(...) { value ->
// ...
}

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 Views 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.

val animate: Animator

val animation = 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
}
}

There is no requirement that the animations define within the block have the same duration or are even of the same type.

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:

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 250ms, and starts immediately (no delay).

animate(0f to 100f, using = tweenFloat(linear, 250 * milliseconds)) {
println(it)
}

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 Position, 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
  • Rectangle
  • Color
Animate Point
animate(Origin to Point(100, 200), using = tweenPoint(...)) {
// ...
}

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.

Inspired by Tore Bernhoft

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 (i.e. easeInQuad, easeInOutQuint, easeOutElastic, easeInOutBounce and others).

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.

t
y
val duration = 1 * seconds

animation = 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
}) {
// ...
}

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* 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 for here as it does for tween*.

  • Point
  • Rectangle
  • Color
Animate Point
animate(Origin to Point(100, 200), using = keyFramesPoint(...)) {
// key frame definition
}) {
// animation block
}

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.

animate(0f to 1f, repeat(tweenFloat(easing, duration), times = 2, type = Reverse)) {
// ...
}

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.

animate(200.0 to 100.0, loop(tweenDouble(easing, duration), type = Reverse)) {
// ...
}

Animating Properties

The Animator interface 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:

class MyView(animate: Animator): View() {
// ..

var color by animate(default = Red, tweenColor(linear, 250 * milliseconds)) { _,_ ->
rerenderNow()
}

// ..
}

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<T> 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:

animation.completed += { /*...*/ }
animation.canceled += { /*...*/ }

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.

// using autoCanceling ensures existing animation is canceled when a new one is assigned
var someAnimation: Animation<Float>? by autoCanceling()

someAnimation = animate(0f to 1f, using tween(linear, 1 * seconds)) {
// ...
}

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<T> or MultiDataConverter<T>. These support single and multi-dimensional data respectively. These converters define how a type is mapped to and from a Double or Array<Double>.

AppFramework*DataConverterloopanimate(...)1serialize(...)2animate numeric value3deserialize(...)4animation(...) { value -> ... }5AppFramework*DataConverter

Simply implement a custom converter and provide it to one of the generic animation builders as follows:

object CustomDataConverter: SingleDataConverter<Foo> {
public val zero: Foo = ...
public fun serialize (value: Foo ): Double { ... }
public fun deserialize(value: Double): Foo { ... }
}

animate(Foo1 to Foo2, tween(CustomDataConverter, linear, 1 * seconds)) { value: Foo ->
}

This is actually how Doodle supports animation of Rectangle and other types. It defines custom converters like this:

// Defines Rectangle as convertible to 4 Doubles for animation.
public val Rectangle.Companion.animationConverter: MultiDataConverter<Rectangle> get() = object: MultiDataConverter<Rectangle> {
override val size get() = 4
override val zero = Empty
override fun serialize (value: Rectangle ) = arrayOf(value.x, value.y, value.width, value.height)
override fun deserialize(value: Array<Double>) = Rectangle(value[0], value[1], max(0.0, value[2]), max(0.0, value[3]))
}

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.

The AnimationPlan<T> interface is defined as follows:

interface AnimationPlan<T> {
// Returns the value of the animation after [elapsedTime].
public fun value(elapsedTime: Measure<Time>): T

// Returns the velocity of the animation at the [elapsedTime].
public fun velocity(elapsedTime: Measure<Time>): Velocity<T>

// Returns `true` IFF the animation if completed after [elapsedTime].
public fun finished(elapsedTime: Measure<Time>): Boolean
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK