相對於WPF/Silverlight,UWP的動畫系統可以說有大幅提高,不過本文無意深入討論這些動畫API,本文將介紹使用Shape做一些進度、等待方面的動畫,除此之外也會介紹一些相關技巧。 1. 使用StrokeDashOffset做等待提示動畫 圓形的等待提示動畫十分容易做,只要讓它旋轉就可以了 ...
相對於WPF/Silverlight,UWP的動畫系統可以說有大幅提高,不過本文無意深入討論這些動畫API,本文將介紹使用Shape做一些進度、等待方面的動畫,除此之外也會介紹一些相關技巧。
1. 使用StrokeDashOffset做等待提示動畫
圓形的等待提示動畫十分容易做,只要讓它旋轉就可以了:
但是圓形以外的形狀就不容易做了,例如三角形,總不能讓它單純地旋轉吧:
要解決這個問題可以使用StrokeDashOffset。StrokeDashOffset用於控制虛線邊框的第一個短線相對於Shape開始點的位移,使用動畫控制這個數值可以做出邊框滾動的效果:
<Page.Resources>
<Storyboard x:Name="ProgressStoryboard">
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetProperty="(Shape.StrokeDashOffset)"
Storyboard.TargetName="triangle">
<EasingDoubleKeyFrame KeyTime="0:1:0"
Value="-500" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Page.Resources>
<Grid Background="#FFCCCCCC">
<Grid Height="100"
HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock Text="L"
FontSize="55"
Margin="0,0,5,4" />
<local:Triangle x:Name="triangle"
Height="40"
Width="40"
StrokeThickness="2"
Stroke="RoyalBlue"
StrokeDashArray="4.045 4.045"
StrokeDashOffset="0.05"
StrokeDashCap="Round" />
<TextBlock Text="ading..."
FontSize="55"
Margin="5,0,0,4" />
</StackPanel>
</Grid>
</Grid>
需要註意的是Shape的邊長要正好能被StrokeDashArray中短線和缺口的和整除,即 滿足邊長 / StrokeThickness % Sum( StrokeDashArray ) = 0
,這是因為在StrokeDashOffset=0的地方會截斷短線,如下圖所示:
另外註意的是邊長的計算,如Rectangle,邊長並不是(Height + Width) * 2
,而是(Height - StrokeThickness) * 2 + (Width- StrokeThickness) * 2
,如下圖所示,邊長應該從邊框正中間開始計算:
有一些Shape的邊長計算還會受到Stretch影響,如上一篇中自定義的Triangle:
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Grid Height="50"
Width="50">
<local:Triangle Stretch="Fill"
StrokeThickness="5"
Stroke="RoyalBlue" />
</Grid>
<Grid Height="50"
Width="50"
Margin="10,0,0,0">
<local:Triangle Stretch="None"
StrokeThickness="5"
Stroke="RoyalBlue" />
</Grid>
</StackPanel>
2. 使用StrokeDashArray做進度提示動畫
StrokeDashArray用於將Shape的邊框變成虛線,StrokeDashArray的值是一個double類型的有序集合,裡面的數值指定虛線中每一段以StrokeThickness為單位的長度。用StrokeDashArray做進度提示的基本做法就是將進度Progress通過Converter轉換為分成兩段的StrokeDashArray,第一段為實線,表示當前進度,第二段為空白。假設一個Shape的邊長是100,當前進度為50,則將StrokeDashArray設置成{50,double.MaxValue}兩段。
做成動畫如下圖所示:
<Page.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontSize"
Value="12" />
</Style>
<local:ProgressToStrokeDashArrayConverter x:Key="ProgressToStrokeDashArrayConverter"
TargetPath="{Binding ElementName=Triangle}" />
<local:ProgressToStrokeDashArrayConverter2 x:Key="ProgressToStrokeDashArrayConverter2"
TargetPath="{Binding ElementName=Triangle}" />
<toolkit:StringFormatConverter x:Key="StringFormatConverter" />
<local:ProgressWrapper x:Name="ProgressWrapper" />
<Storyboard x:Name="Storyboard1">
<DoubleAnimation Duration="0:0:5"
To="100"
Storyboard.TargetProperty="Progress"
Storyboard.TargetName="ProgressWrapper"
EnableDependentAnimation="True" />
</Storyboard>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Viewbox Height="150">
<StackPanel Orientation="Horizontal">
<Grid>
<local:Triangle Height="40"
Width="40"
StrokeThickness="2"
Stroke="DarkGray" />
<local:Triangle x:Name="Triangle"
Height="40"
Width="40"
StrokeThickness="2"
Stroke="RoyalBlue"
StrokeDashArray="{Binding Progress,Source={StaticResource ProgressWrapper},Converter={StaticResource ProgressToStrokeDashArrayConverter}}" />
<TextBlock Text="{Binding Progress,Source={StaticResource ProgressWrapper},Converter={StaticResource StringFormatConverter},ConverterParameter='{}{0:0}'}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,15,0,0" />
</Grid>
<Grid Margin="20,0,0,0">
<local:Triangle Height="40"
Width="40"
StrokeThickness="2"
Stroke="DarkGray" />
<local:Triangle x:Name="Triangle2"
Height="40"
Width="40"
StrokeThickness="2"
Stroke="RoyalBlue"
StrokeDashArray="{Binding Progress,Source={StaticResource ProgressWrapper},Converter={StaticResource ProgressToStrokeDashArrayConverter2}}" />
<TextBlock Text="{Binding Progress,Source={StaticResource ProgressWrapper},Converter={StaticResource StringFormatConverter},ConverterParameter='{}{0:0}'}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,15,0,0" />
</Grid>
</StackPanel>
</Viewbox>
</Grid>
其中ProgressToStrokeDashArrayConverter和ProgressToStrokeDashArrayConverter2的代碼如下:
public class ProgressToStrokeDashArrayConverter : DependencyObject, IValueConverter
{
/// <summary>
/// 獲取或設置TargetPath的值
/// </summary>
public Path TargetPath
{
get { return (Path)GetValue(TargetPathProperty); }
set { SetValue(TargetPathProperty, value); }
}
/// <summary>
/// 標識 TargetPath 依賴屬性。
/// </summary>
public static readonly DependencyProperty TargetPathProperty =
DependencyProperty.Register("TargetPath", typeof(Path), typeof(ProgressToStrokeDashArrayConverter), new PropertyMetadata(null));
public virtual object Convert(object value, Type targetType, object parameter, string language)
{
if (value is double == false)
return null;
var progress = (double)value;
if (TargetPath == null)
return null;
var totalLength = GetTotalLength();
var firstSection = progress * totalLength / 100 / TargetPath.StrokeThickness;
if (progress == 100)
firstSection = Math.Ceiling(firstSection);
var result = new DoubleCollection { firstSection, double.MaxValue };
return result;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
protected double GetTotalLength()
{
var geometry = TargetPath.Data as PathGeometry;
if (geometry == null)
return 0;
if (geometry.Figures.Any() == false)
return 0;
var figure = geometry.Figures.FirstOrDefault();
if (figure == null)
return 0;
var totalLength = 0d;
var point = figure.StartPoint;
foreach (var item in figure.Segments)
{
var segment = item as LineSegment;
if (segment == null)
return 0;
totalLength += Math.Sqrt(Math.Pow(point.X - segment.Point.X, 2) + Math.Pow(point.Y - segment.Point.Y, 2));
point = segment.Point;
}
totalLength += Math.Sqrt(Math.Pow(point.X - figure.StartPoint.X, 2) + Math.Pow(point.Y - figure.StartPoint.Y, 2));
return totalLength;
}
}
public class ProgressToStrokeDashArrayConverter2 : ProgressToStrokeDashArrayConverter
{
public override object Convert(object value, Type targetType, object parameter, string language)
{
if (value is double == false)
return null;
var progress = (double)value;
if (TargetPath == null)
return null;
var totalLength = GetTotalLength();
totalLength = totalLength / TargetPath.StrokeThickness;
var thirdSection = progress * totalLength / 100;
if (progress == 100)
thirdSection = Math.Ceiling(thirdSection);
var secondSection = (totalLength - thirdSection) / 2;
var result = new DoubleCollection { 0, secondSection, thirdSection, double.MaxValue };
return result;
}
}
由於代碼只是用於演示,protected double GetTotalLength()
寫得比較將就。可以看到這兩個Converter繼承自DependencyObject,這是因為這裡需要通過綁定為TargetPath賦值。
這裡還有另一個類ProgressWrapper:
public class ProgressWrapper : DependencyObject
{
/// <summary>
/// 獲取或設置Progress的值
/// </summary>
public double Progress
{
get { return (double)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
}
/// <summary>
/// 標識 Progress 依賴屬性。
/// </summary>
public static readonly DependencyProperty ProgressProperty =
DependencyProperty.Register("Progress", typeof(double), typeof(ProgressWrapper), new PropertyMetadata(0d));
}
因為這裡沒有可供Storyboard操作的double屬性,所以用這個類充當Storyboard和StrokeDashArray的橋梁。UWPCommunityToolkit中也有一個差不多用法的類BindableValueHolder,這個類通用性比較強,可以參考它的用法。
3. 使用Behavior改進進度提示動畫代碼
只是做個動畫而已,又是Converter,又是Wrapper,又是Binding,看起來十分複雜,如果Shape上面有Progress屬性就方便多了。這時候首先會考慮附加屬性,在XAML用法如下:
<UserControl.Resources>
<Storyboard x:Name="Storyboard1">
<DoubleAnimation Duration="0:0:5"
To="100"
Storyboard.TargetProperty="(local:PathExtention.Progress)"
Storyboard.TargetName="Triangle" />
</Storyboard>
</UserControl.Resources>
<Grid x:Name="LayoutRoot"
Background="White">
<local:Triangle x:Name="Triangle"
Height="40"
local:PathExtention.Progress="0"
Width="40"
StrokeThickness="2"
Stroke="RoyalBlue" >
</local:Triangle>
</Grid>
但其實這是行不通的,XAML有一個存在了很久的限制:However, an existing limitation of the Windows Runtime XAML implementation is that you cannot animate a custom attached property.。這個限制決定了XAML不能對自定義附加屬性做動畫。不過,這個限制只限制了不能對自定義附加屬性本身做動畫,但對附加屬性中的類的屬性則可以,例如以下這種寫法應該是行得通的:
<UserControl.Resources>
<Storyboard x:Name="Storyboard1">
<DoubleAnimation Duration="0:0:5"
To="100"
Storyboard.TargetProperty="(local:PathExtention.Progress)"
Storyboard.TargetName="TrianglePathExtention" />
</Storyboard>
</UserControl.Resources>
<Grid x:Name="LayoutRoot"
Background="White">
<local:Triangle x:Name="Triangle"
Height="40"
Width="40"
StrokeThickness="2"
Stroke="RoyalBlue" >
<local:PathHelper>
<local:PathExtention x:Name="TrianglePathExtention"
Progress="0" />
</local:PathHelper>
</local:Triangle>
</Grid>
更優雅的寫法是利用XamlBehaviors,這篇文章很好地解釋了XamlBehaviors的作用:
XAML Behaviors非常重要,因為它們提供了一種方法,讓開發人員能夠以一種簡潔、可重覆的方式輕鬆地向UI對象添加功能。他們無需創建控制項的子類或重覆編寫邏輯代碼,只要簡單地增加一個XAML代碼片段。
要使用Behavior改進現有代碼,只需實現一個PathProgressBehavior:
public class PathProgressBehavior : Behavior<UIElement>
{
protected override void OnAttached()
{
base.OnAttached();
UpdateStrokeDashArray();
}
/// <summary>
/// 獲取或設置Progress的值
/// </summary>
public double Progress
{
get { return (double)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
}
/*Progress DependencyProperty*/
protected virtual void OnProgressChanged(double oldValue, double newValue)
{
UpdateStrokeDashArray();
}
protected virtual double GetTotalLength(Path path)
{
/*some code*/
}
private void UpdateStrokeDashArray()
{
var target = AssociatedObject as Path;
if (target == null)
return;
double progress = Progress;
//if (target.ActualHeight == 0 || target.ActualWidth == 0)
// return;
if (target.StrokeThickness == 0)
return;
var totalLength = GetTotalLength(target);
var firstSection = progress * totalLength / 100 / target.StrokeThickness;
if (progress == 100)
firstSection = Math.Ceiling(firstSection);
var result = new DoubleCollection { firstSection, double.MaxValue };
target.StrokeDashArray = result;
}
}
XAML中如下使用:
<UserControl.Resources>
<Storyboard x:Name="Storyboard1">
<DoubleAnimation Duration="0:0:5"
To="100"
Storyboard.TargetProperty="Progress"
Storyboard.TargetName="PathProgressBehavior"
EnableDependentAnimation="True"/>
</Storyboard>
</UserControl.Resources>
<Grid x:Name="LayoutRoot"
Background="White">
<local:Triangle x:Name="Triangle"
Height="40"
local:PathExtention.Progress="0"
Width="40"
StrokeThickness="2"
Stroke="RoyalBlue" >
<interactivity:Interaction.Behaviors>
<local:PathProgressBehavior x:Name="PathProgressBehavior" />
</interactivity:Interaction.Behaviors>
</local:Triangle>
</Grid>
這樣看起來就清爽多了。
4. 模仿背景填充動畫
先看看效果:
其實這篇文章里並不會討論填充動畫,不過首先聲明做填充動畫會更方便快捷,這一段只是深入學習過程中的產物,實用價值不高。
上圖三角形的填充的效果只需要疊加兩個同樣大小的Shape,前面那個設置Stretch="Uniform"
,再通過DoubleAnimation改變它的高度就可以了。文字也是相同的原理,疊加兩個相同的TextBlock,將前面那個放在一個無邊框的ScrollViewer里再去改變ScrollViewer的高度。
<Page.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontSize"
Value="12" />
</Style>
<local:ProgressToHeightConverter x:Key="ProgressToHeightConverter"
TargetContentControl="{Binding ElementName=ContentControl}" />
<local:ReverseProgressToHeightConverter x:Key="ReverseProgressToHeightConverter"
TargetContentControl="{Binding ElementName=ContentControl2}" />
<toolkit:StringFormatConverter x:Key="StringFormatConverter" />
<local:ProgressWrapper x:Name="ProgressWrapper" />
<Storyboard x:Name="Storyboard1">
<DoubleAnimation Duration="0:0:5"
To="100"
Storyboard.TargetProperty="Progress"
Storyboard.TargetName="ProgressWrapper"
EnableDependentAnimation="True" />
</Storyboard>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<local:Triangle Height="40"
Width="40"
StrokeThickness="2"
Fill="LightGray" />
<local:Triangle Height="40"
Width="40"
Stretch="Fill"
StrokeThickness="2"
Stroke="RoyalBlue" />
<ContentControl x:Name="ContentControl"
VerticalAlignment="Bottom"
HorizontalAlignment="Center"
Height="{Binding Progress,Source={StaticResource ProgressWrapper},Converter={StaticResource ProgressToHeightConverter}}">
<local:Triangle x:Name="Triangle3"
Height="40"
Width="40"
StrokeThickness="2"
Fill="RoyalBlue"
Stretch="Uniform"
VerticalAlignment="Bottom" />
</ContentControl>
<TextBlock Text="{Binding Progress,Source={StaticResource ProgressWrapper},Converter={StaticResource StringFormatConverter},ConverterParameter='{}{0:0}'}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,12,0,0"
Foreground="White" />
<ContentControl x:Name="ContentControl2"
Height="{Binding Progress,Source={StaticResource ProgressWrapper},Converter={StaticResource ReverseProgressToHeightConverter}}"
VerticalAlignment="Top"
HorizontalAlignment="Center">
<ScrollViewer BorderThickness="0"
Padding="0,0,0,0"
VerticalScrollBarVisibility="Disabled"
HorizontalScrollBarVisibility="Disabled"
VerticalAlignment="Top"
Height="40">
<Grid Height="40">
<TextBlock Text="{Binding Progress,Source={StaticResource ProgressWrapper},Converter={StaticResource StringFormatConverter},ConverterParameter='{}{0:0}'}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0,12,0,0" />
</Grid>
</ScrollViewer>
</ContentControl>
</Grid>
</Grid>
ProgressToHeightConverter和ReverseProgressToHeightConverter的代碼如下:
public class ProgressToHeightConverter : DependencyObject, IValueConverter
{
/// <summary>
/// 獲取或設置TargetContentControl的值
/// </summary>
public ContentControl TargetContentControl
{
get { return (ContentControl)GetValue(TargetContentControlProperty); }
set { SetValue(TargetContentControlProperty, value); }
}
/// <summary>
/// 標識 TargetContentControl 依賴屬性。
/// </summary>
public static readonly DependencyProperty TargetContentControlProperty =
DependencyProperty.Register("TargetContentControl", typeof(ContentControl), typeof(ProgressToHeightConverter), new PropertyMetadata(null));
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is double == false)
return 0d;
var progress = (double)value;
if (TargetContentControl == null)
return 0d;
var element = TargetContentControl.Content as FrameworkElement;
if (element == null)
return 0d;
return element.Height * progress / 100;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
public class ReverseProgressToHeightConverter : DependencyObject, IValueConverter
{
/// <summary>
/// 獲取或設置TargetContentControl的值
/// </summary>
public ContentControl TargetContentControl
{
get { return (ContentControl)GetValue(TargetContentControlProperty); }
set { SetValue(TargetContentControlProperty, value); }
}
/// <summary>
/// 標識 TargetContentControl 依賴屬性。
/// </summary>
public static readonly DependencyProperty TargetContentControlProperty =
DependencyProperty.Register("TargetContentControl", typeof(ContentControl), typeof(ReverseProgressToHeightConverter), new PropertyMetadata(null));
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is double == false)
return double.NaN;
var progress = (double)value;
if (TargetContentControl == null)
return double.NaN;
var element = TargetContentControl.Content as FrameworkElement;
if (element == null)
return double.NaN;
return element.Height * (100 - progress) / 100;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
再提醒一次,實際上老老實實做填充動畫好像更方便些。
5. 將動畫應用到Button的ControlTemplate
同樣的技術,配合ControlTemplate可以製作很有趣的按鈕:
PointerEntered時,按鈕的邊框從進入點向反方向延伸。PointerExited時,邊框從反方向向移出點消退。要做到這點需要在PointerEntered時改變邊框的方向,使用了ChangeAngleToEnterPointerBehavior:
public class ChangeAngleToEnterPointerBehavior : Behavior<Ellipse>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PointerEntered += OnAssociatedObjectPointerEntered;
AssociatedObject.PointerExited += OnAssociatedObjectPointerExited;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.PointerEntered -= OnAssociatedObjectPointerEntered;
AssociatedObject.PointerExited -= OnAssociatedObjectPointerExited;
}
private void OnAssociatedObjectPointerExited(object sender, PointerRoutedEventArgs e)
{
UpdateAngle(e);
}
private void OnAssociatedObjectPointerEntered(object sender, PointerRoutedEventArgs e)
{
UpdateAngle(e);
}
private void UpdateAngle(PointerRoutedEventArgs e)
{
if (AssociatedObject == null || AssociatedObject.StrokeThickness == 0)
return;
AssociatedObject.RenderTransformOrigin = new Point(0.5, 0.5);
var rotateTransform = AssociatedObject.RenderTransform as RotateTransform;
if (rotateTransform == null)
{
rotateTransform = new RotateTransform();
AssociatedObject.RenderTransform = rotateTransform;
}
var point = e.GetCurrentPoint(AssociatedObject.Parent as UIElement).Position;
var centerPoint = new Point(AssociatedObject.ActualWidth / 2, AssociatedObject.ActualHeight / 2);
var angleOfLine = Math.Atan2(point.Y - centerPoint.Y, point.X - centerPoint.X) * 180 / Math.PI;
rotateTransform.Angle = angleOfLine + 180;
}
}
這個類命名不是很好,不過將就一下吧。
為了做出邊框延伸的效果,另外需要一個類EllipseProgressBehavior:
public class EllipseProgressBehavior : Behavior<Ellipse>
{
/// <summary>
/// 獲取或設置Progress的值
/// </summary>
public double Progress
{
get { return (double)GetValue(ProgressProperty); }
set { SetValue(ProgressProperty, value); }
}
/// <summary>
/// 標識 Progress 依賴屬性。
/// </summary>
public static readonly DependencyProperty ProgressProperty =
DependencyProperty.Register("Progress", typeof(double), typeof(EllipseProgressBehavior), new PropertyMetadata(0d, OnProgressChanged));
private static void OnProgressChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var target = obj as EllipseProgressBehavior;
double oldValue = (double)args.OldValue;
double newValue = (double)args.NewValue;
if (oldValue != newValue)
target.OnProgressChanged(oldValue, newValue);
}
protected virtual void OnProgressChanged(double oldValue, double newValue)
{
UpdateStrokeDashArray();
}
protected virtual double GetTotalLength()
{
if (AssociatedObject == null)
return 0;
return (AssociatedObject.ActualHeight - AssociatedObject.StrokeThickness) * Math.PI;
}
private void UpdateStrokeDashArray()
{
if (AssociatedObject == null || AssociatedObject.StrokeThickness == 0)
return;
//if (target.ActualHeight == 0 || target.ActualWidth == 0)
// return;
var totalLength = GetTotalLength();
totalLength = totalLength / AssociatedObject.StrokeThickness;
var thirdSection = Progress * totalLength / 100;
var secondSection = (totalLength - thirdSection) / 2;
var result = new DoubleCollection { 0, secondSection, thirdSection, double.MaxValue };
AssociatedObject.StrokeDashArray = result;
}
}
套用到ControlTemplate如下:
<ControlTemplate TargetType="Button">
<Grid x:Name="RootGrid">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:1"
To="Normal">
<Storyboard>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetProperty="(local:EllipseProgressBehavior.Progress)"
Storyboard.TargetName="EllipseProgressBehavior">
<EasingDoubleKeyFrame KeyTime="0:0:1"
Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<QuinticEase EasingMode="EaseOut" />
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition GeneratedDuration="0:0:1"
To="PointerOver">
<Storyboard>
<DoubleAnimationUsingKeyFrames EnableDependentAnimation="True"
Storyboard.TargetProperty="(local:EllipseProgressBehavior.Progress)"
Storyboard.TargetName="EllipseProgressBehavior">
<EasingDoubleKeyFrame KeyTime="0:0:1"
Value="100">
<EasingDoubleKeyFrame.EasingFunction>
<QuinticEase EasingMode="EaseOut" />
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal">
<Storyboard>
<PointerUpThemeAnimation Storyboard.TargetName="RootGrid" />
</Storyboard>
</VisualState>
<VisualState x:Name="PointerOver">
<Storyboard>
<PointerUpThemeAnimation Storyboard.TargetName="RootGrid" />
</Storyboard>
<VisualState.Setters>
<Setter Target="EllipseProgressBehavior.(local:EllipseProgressBehavior.Progress)"
Value="100" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<PointerDownThemeAnimation Storyboard.TargetName="RootGrid" />
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled" />
</VisualStateGroup>
<