A week or so ago I noticed a really slick piece of animation done as part of a Flutter app. Unfortunately I can no longer find the tweet as the account has been removed. However, it perked my interest enough to go playing with animations in XAML to see what I can do. Note that I’m not trying to reproduce the original sample, just demonstrate that can be done with a bit of XAML and a bit of code.
Since I can’t show you the original animation, I’ll skip to the end and show you what I came up with. What you’re seeing in the following animated GIF is a drag animation that follows the mouse/touch pointer across the screen – imagine you’re using this to reveal a side menu or a different part of the UI of the app.
Ok, so how did I achieve this. Well, whilst I’d love to say that it was as easy as opening Blend, drawing a curve and then recording a storyboard, that is NOT what happened. In fact there’s almost no designer support for creating either the curve or the storyboard, mainly because it involves defining and then animation a path. However, with this said, I did do all the coding in Blend for Visual Studio and it did update the designer as I made changes.
If we take a look at a single frame, we can see what the shape is that we’re going to try to create.
Starting in the top left corner (0,0) we need a path that consists of
- Horizontal line to (20,0)
- Vertical line to (20,100)
- A curve to (80,150)
- A curve to (20,200)
- Vertical line to (20,1000)
- Horizontal line to (0,1000)
- Vertical line to (0,0) to close the shape
In XAML this looks like
<Path x:Name="path"
Stroke="Red"
Fill="Red"
StrokeThickness="2">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigureCollection>
<PathFigure StartPoint="0,0">
<PathFigure.Segments>
<PathSegmentCollection>
<LineSegment Point="20,0"
x:Name="StartLine" />
<LineSegment Point="20,100"
x:Name="TopLine"/>
<BezierSegment Point1="20,125"
x:Name="Part1"
Point2="80,135"
Point3="80,150" />
<BezierSegment Point1="80,165"
x:Name="Part2"
Point2="20,175"
Point3="20,200" />
<LineSegment Point="20,1000" x:Name="BottomLine"
/>
<LineSegment Point="0,1000"
x:Name="EndLine" />
</PathSegmentCollection>
</PathFigure.Segments>
</PathFigure>
</PathFigureCollection>
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
The next part is to animate this shape, which we can do by creating a storyboard and then moving each of the points on the shape. From the Objects and Timeline tool window, click the green + button to create a new Storyboard, which we’ve called our customStoryboard. Unfortunately this is where the designer support for animations runs out.
Dropping into XAML we can define the animations that make up the storyboard
<Page.Resources>
<Storyboard x:Name="customStoryboard">
<PointAnimationUsingKeyFrames Storyboard.TargetProperty="Point"
Storyboard.TargetName="StartLine"
EnableDependentAnimation="True">
<LinearPointKeyFrame KeyTime="0:0:0.5"
x:Name="StartLinePoint"
Value="20,0"/>
</PointAnimationUsingKeyFrames>
<PointAnimationUsingKeyFrames Storyboard.TargetProperty="Point"
Storyboard.TargetName="TopLine"
EnableDependentAnimation="True">
<LinearPointKeyFrame KeyTime="0:0:0.5"
x:Name="TopLinePoint"
Value="20,100"/>
</PointAnimationUsingKeyFrames>
<PointAnimationUsingKeyFrames Storyboard.TargetProperty="Point1"
Storyboard.TargetName="Part1"
EnableDependentAnimation="True">
<LinearPointKeyFrame KeyTime="0:0:0.5"
x:Name="Part1Point1"
Value="20,125"/>
</PointAnimationUsingKeyFrames>
<PointAnimationUsingKeyFrames Storyboard.TargetProperty="Point2"
Storyboard.TargetName="Part1"
EnableDependentAnimation="True">
<LinearPointKeyFrame KeyTime="0:0:0.5"
x:Name="Part1Point2"
Value="150,135"/>
</PointAnimationUsingKeyFrames>
<PointAnimationUsingKeyFrames Storyboard.TargetProperty="Point3"
Storyboard.TargetName="Part1"
EnableDependentAnimation="True">
<LinearPointKeyFrame KeyTime="0:0:0.5"
x:Name="Part1Point3"
Value="150,150"/>
</PointAnimationUsingKeyFrames>
<PointAnimationUsingKeyFrames Storyboard.TargetProperty="Point1"
Storyboard.TargetName="Part2"
EnableDependentAnimation="True">
<LinearPointKeyFrame KeyTime="0:0:0.5"
x:Name="Part2Point1"
Value="150,165"/>
</PointAnimationUsingKeyFrames>
<PointAnimationUsingKeyFrames Storyboard.TargetProperty="Point2"
Storyboard.TargetName="Part2"
EnableDependentAnimation="True">
<LinearPointKeyFrame KeyTime="0:0:0.5"
x:Name="Part2Point2"
Value="20,175"/>
</PointAnimationUsingKeyFrames>
<PointAnimationUsingKeyFrames Storyboard.TargetProperty="Point3"
Storyboard.TargetName="Part2"
EnableDependentAnimation="True">
<LinearPointKeyFrame KeyTime="0:0:0.5"
x:Name="Part2Point3"
Value="20,200"/>
</PointAnimationUsingKeyFrames>
<PointAnimationUsingKeyFrames Storyboard.TargetProperty="Point"
Storyboard.TargetName="BottomLine"
EnableDependentAnimation="True">
<LinearPointKeyFrame KeyTime="0:0:0.5"
x:Name="BottomLinePoint"
Value="20,1000"/>
</PointAnimationUsingKeyFrames>
</Storyboard>
</Page.Resources>
After defining the storyboard, now you can return to the Objects and Timeline window and select the storyboard from the dropdown. Once selected you can position the cursor in the timezone.
At close to the 0.5 second mark (close to completion for most animations) the screen looks like this – definitely on the right track
Up to this point we’ve defined a set of line segments that make up a connected entity and a basic animation that simply adjusts the position of the points. What’s left to do is for the animation to track the mouse cursor or touch point.
We’re going to intercept and handle the Pointer_Moved event that is raised whenever a pointer moves across the page. In the event handler we need to calculate and apply the new position co-ordinates so that the animation can be amended.
The following code illustrate updating values (points) in the animation.
public MainPage()
{
this.InitializeComponent();
SizeChanged += MainPage_SizeChanged;
}
private void MainPage_SizeChanged(object sender, SizeChangedEventArgs e)
{
Part1.Point3 = new Point(500, e.NewSize.Height / 2);
Part2.Point2 = new Point(50, e.NewSize.Height - 100);
Part2.Point3 = new Point(0, e.NewSize.Height);
EndLine.Point = new Point(0, e.NewSize.Height);
}
private void Start_Animation(object sender, RoutedEventArgs e)
{
//myStoryboard.Begin();
}
const int StartWidth = 50;
const int MaxDiffX = 400;
const double HeightToWidthRatio = 1.5;
private void path_PointerMoved(object sender, PointerRoutedEventArgs e)
{
// The location of the center is where the mouse/touch point is
var newCenter = e.GetCurrentPoint(this).Position;
var part1_point3 = newCenter;
// Next determine the width of the shape
var startX = Math.Max(StartWidth, newCenter.X - MaxDiffX);
var width = newCenter.X - startX;
// Calculate the height of the shape using the ratio
var height = width * HeightToWidthRatio;
// As we approach the other size, calculate how much space there is
// and if close, reduce the width of the shape accordingly
var remainingWidth = ActualWidth - newCenter.X;
if (remainingWidth < width)
{
width = remainingWidth;
startX = Math.Max(StartWidth, newCenter.X - width);
}
// The first line goes from 0,0 out along the X axis
var start_point = new Point(startX, 0);
// The last line goes from 0,<actual height> out along the X axis
var end_point = new Point(startX, this.ActualHeight);
// Calculate top and bottom of the shape
var top = newCenter.Y - height / 2;
var top_point = new Point(start_point.X, top);
var bottom = newCenter.Y + height / 2;
var part2_point3 = new Point(start_point.X, bottom);
// Spread the remaining points in the shape
var part1_point1 = new Point(start_point.X, top+ height * 0.25);
var part1_point2 = new Point(newCenter.X, top+ height * 0.35);
var part2_point1 = new Point(newCenter.X, top + height * 0.65);
var part2_point2 = new Point(start_point.X, top+ height * 0.75);
StartLinePoint.Value = start_point;
TopLinePoint.Value = top_point;
Part1Point1.Value = part1_point1;
Part1Point2.Value = part1_point2;
Part1Point3.Value = part1_point3;
Part2Point1.Value = part2_point1;
Part2Point2.Value = part2_point2;
Part2Point3.Value = part2_point3;
BottomLinePoint.Value = end_point;
if (customStoryboard.GetCurrentState() != Windows.UI.Xaml.Media.Animation.ClockState.Stopped)
{
customStoryboard.Pause();
customStoryboard.Resume();
}
else
{
customStoryboard.Begin();
}
}
And that’s it – a basic animation written in XAML, with a bit of code behind to check temp. It’s worth pointing out that the use of point based animations will result in smooth animations, rather than simply setting the points directly on the shape.