65.9K
CodeProject 正在变化。 阅读更多。
Home

WPFSpark:n之6:FluidProgressBar

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2011年12月21日

Ms-PL

4分钟阅读

viewsIcon

46257

downloadIcon

1562

WPF 中一款 Windows Phone 风格的非确定性 ProgressBar。

引言

这是 WPFSpark 系列的第六篇文章。到目前为止,我已经介绍了 WPFSpark 中的五个控件 - SprocketControlToggleSwitchFluidWrapPanelSparkWindowFluidPivotPanel

可以从此处访问 WPFSpark 系列的先前文章

  1. WPFSpark:n之1:SprocketControl
  2. WPFSpark:n之2:ToggleSwitch
  3. WPFSpark:n之3:FluidWrapPanel
  4. WPFSpark:n之4:SparkWindow
  5. WPFSpark:n之5:FluidPivotPanel

在本文中,我将详细介绍此库中的第六个控件 - FluidProgressBar 控件。

灵感

FluidProgressBar 的灵感来源于 Windows Phone 7+ 的非确定性 ProgressBar。Jeff Wilcox 的高性能 ProgressBar 在理解 Windows Phone 7+ 当前 ProgressBar 的细节方面提供了很大帮助。

FluidProgressBar 揭秘

FluidProgressBar 不继承自 ProgressBar。相反,它只是一个用于描绘 ProgressBar 非确定性状态的 UserControl。它还提供了由用户定义的、基于比例的动画的灵活性。

FluidProgressBar 主要由五个 Rectangle(也称为 Dot)组成,它们的 X 方向平移被动画化,因此当它们从左侧移入时,它们似乎会汇聚在中心,然后随着它们向右侧移动而发散。

每个 Dot 都使用 DoubleAnimationUsingKeyFramesAnimation 进行动画处理。它包含四个 KeyFrame

  • KeyFrame0 - 第零个 KeyFrame 或起始 KeyFrame。Dot 在此 KeyFrame 的位置在其父 Grid 左侧 10 像素处。
  • KeyFrameA - 第一个 KeyFrame。从 KeyFrame0KeyFrameA 的动画是具有 ExponentionalEaseOut 缓动模式的线性动画。Dot 在此 KeyFrame 的位置定义为 FluidProgressBar 总宽度的分数。它通常的值范围是 01,默认值为 0.33
  • KeyFrameB - 第一个 KeyFrame。从 KeyFrameAKeyFrameB 的动画是没有任何缓动的线性动画。Dot 在此 KeyFrame 的位置定义为 FluidProgressBar 总宽度的分数。它通常的值范围是 01,默认值为 0.63
  • KeyFrameC - 第一个 KeyFrame。从 KeyFrameBKeyFrameC 的动画是具有 ExponentionalEaseIn 缓动模式的线性动画。Dot 在此 KeyFrame 的位置在其父 Grid 右侧 10 像素处。

KeyFrame0KeyFrameA 之间的时间段由 DurationA 属性定义,KeyFrameAKeyFrameB 之间的时间段由 DurationB 属性定义,KeyFrameBKeyFrameC 之间的时间段由 DurationA 属性定义。

由于它们看起来像是五点依次移动的直线,因此每个 Dot 的动画之间存在默认 100 毫秒的延迟。通过设置 FluidProgressBarDelay 属性,可以配置延迟持续时间。

首次创建 FluidProgressBar 时,它会解析其 **Resources** 中 Dot 的动画所在的 Storyboard。一旦获得 Storyboard,它就会获取动画 Dot 所涉及的所有 KeyFrame,并将它们添加到字典中。每当 FluidProgressBar 的属性发生更改时,就会操纵这些 KeyFrame。例如,每当 FluidProgressBar 首次加载或调整大小时,它都会通过调用 UpdateKeyFrames() 方法来重新计算 KeyFrameAKeyFrameB 的位置。

这是 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 属性

依赖属性类型描述默认值
延迟持续时间获取或设置每个 Dot 动画之间的时间段。100 毫秒
DotWidth双精度浮点型获取或设置每个 DotWidth4.0
DotHeight双精度浮点型获取或设置每个 DotHeight4.0
DotRadiusX双精度浮点型获取或设置用于使 Dot 的角圆角的椭圆的 x 轴半径。 0.0
DotRadiusY双精度浮点型获取或设置用于使 Dot 的角圆角的椭圆的 y 轴半径。 0.0
DurationA持续时间获取或设置 KeyFrame0KeyFrameA 之间的时间段。 0.5 秒
DurationB持续时间获取或设置 KeyFrameAKeyFrameB 之间的时间段。 1.5 秒
DurationC持续时间获取或设置 KeyFrameBKeyFrameC 之间的时间段。 0.5 秒
KeyFrameA双精度浮点型获取或设置 Dot 在 X 轴上从 KeyFrame0 位置平移的 FluidProgressBar 总宽度的分数。0.33
KeyFrameB双精度浮点型获取或设置 DotKeyFrameA 位置平移的 FluidProgressBar 总宽度的分数。0.63
Oscillate布尔值获取或设置当 DotKeyFrame0KeyFrameC 的动画在完成一次正向迭代后是否自动反向播放。
ReverseDuration持续时间Oscillate 属性为 True 时,获取或设置每个 Dot 动画时间线的总持续时间。2.9 秒
TotalDuration持续时间Oscillate 属性为 False 时,获取或设置每个 Dot 动画时间线的总持续时间。4.4 秒

历史

  • 2011 年 12 月 21 日:WPFSpark v1.0 发布。
© . All rights reserved.