Google Developers
Published in

Google Developers

Defining motion paths in MotionLayout

Intro to MotionLayout part IV: Keyframes Deep Dive

Introduction

  • 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

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

  • 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

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?

  • 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

  • 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

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

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

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

Arc Motion

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

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

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

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

  • 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

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