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.
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
Y are multiples of the element's size. You can visualize the path in your head, or just look at the animation again.
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
Keyframeis used it uses the
SplineTrackprovider, otherwise the
EasingTrackprovider 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.
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?
Move I showed above is set inside an
<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%.
Keyframeitems specify a
Timeparameter, which is given in seconds. When driven by something like
EnteringAnimationthe 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
The counterpart to
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
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, like
DeactivatingAnimationif the direction isn't relevant. It also allows for
WhileInactive, which can both specify a
Treshholdfor how active they need to be.
Movetimeline is just a generic animator and doesn't care what's driving it. A
Scalecould be plugged in here just as easily. The
Keyframeapplies equally to any of these animators.
It's nice to see it work out so well in this demo.