
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 Rectangle
s (a.k.a. Dot
s) 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 Dot
s is animated using a DoubleAnimationUsingKeyFramesAnimation
. It consists of four KeyFrame
s:
- 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 Dot
s
available in its Resources. Once the Storyboard
is obtained, it gets all the KeyFrame
s involved in animating the Dot
s
and adds them to a dictionary. These KeyFrame
s 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.