Defining motion paths in MotionLayout

Intro to MotionLayout part IV: Keyframes Deep Dive


Introduction

MotionLayout is a new layout focusing on animation, provided with the ConstraintLayout 2.0 library. The previous articles in this series provide a great overview of the system; I would highly encourage you to start with them before reading this article.

  • Introduction to MotionLayout (part I)
  • Custom attributes, image transitions, keyframes (part II)
  • Taking advantage of MotionLayout in your existing layouts (CoordinatorLayout, DrawerLayout, ViewPager) (part III)

The MotionLayout animation system works by interpolating values (typically, position/size of widgets) between two states, specified using the full Constraint system of ConstraintLayout, as well as view attributes. The transition between those two states can also be driven entirely by touch. This system will generally give you great results for your transitions.

In addition to states, MotionLayout also supports Keyframes — briefly introduced in part II of this series— which we are going to cover in depth in this article. Note that while keyframes are great, it’s definitely a more specialized tool; one you may not need, or need only sporadically.

Keep in mind that adding motion in your application should be meaningful; don’t over do it!

But, should you need extra capabilities to define your transition, keyframes will expand what you can do with MotionLayout. As you will see, there is a lot to cover:


Keyframes : a Rendez-vous in Time

At a high level, Keyframes allow you to specify a change, at a given time during the interpolation between two states.

There are different types of Keyframes supported by MotionLayout:

  • Position keyframe : KeyPosition
  • Attribute keyframe : KeyAttribute
  • Cycle keyframe : KeyCycle
  • TimeCycle keyframe : KeyTimeCycle
Note that each type of keyframe is independent from the others — that is, you don’t need to define all of them at the same points (but you cannot define at the same point multiple keyframes of the same type)

Common Attributes

All keyframes (Position, Attribute, Cycle, TimeCycle) share some common attributes:

  • motion:framePosition : when does the keyframe apply during the transition (from 0 to 100)
  • motion:target : which object is affect by this keyframe
  • motion:transitionEasing : which easing curve to use (default is linear)
  • motion:curveFit : spline (default) or linear — which interpolation curve is fitted to the keyframes. The default is a monotonic spline curve, making for smoother transitions, but you can decide to have linear segments instead.

Position Keyframes

The position keyframes are likely going to be the most common keyframes you will encounter or use. They allow you to modify the path a widget will take on screen during a transition. For example, let’s take the following animation of a single widget, contained in a MotionLayout (“parent”):

We have a start (bottom left) and end (top right) states, and the motion path is simply the linear interpolation between those two states — the widget will move in a straight line.

By introducing a position keyframe, we can change the motion path to a curved motion:

Adding more keyframes will allow you to create complex motion paths.

<KeyFrameSet>
<KeyPosition
motion:keyPositionType="pathRelative"
motion:percentX="0.75"
motion:percentY="-0.3"
motion:framePosition="25"
motion:target="@id/button"/>
<KeyPosition
motion:keyPositionType="pathRelative"
motion:percentY="-0.4"
motion:framePosition="50"
motion:target="@id/button"/>
<KeyPosition
motion:keyPositionType="pathRelative"
motion:percentX="0.25"
motion:percentY="-0.3"
motion:framePosition="75"
motion:target="@id/button"/>
</KeyFrameSet>

Why Position Keyframes?

You may ask yourself what is the point of position keyframes, if ConstraintSets already allow you to position your widget in a very flexible manner. There are a few reasons:

  • keyframes are expressing a transient modification, while ConstraintSets express a “resting” state
  • keyframes are more lightweight than ConstraintSet to compute
  • position keyframes allow you to manipulate the motion path of a widget— ConstraintSets instead specify the position of a widget, relative to other widgets.
Note: It is possible to define multiple ConstraintSets within a MotionScene, so if you have a multi-step motion where such steps are valid “resting” state, you can use them instead of keyframes. Transitioning state to state would have to be done in code (change listeners are available).

XML Representation

Keyframes are contained in a <KeyFrameSet> attribute, itself contained in a <Transition> in the MotionScene file. Position keyframes are represented via the tag <KeyPosition>, and need to contain at least:

  • target: the widget the keyframe apply to
  • framePosition: from 0 to 100, when does the keyframe applies
  • keyPositionType: the coordinate system used, parentRelative, deltaRelative, pathRelative
  • percentX / percentY : the (x,y) coordinate of the position
