WPFSpark: 6 of n: FluidProgressBar





5.00/5 (2 votes)
A Windows Phone style indeterminate ProgressBar in WPF.
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
toKeyFrameA
is a Linear animation with anExponentionalEaseOut
Easing Mode. The location of theDot
at this KeyFrame is defined as a fraction of the total width of theFluidProgressBar
. It usually has values ranging from0
to1
with a default value of0.33
. - KeyFrameB - The first KeyFrame. The animation from
KeyFrameA
toKeyFrameB
is a Linear animation with no Easing. The location of theDot
at this KeyFrame is defined as a fraction of the total width of theFluidProgressBar
. It usually has values ranging from0
to1
with a default value of0.63
. - KeyFrameC - The first KeyFrame. The animation from
KeyFrameB
toKeyFrameC
is a Linear animation with anExponentionalEaseIn
Easing Mode. The location of theDot
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
:
/// <summary>
/// Interaction logic for FluidProgressBar.xaml
/// </summary>
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;
//KeyTime keyA = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0));
//KeyTime keyB = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.5));
//KeyTime keyC = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(2.0));
//KeyTime keyD = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(2.5));
Storyboard sb;
bool isStoryboardRunning;
#endregion
#region Dependency Properties
...
#endregion
#region Construction / Initialization
/// <summary>
/// Ctor
/// </summary>
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
/// <summary>
/// Handles the Loaded event
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">EventArgs</param>
void OnLoaded(object sender, System.Windows.RoutedEventArgs e)
{
// Update the key frames
UpdateKeyFrames();
// Start the animation
StartFluidAnimation();
}
/// <summary>
/// Handles the SizeChanged event
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">EventArgs</param>
void OnSizeChanged(object sender, System.Windows.SizeChangedEventArgs e)
{
// Restart the animation
RestartStoryboardAnimation();
}
/// <summary>
/// Handles the IsVisibleChanged event
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">EventArgs</param>
void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (this.Visibility == Visibility.Visible)
{
UpdateKeyFrames();
StartFluidAnimation();
}
else
{
StopFluidAnimation();
}
}
#endregion
#region Helpers
/// <summary>
/// Starts the animation
/// </summary>
private void StartFluidAnimation()
{
if ((sb != null) && (!isStoryboardRunning))
{
sb.Begin();
isStoryboardRunning = true;
}
}
/// <summary>
/// Stops the animation
/// </summary>
private void StopFluidAnimation()
{
if ((sb != null) && (isStoryboardRunning))
{
// Move the timeline to the end and stop the animation
sb.SeekAlignedToLastTick(TimeSpan.FromSeconds(0));
sb.Stop();
isStoryboardRunning = false;
}
}
/// <summary>
/// Stops the animation, updates the keyframes and starts the animation
/// </summary>
private void RestartStoryboardAnimation()
{
StopFluidAnimation();
UpdateKeyFrames();
StartFluidAnimation();
}
/// <summary>
/// Obtains the keyframes for each animation in the storyboard so that
/// they can be updated when required.
/// </summary>
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"));
}
}
}
}
/// <summary>
/// Gets the keyframes in the given animation and stores them in a map
/// </summary>
/// <param name="dakeys">Animation containg keyframes</param>
/// <param name="isOpacityAnim">Flag to indicate whether
/// the animation targets the opacity or the translate transform</param>
private void ProcessDoubleAnimationWithKeys(DoubleAnimationUsingKeyFrames dakeys, bool isOpacityAnim = false)
{
// Get all the keyframes in the instance.
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>() };
}
// Update the keyframe time and add it to the map
targetMap[i].KeyFrameTime = frame.KeyTime;
targetMap[i].KeyFrames.Add(frame);
}
}
/// <summary>
/// Update the key value of each keyframe based on the current width of the FluidProgressBar
/// </summary>
private void UpdateKeyFrames()
{
// Get the current width of the FluidProgressBar
double width = this.ActualWidth;
// Update the values only if the current width is greater than Zero and is visible
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;
// Update the keyframes stored in the map
UpdateKeyFrame(0, Point0);
UpdateKeyFrame(1, PointA);
UpdateKeyFrame(2, PointB);
UpdateKeyFrame(3, PointC);
}
}
/// <summary>
/// Update the key value of the keyframes stored in the map
/// </summary>
/// <param name="key">Key of the dictionary</param>
/// <param name="newValue">New value
/// to be given to the key value of the keyframes</param>
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);
}
}
}
}
/// <summary>
/// Updates the duration of each of the keyframes stored in the map
/// </summary>
/// <param name="key">Key of the dictionary</param>
/// <param name="newValue">New value to be given
/// to the duration value of the keyframes</param>
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;
}
// Update the opacity animation duration based on the complete duration
// of the animation
UpdateOpacityKeyTime(1, DurationA + DurationB + DurationC);
}
/// <summary>
/// Updates the duration of each of the keyframes stored in the map
/// </summary>
/// <param name="key">Key of the dictionary</param>
/// <param name="newDuration">New value to be given
/// to the duration value of the keyframes</param>
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);
}
}
}
}
/// <summary>
/// Updates the duration of the second keyframe of all the opacity animations
/// </summary>
/// <param name="key">Key of the dictionary</param>
/// <param name="newDuration">New value to be given
/// to the duration value of the keyframes</param>
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);
}
}
}
}
/// <summary>
/// Updates the delay between consecutive timelines
/// </summary>
/// <param name="newDelay">Delay duration</param>
private void UpdateTimelineDelay(Duration newDelay)
{
Duration nextDelay = new Duration(TimeSpan.FromSeconds(0));
if (sb != null)
{
for (int i = 0; i < sb.Children.Count; i++)
{
// The first five animations are for translation
// The next five animations are for opacity
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
/// <summary>
/// Releases all resources used by an instance of the FluidProgressBar class.
/// </summary>
/// <remarks>
/// This method calls the virtual Dispose(bool) method, passing in 'true', and then suppresses
/// finalization of the instance.
/// </remarks>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged resources before an instance of the FluidProgressBar
/// class is reclaimed by garbage collection.
/// </summary>
/// <remarks>
/// NOTE: Leave out the finalizer altogether if this class doesn't own unmanaged resources itself,
/// but leave the other methods exactly as they are.
/// This method releases unmanaged resources by calling the virtual Dispose(bool), passing in 'false'.
/// </remarks>
~FluidProgressBar()
{
Dispose(false);
}
/// <summary>
/// Releases the unmanaged resources used by an instance
/// of the FluidProgressBar class and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">'true' to release both managed
/// and unmanaged resources; 'false' to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// free managed resources here
this.SizeChanged -= OnSizeChanged;
this.Loaded -= OnLoaded;
this.IsVisibleChanged -= OnIsVisibleChanged;
}
// free native resources if there are any.
}
#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.