
Introduction
This is the sixth article in the WPFSpark series. Till now I have covered five controls
in WPFSpark - SprocketControl, ToggleSwitch, FluidWrapPanel, SparkWindow, and FluidPivotPanel.
The previous articles in the WPFSpark series can be accessed here:
- WPFSpark: 1 of n: SprocketControl
- WPFSpark: 2 of n: ToggleSwitch
- WPFSpark: 3 of n: FluidWrapPanel
- WPFSpark: 4 of n: SparkWindow
- WPFSpark: 5 of n: FluidPivotPanel
In this article, I describe in detail the sixth control in this library - the FluidProgressBar control.

Inspiration
FluidProgressBar is inspired from the Indeterminate ProgressBar
of Windows Phone 7+. Jeff Wilcox's High Performance ProgressBar provided much help
in understanding the details of the current ProgressBar being used in Windows Phone 7+.
FluidProgressBar Demystified
FluidProgressBar does not derive from ProgressBar. Instead, it is just a UserControl for depicting the indeterminate
state of the ProgressBar. It also provides the flexibility of having a ratio-based animation defined by the user.
FluidProgressBar basically consists of five Rectangles (a.k.a. Dots) whose Translation in the X direction is animated
so that they appear to converge at the center as they move in from the left end and then diverge as they move towards the right end.
Each of the Dots is animated using a DoubleAnimationUsingKeyFramesAnimation. It consists of four KeyFrames:
- KeyFrame0 - The zeroth KeyFrame or the starting KeyFrame. The location of the
Dot at this KeyFrame is 10 pixels to the left of its parent Grid.
- KeyFrameA - The first KeyFrame. The animation from
KeyFrame0 to KeyFrameA is a Linear animation with
an ExponentionalEaseOut Easing Mode. The location of the Dot at this KeyFrame is defined as a fraction of the total width
of the FluidProgressBar. It usually has values ranging from 0 to 1 with a default value of 0.33.
- KeyFrameB - The first KeyFrame. The animation from
KeyFrameA to KeyFrameB is a Linear animation with
no Easing. The location of the Dot at this KeyFrame is defined as a fraction of the total width of the FluidProgressBar.
It usually has values ranging from 0 to 1 with a default value of 0.63.
- KeyFrameC - The first KeyFrame. The animation from
KeyFrameB to KeyFrameC is a Linear animation with
an ExponentionalEaseIn Easing Mode. The location of the Dot at this KeyFrame is 10 pixels to the right of its parent Grid.
The duration between KeyFrame0 and KeyFrameA is defined by the DurationA property, the duration between KeyFrameA
and KeyFrameB is defined by the DurationB property, and the duration between KeyFrameB and KeyFrameC is defined
by the DurationA property.