<Transition ...>
<KeyFrameSet>
<KeyPosition
motion:keyPositionType="parentRelative"
motion:percentY="0.25"
motion:framePosition="50"
motion:target="@+id/button"/>
</KeyFrameSet>
</Transition>

Different coordinate systems

The start and end states in MotionLayout allow complex positioning. As ConstraintSets, they have access to the full capabilities of ConstraintLayout. The system will correctly handle changes in density, screen orientation, language, etc., for those states.

For position keyframes to be useful in such a system, we need them to be able to position themselves in a similar, adaptive manner — we can’t simply have them be defined as fixed positions.

To address this, yet keep the keyframes system lightweight, we came up with a flexible approach — each keyframe’s position is expressed in terms of a (x,y) coordinate pair, in a given coordinate system:

  • motion:percentX=”<float>”
  • motion:percentY=”<float>”

The meaning of those coordinates depends on the type of coordinate system used: parentRelative, deltaRelative, or pathRelative.

Note: each keyframe position is done individually — each one can be expressed using their own coordinate system, independent from the others.

parentRelative

The coordinates are expressed relative to the parent container. This is a very straightforward and intuitive way to express the position of the keyframe, and will often be enough. You typically would use this for large motions that need to be relative to the container.

As this coordinate system is based only on the parent dimensions, and not on the start/end positions of the moving widget, you may encounter situations where the resulting keyframe position ends in a suboptimal position (relative to the start/end positions).

deltaRelative

This second coordinate system addresses this exact issue, by being defined using the start/end positions. The coordinates express a percentage of the distance between the start and end positions.

Similar to parentRelative, this is a relatively intuitive coordinate system, and will generally give good results as well. It is particularly useful when you want widgets to begin or end in a horizontal or vertical motion.

There’s also a potential issue with it — as it’s defined based on the difference between the start and end position of the widget, if the difference is very small (or nil), the position of the keyframe will not change in the affected axis. For example, if a widget moves from left to right on the screen, while staying at the same height, using a deltaRelative percentY for a position keyframe will have no effect.

pathRelative

This last coordinate system is defined as relative to the straight path between the start and end states. It does address the issue raised with the deltaRelative coordinate system — even on a widget not moving in the vertical axis, using pathRelative will allow a position keyframe to be set off-path. Note that negative coordinates are also supported. It’s a more specialized coordinate system, but one that can be particularly useful at time. An example would be to achieve a curve shape (like an “S”) that will stay constant even if the endpoints change.


Arc Motion

A typical motion type used in Material Design is the arc motion. One way to create arc motion with MotionLayout is by adding correctly placed position Keyframes in between your start and end position, as explained in the previous section.

In ConstraintLayout 2.0.0 alpha 2, we introduced a new way of achieving a perfect arc motion — and it’s even easier to use. You will simply need to add the motion:pathMotionArc attribute to the starting ConstraintSet, to switch from the default linear motion to an arc motion.

Let’s look at a basic example, with the starting state at the bottom right of the screen, and the ending state at the top middle of the screen. Adding the attribute is enough to generate an arc motion:

motion:pathMotionArc=”startHorizontal”

Arc Motion: startHorizontal (preview in Android Studio)

Switching the parameter to:

motion:pathMotionArc=”startVertical”

will reverse the starting direction of the arc:

Arc Motion: startVertical (preview in Android Studio)

You can still use position keyframes, to construct more complex arc paths. The following result:

Arc Motion: intermediate keyframe (preview in Android Studio)

is achieved by adding a keyframe vertically centered on the screen, at the middle of the animation:

<KeyPosition
motion:keyPositionType="parentRelative"
motion:percentY="0.5"
motion:framePosition="50"
motion:target="@id/button"/>

Position keyframes in that scenario can also be used to change the direction of the arc, by setting the motion:pathMotionArc attribute. The attribute can be either flip (flipping the current arc direction), none (revert to linear motion), or explicitly startHorizontal or startVertical.

