Click here to Skip to main content
Click here to Skip to main content
Go to top

WPFSpark: 6 of n: FluidProgressBar

, 19 Jan 2012
Rate this:
Please Sign up or sign in to vote.
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:

  1. WPFSpark: 1 of n: SprocketControl
  2. WPFSpark: 2 of n: ToggleSwitch
  3. WPFSpark: 3 of n: FluidWrapPanel
  4. WPFSpark: 4 of n: SparkWindow
  5. 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:

/// <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 PropertyTypeDescriptionDefault Value
DelayDurationGets or sets the duration between the animations of each Dot.100 milliseconds
DotWidthDoubleGets or sets the Width of each Dot.4.0
DotHeightDoubleGets or sets the Height of each Dot.4.0
DotRadiusXDoubleGets or sets the x-axis radius of the ellipse that is used to round the corners of the Dot. 0.0
DotRadiusYDoubleGets or sets the y-axis radius of the ellipse that is used to round the corners of the Dot. 0.0
DurationADurationGets or sets the duration between KeyFrame0 and KeyFrameA. 0.5 seconds
DurationBDurationGets or sets the duration between KeyFrameA and KeyFrameB. 1.5 seconds
DurationCDurationGets or sets the duration between KeyFrameB and KeyFrameC. 0.5 seconds
KeyFrameADoubleGets 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
KeyFrameBDoubleGets 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
OscillateBooleanGets 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
ReverseDurationDurationGets or sets the total duration of the animation timeline for each Dot when the Oscillate property is True.2.9 seconds
TotalDurationDurationGets 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.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Ratish Philip
Software Developer NA
India India
Ratish Philip is a software developer with 8 years of experience. He loves programming in C#, WPF & Silverlight.
 
He is currently exploring the depths of Windows 8 programming.
 
Creating enriched user experiences is what appeals to him the most.
 
Occasionally expresses his creativity through pencil sketching too!
 
Ratish's personal blog: wpfspark.wordpress.com

Comments and Discussions

 
QuestionCustomizing PinmemberMember 454471928-Jan-13 1:21 
AnswerRe: Customizing PinmemberRatish Philip12-Feb-14 14:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140921.1 | Last Updated 20 Jan 2012
Article Copyright 2011 by Ratish Philip
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid