WPFSpark:n之6:FluidProgressBar





5.00/5 (2投票s)
WPF 中一款 Windows Phone 风格的非确定性 ProgressBar。
引言
这是 WPFSpark 系列的第六篇文章。到目前为止,我已经介绍了 WPFSpark 中的五个控件 - SprocketControl
、ToggleSwitch
、FluidWrapPanel
、SparkWindow
和 FluidPivotPanel
。
可以从此处访问 WPFSpark 系列的先前文章
- WPFSpark:n之1:SprocketControl
- WPFSpark:n之2:ToggleSwitch
- WPFSpark:n之3:FluidWrapPanel
- WPFSpark:n之4:SparkWindow
- 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。从
KeyFrame0
到KeyFrameA
的动画是具有ExponentionalEaseOut
缓动模式的线性动画。Dot
在此 KeyFrame 的位置定义为FluidProgressBar
总宽度的分数。它通常的值范围是0
到1
,默认值为0.33
。 - KeyFrameB - 第一个 KeyFrame。从
KeyFrameA
到KeyFrameB
的动画是没有任何缓动的线性动画。Dot
在此 KeyFrame 的位置定义为FluidProgressBar
总宽度的分数。它通常的值范围是0
到1
,默认值为0.63
。 - KeyFrameC - 第一个 KeyFrame。从
KeyFrameB
到KeyFrameC
的动画是具有ExponentionalEaseIn
缓动模式的线性动画。Dot
在此 KeyFrame 的位置在其父 Grid 右侧 10 像素处。
KeyFrame0
和 KeyFrameA
之间的时间段由 DurationA
属性定义,KeyFrameA
和 KeyFrameB
之间的时间段由 DurationB
属性定义,KeyFrameB
和 KeyFrameC
之间的时间段由 DurationA
属性定义。
由于它们看起来像是五点依次移动的直线,因此每个 Dot
的动画之间存在默认 100 毫秒的延迟。通过设置 FluidProgressBar
的 Delay
属性,可以配置延迟持续时间。
首次创建 FluidProgressBar
时,它会解析其 **Resources** 中 Dot
的动画所在的 Storyboard
。一旦获得 Storyboard
,它就会获取动画 Dot
所涉及的所有 KeyFrame
,并将它们添加到字典中。每当 FluidProgressBar
的属性发生更改时,就会操纵这些 KeyFrame
。例如,每当 FluidProgressBar
首次加载或调整大小时,它都会通过调用 UpdateKeyFrames()
方法来重新计算 KeyFrameA
和 KeyFrameB
的位置。
这是 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 | 双精度浮点型 | 获取或设置每个 Dot 的 Width 。 | 4.0 |
DotHeight | 双精度浮点型 | 获取或设置每个 Dot 的 Height 。 | 4.0 |
DotRadiusX | 双精度浮点型 | 获取或设置用于使 Dot 的角圆角的椭圆的 x 轴半径。 |
0.0 |
DotRadiusY | 双精度浮点型 | 获取或设置用于使 Dot 的角圆角的椭圆的 y 轴半径。 |
0.0 |
DurationA | 持续时间 | 获取或设置 KeyFrame0 和 KeyFrameA 之间的时间段。 |
0.5 秒 |
DurationB | 持续时间 | 获取或设置 KeyFrameA 和 KeyFrameB 之间的时间段。 |
1.5 秒 |
DurationC | 持续时间 | 获取或设置 KeyFrameB 和 KeyFrameC 之间的时间段。 |
0.5 秒 |
KeyFrameA | 双精度浮点型 | 获取或设置 Dot 在 X 轴上从 KeyFrame0 位置平移的 FluidProgressBar 总宽度的分数。 | 0.33 |
KeyFrameB | 双精度浮点型 | 获取或设置 Dot 从 KeyFrameA 位置平移的 FluidProgressBar 总宽度的分数。 | 0.63 |
Oscillate | 布尔值 | 获取或设置当 Dot 从 KeyFrame0 到 KeyFrameC 的动画在完成一次正向迭代后是否自动反向播放。 | 假 |
ReverseDuration | 持续时间 | 当 Oscillate 属性为 True 时,获取或设置每个 Dot 动画时间线的总持续时间。 | 2.9 秒 |
TotalDuration | 持续时间 | 当 Oscillate 属性为 False 时,获取或设置每个 Dot 动画时间线的总持续时间。 | 4.4 秒 |
历史
- 2011 年 12 月 21 日:WPFSpark v1.0 发布。