Arc Motion: intermediate keyframe with flipped direction (preview in Android Studio)
<KeyPosition
motion:keyPositionType="parentRelative"
motion:pathMotionArc="flip"
motion:percentY="0.5"
motion:framePosition="50"
motion:target="@id/button"/>
Arc Motion: keyframe with pathMotionArc=”none” (preview in Android Studio)
<KeyPosition
motion:keyPositionType="parentRelative"
motion:pathMotionArc="none"
motion:percentY="0.5"
motion:framePosition="50"
motion:target="@id/button"/>

Easing

In the previous sections, we covered various mechanisms allowing you to define a motion path. An animation though is more than just the path taken; timing is critical.

As position keyframes are specified in time, you can use them to define how fast or slow a widget will move, depending on the space traveled.

But within a single segment — between the start/end states, or between keyframes — the time interpolation is linear.

You can change this by specifying an easing curve, using the motion:transitionEasing attribute. You can apply this attribute on the ConstraintSets or the Keyframes, and it will apply going forward. It can take the following values:

  • cubic(float, float , float, float), where the arguments are x1,y1,x2,y2 representing the control points of a cubic bezier from 0,0 to 1,1
  • or use a keyword:standard, accelerate, decelerate, which are pre-defined curves, similar to the Material Design definitions.

Standard easing

corresponding to cubic(0.4, 0.0, 0.2, 1)

Typically used to add character to a non touch-driven animation. It works best with elements that begin and end at rest.

Accelerate easing

corresponding to cubic(0.4, 0.0, 1, 1)

Accelerate is commonly used when moving elements out of the scene.

Decelerate easing

corresponding to cubic(0.0, 0.0, 0.2, 1)

Decelerate is commonly used when moving elements into the scene.


KeyAttribute

Attribute keyframes let you specify widget attributes changes at a given point in time during the animation — in other words, they are similar to position keyframes, but work on attributes rather than position.

KeyAttribute example

The above example can be specified adding the following KeyAttribute element in the MotionScene file:

<KeyFrameSet>
<KeyAttribute
android:scaleX="2"
android:scaleY="2"
android:rotation="-45"
motion:framePosition="50"
motion:target="@id/button" />
</KeyFrameSet>

As for KeyPosition, we need to specify framePosition (when the keyframe applies) and a target (which object is affected).

Supported Attributes

The attributes you can use out of the box are view attributes: android:visibility, android:alpha, android:elevation, android:rotation, android:rotationX, android:rotationY, android:scaleX, android:scaleY, android:translationX, android:translationY, android:translationZ

Important: depending on which SDK level you target for your application, some of those attributes will not work:

  • android:elevation was introduced in SDK 21
  • android:translationZ was introduced in SDK 21

Custom Attributes

You can declare custom attributes in both the ConstraintSets and KeyAttribute elements, by adding a child <CustomAttribute> element. This element needs a name (attributeName), which is the name of the getter/setter (minus the set/get prefix), and the value to interpolate or apply, specified using one of the following attributes:

  • customColorValue : apply a color value
  • customColorDrawableValue : apply a color value, wrapped as a drawable
  • customIntegerValue : apply an integer value
  • customFloatValue : apply a float value
  • customStringValue : apply a string value
  • customDimension : apply a dimension value
  • customBoolean : apply a boolean value

For example, here’s the XML corresponding to the above animation:

<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/button" ...>
<CustomAttribute
motion:attributeName="backgroundColor"
motion:customColorValue="#D81B60"/>
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/button" ...>
<CustomAttribute
motion:attributeName="backgroundColor"
motion:customColorValue="#9999FF"/>
</Constraint>
</ConstraintSet>

Conclusion

This article covered the most common keyframes and path specifications available in MotionLayout. We will discuss in part V of this series the KeyCycle and KeyTimeCycle keyframes, which are introducing a very powerful way of adding perturbations (as waveforms) to attributes (path or time-based), allowing for various interesting yet predictable cyclic effects (bounce, shaking, pulsations, etc.).

Various examples on how to use MotionLayout are available on the ConstraintLayout examples github repository.

Feedback, feature request, bug reports? Please file them on the android issue tracker.

More info on ConstraintLayout & MotionLayout ? Follow us on twitter, @camaelon and @johnhoford

Like what you read? Give Nicolas Roard a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.