When I saw the below demo I thought "neat, how'd they do that"? That may sound surprising if you knew that I wrote the API for this. In this article I'll describe how three orthogonal features, generic navigation, event triggers and keyframe tracks come together to produce this pleasing animation.
Keyframe animation
Creating the jagged path is perhaps the easiest part. Keyframe animation is something I added to the API a long time ago -- our users felt understandably constrained with only simple easings.
Let's take a look at one-half of the animation, the movement to the left of center.
<Move RelativeTo="Size">
<Keyframe Time="0.25" X="-0.7" Y="0.7"/>
<Keyframe Time="0.5" X="-1.4" Y="0"/>
<Keyframe Time="0.75" X="-2.1" Y="-0.7"/>
<Keyframe Time="1" X="-3.8" Y="-1.4"/>
</Move>
Our animation engine is built around moving things away from where they normally are. In this case, each card has a position on the screen determined by the layout engine. This Move
says how to move it away from that position. The RelativeTo="Size"
also uses the layout size: the fractional values for X
and Y
are multiples of the element's size. You can visualize the path in your head, or just look at the animation again.
Spline track
A jagged path works well in this example, but it's not always desired. We could instead put <Move KeyframeInterpolation="Smooth">
to create a path with rounded corners -- it creates a spline curve from the values.
The animators uses dynamic composition to implement the path. If a
Keyframe
is used it uses theSplineTrack
provider, otherwise theEasingTrack
provider is used. There are a couple more for dealing with discrete values.
How does it create the smooth curve? In short, I'm using the Kochanek-Bartels equation to get tangents, which are turned into a cubic Hermite spline. I ended up using the exact same approach (same code even) to create a smooth curve in our vector drawing. You can read more about this in my spline article.
Animation unaware navigation
Move
specifies the complete path, but the cards are moving only part of the distance on each transition. This is where the linear navigation comes in.
The tricky part in Fuse was to define a navigation system that didn't have prebaked animations. We wanted to give designers free reign in what navigation looks like on the screen. Getting this "right" took a lot of iterations in the code. This history is still apparent in the interfaces for navigation -- I have an outstanding issue to clean this up a bit. Okay, but how does it work?
Each card is a page in a LinearNavigation
, giving them a specific order in the navigation (you can see the left-right order in the demo). The active page is given progress 0 for navigation. The page just "in front" of it is given progress 1, the page just "behind" it has -1. All pages are given a progress based on their distance to the active page. The page two behind the active one has -2, three behind, -3, etc.
These values are continuous. When the navigation switches pages it gradually alters the values; for example, from 0 to 1 for a page moving forward. In this particular example the navigation is configured to use a "CircularOut" easing for the transition.
The key point here is that the navigation only deals with these progress values. It's blissfully unaware of the visual representation of the pages. How then is the animation achieved?
Scaled animators
The Move
I showed above is set inside an EnteringAnimation
trigger.
<EnteringAnimation Scale="0.25">
<Move RelativeTo="Size">
EnteringAnimation
subscribes to progress update events on the page (this uses a C#/Uno event
). It converts the page progress value into a progress value for the Move
timeline -- it seeks to this new value. By default it maps progress 0 to 0%, and progress 1 to 100%.
The
Keyframe
items specify aTime
parameter, which is given in seconds. When driven by something likeEnteringAnimation
the actual seconds part is ignored -- the timeline is normalized from 0% to 100% progress and driven by the progress value of the page.
We don't want to animate over the entire Move
timeline. We only want to navigate a bit of the way. This is what the Scale="0.25"
does. The page progress value is multiplied by this amount. A page with progress 1 will thus only seek to 25% of the animation. A page at progress 2, two away from the active one, will seek to 50% in the timeline. These scaled values match the Time
values given in the Keyframe
.
The counterpart to EnteringAnimation
is ExitingAnimation
. We saw that the page progress values can be positive or negative. EnteringAnimation
deals only with position values. ExitingAnimation
deals with the negative values -- converting -1 to 100%. This is how we can provide different animations for pages "in front of" and "behind" the active page.
The names "Entering" and "Exiting" were chosen based on early use-cases where pages were visually entering and leaving the screen. This was actually done prior to coming up the page progress mechanism. We've debated often about these names, yet never managed to come up with something clearer. Nobody seemed to like my
PositivePageProgressAnimation
suggestion.
Other triggers
This demo is a good example of the value in an orthogonal API:
- The navigation system cares only about modifying a page progress value. Whether it's a timed transition, or the user swiping on the screen, it's all mapped back to this simple progress value.
- The trigger system, like
EnteringAnimation
, cares only about converting this value into an animation progress. This makes it easy to create other triggers, likeExitingAnimation
, orActivatingAnimation
andDeactivatingAnimation
if the direction isn't relevant. It also allows forWhileActive
orWhileInactive
, which can both specify aTreshhold
for how active they need to be. - The
Move
timeline is just a generic animator and doesn't care what's driving it. ARotate
orScale
could be plugged in here just as easily. TheKeyframe
applies equally to any of these animators.
It's nice to see it work out so well in this demo.
Top comments (0)