There is a default delay of 100 milliseconds between the animation of each Dot due to which they appear as five dots moving in a line,
one behind the other. It is possible to configure the delay duration by setting the Delay property of the FluidProgressBar.
When the FluidProgressBar is created for the first time, it parses the Storyboard containing the animations for the Dots
available in its Resources. Once the Storyboard is obtained, it gets all the KeyFrames involved in animating the Dots
and adds them to a dictionary. These KeyFrames are then manipulated whenever the properties of FluidProgressBar changes. For example,
whenever the FluidProgressBar is loaded for the first time or is resized, it recalculates the locations for KeyFrameA and KeyFrameB
by calling the UpdateKeyFrames() method.
Here is the code for the FluidProgressBar:
public partial class FluidProgressBar : UserControl, IDisposable
{
#region Internal class
private class KeyFrameDetails
{
public KeyTime KeyFrameTime { get; set; }
public List<DoubleKeyFrame> KeyFrames { get; set; }
}
#endregion
#region Fields
Dictionary<int, KeyFrameDetails> keyFrameMap = null;
Dictionary<int, KeyFrameDetails> opKeyFrameMap = null;
Storyboard sb;
bool isStoryboardRunning;
#endregion
#region Dependency Properties
...
#endregion
#region Construction / Initialization
public FluidProgressBar()
{
InitializeComponent();
keyFrameMap = new Dictionary<int, KeyFrameDetails>();
opKeyFrameMap = new Dictionary<int, KeyFrameDetails>();
GetKeyFramesFromStoryboard();
this.SizeChanged += new SizeChangedEventHandler(OnSizeChanged);
this.Loaded += new RoutedEventHandler(OnLoaded);
this.IsVisibleChanged += new DependencyPropertyChangedEventHandler(OnIsVisibleChanged);
}
#endregion
#region Event Handlers
void OnLoaded(object sender, System.Windows.RoutedEventArgs e)
{
UpdateKeyFrames();
StartFluidAnimation();
}
void OnSizeChanged(object sender, System.Windows.SizeChangedEventArgs e)
{
RestartStoryboardAnimation();
}
void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (this.Visibility == Visibility.Visible)
{
UpdateKeyFrames();
StartFluidAnimation();
}
else
{
StopFluidAnimation();
}
}
#endregion
#region Helpers
private void StartFluidAnimation()
{
if ((sb != null) && (!isStoryboardRunning))
{
sb.Begin();
isStoryboardRunning = true;
}
}
private void StopFluidAnimation()
{
if ((sb != null) && (isStoryboardRunning))
{
sb.SeekAlignedToLastTick(TimeSpan.FromSeconds(0));
sb.Stop();
isStoryboardRunning = false;
}
}
private void RestartStoryboardAnimation()
{
StopFluidAnimation();
UpdateKeyFrames();
StartFluidAnimation();
}
private void GetKeyFramesFromStoryboard()
{
sb = (Storyboard)this.Resources["FluidStoryboard"];
if (sb != null)
{
foreach (Timeline timeline in sb.Children)
{
DoubleAnimationUsingKeyFrames dakeys = timeline as DoubleAnimationUsingKeyFrames;
if (dakeys != null)
{
string targetName = Storyboard.GetTargetName(dakeys);
ProcessDoubleAnimationWithKeys(dakeys,
!targetName.StartsWith("Trans"));
}
}
}
}
private void ProcessDoubleAnimationWithKeys(DoubleAnimationUsingKeyFrames dakeys, bool isOpacityAnim = false)
{
for (int i = 0; i < dakeys.KeyFrames.Count; i++)
{
DoubleKeyFrame frame = dakeys.KeyFrames[i];
Dictionary<int, KeyFrameDetails> targetMap = null;
if (isOpacityAnim)
{
targetMap = opKeyFrameMap;
}
else
{
targetMap = keyFrameMap;
}
if (!targetMap.ContainsKey(i))
{
targetMap[i] = new KeyFrameDetails() { KeyFrames = new List<DoubleKeyFrame>() };
}
targetMap[i].KeyFrameTime = frame.KeyTime;
targetMap[i].KeyFrames.Add(frame);
}
}
private void UpdateKeyFrames()
{
double width = this.ActualWidth;
if ((width > 0.0) && (this.Visibility == System.Windows.Visibility.Visible))
{
double Point0 = -10;
double PointA = width * KeyFrameA;
double PointB = width * KeyFrameB;
double PointC = width + 10;
UpdateKeyFrame(0, Point0);
UpdateKeyFrame(1, PointA);
UpdateKeyFrame(2, PointB);
UpdateKeyFrame(3, PointC);
}
}
private void UpdateKeyFrame(int key, double newValue)
{
if (keyFrameMap.ContainsKey(key))
{
foreach (var frame in keyFrameMap[key].KeyFrames)
{
if (frame is LinearDoubleKeyFrame)
{
frame.SetValue(LinearDoubleKeyFrame.ValueProperty, newValue);
}
else if (frame is EasingDoubleKeyFrame)
{
frame.SetValue(EasingDoubleKeyFrame.ValueProperty, newValue);
}
}
}
}
private void UpdateKeyTimes(int key, Duration newDuration)
{
switch (key)
{
case 1:
UpdateKeyTime(1, newDuration);
UpdateKeyTime(2, newDuration + DurationB);
UpdateKeyTime(3, newDuration + DurationB + DurationC);
break;
case 2:
UpdateKeyTime(2, DurationA + newDuration);
UpdateKeyTime(3, DurationA + newDuration + DurationC);
break;
case 3:
UpdateKeyTime(3, DurationA + DurationB + newDuration);
break;
default:
break;
}
UpdateOpacityKeyTime(1, DurationA + DurationB + DurationC);
}
private void UpdateKeyTime(int key, Duration newDuration)
{
if (keyFrameMap.ContainsKey(key))
{
KeyTime newKeyTime = KeyTime.FromTimeSpan(newDuration.TimeSpan);
keyFrameMap[key].KeyFrameTime = newKeyTime;
foreach (var frame in keyFrameMap[key].KeyFrames)
{
if (frame is LinearDoubleKeyFrame)
{
frame.SetValue(LinearDoubleKeyFrame.KeyTimeProperty, newKeyTime);
}
else if (frame is EasingDoubleKeyFrame)
{
frame.SetValue(EasingDoubleKeyFrame.KeyTimeProperty, newKeyTime);
}
}
}
}
private void UpdateOpacityKeyTime(int key, Duration newDuration)
{
if (opKeyFrameMap.ContainsKey(key))
{
KeyTime newKeyTime = KeyTime.FromTimeSpan(newDuration.TimeSpan);
opKeyFrameMap[key].KeyFrameTime = newKeyTime;
foreach (var frame in opKeyFrameMap[key].KeyFrames)
{
if (frame is DiscreteDoubleKeyFrame)
{
frame.SetValue(DiscreteDoubleKeyFrame.KeyTimeProperty, newKeyTime);
}
}
}
}
private void UpdateTimelineDelay(Duration newDelay)
{
Duration nextDelay = new Duration(TimeSpan.FromSeconds(0));
if (sb != null)
{
for (int i = 0; i < sb.Children.Count; i++)
{
if (i == 5)
nextDelay = newDelay;
else
nextDelay += newDelay;
DoubleAnimationUsingKeyFrames timeline = sb.Children[i] as DoubleAnimationUsingKeyFrames;
if (timeline != null)
{
timeline.SetValue(DoubleAnimationUsingKeyFrames.BeginTimeProperty, nextDelay.TimeSpan);
}
}
}
}
#endregion
#region IDisposable Implementation
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~FluidProgressBar()
{
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
this.SizeChanged -= OnSizeChanged;
this.Loaded -= OnLoaded;
this.IsVisibleChanged -= OnIsVisibleChanged;
}
}
#endregion
}
FluidProgressBar Properties
| Dependency Property | Type | Description | Default Value |
Delay | Duration | Gets or sets the duration between the animations of each Dot. | 100 milliseconds |
DotWidth | Double | Gets or sets the Width of each Dot. | 4.0 |
DotHeight | Double | Gets or sets the Height of each Dot. | 4.0 |
DotRadiusX | Double | Gets or sets the x-axis radius of the ellipse that is used to round the corners of the Dot. |
0.0 |
DotRadiusY | Double | Gets or sets the y-axis radius of the ellipse that is used to round the corners of the Dot. |
0.0 |
DurationA | Duration | Gets or sets the duration between KeyFrame0 and KeyFrameA. |
0.5 seconds |
DurationB | Duration | Gets or sets the duration between KeyFrameA and KeyFrameB. |
1.5 seconds |
DurationC | Duration | Gets or sets the duration between KeyFrameB and KeyFrameC. |
0.5 seconds |
KeyFrameA | Double | Gets or sets the fraction of the total width of the FluidProgressBar by which
the Dot must be translated in the X-axis from the KeyFrame0 position. | 0.33 |
KeyFrameB | Double | Gets or sets the fraction of the total width of the FluidProgressBar by which
the Dot must be translated in the X-axis from the KeyFrameA position. | 0.63 |
Oscillate | Boolean | Gets or sets whether the animation of the Dot from KeyFrame0
to KeyFrameC should be automatically played in reverse after it completes a forward iteration. | False |
ReverseDuration | Duration | Gets or sets the total duration of the animation timeline for each Dot
when the Oscillate property is True. | 2.9 seconds |
TotalDuration | Duration | Gets or sets the total duration of the animation timeline for each Dot
when the Oscillate property is False. | 4.4 seconds |
History
- December 21, 2011: WPFSpark v1.0 released.