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
- Position Keyframes
- Arc Motion
- Easing
- Attributes Keyframes
- Cycle Keyframes & TimeCycle Keyframes (which we will cover in part V)
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 keyframemotion: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 toframePosition
: from 0 to 100, when does the keyframe applieskeyPositionType
: 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”
Switching the parameter to:
motion:pathMotionArc=”startVertical”
will reverse the starting direction of the arc:
You can still use position keyframes, to construct more complex arc paths. The following result:
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
.
<KeyPosition
motion:keyPositionType="parentRelative"
motion:pathMotionArc="flip"
motion:percentY="0.5"
motion:framePosition="50"
motion:target="@id/button"/>
<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
Typically used to add character to a non touch-driven animation. It works best with elements that begin and end at rest.
Accelerate easing
Accelerate is commonly used when moving elements out of the scene.
Decelerate easing
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.
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 valuecustomColorDrawableValue
: apply a color value, wrapped as a drawablecustomIntegerValue
: apply an integer valuecustomFloatValue
: apply a float valuecustomStringValue
: apply a string valuecustomDimension
: apply a dimension valuecustomBoolean
: 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