相对于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>
</VisualStateManager.VisualStateGroups>
<ContentPresenter x:Name="ContentPresenter"
AutomationProperties.AccessibilityView="Raw"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Content="{TemplateBinding Content}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
<Ellipse Fill="Transparent"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="2">
<interactivity:Interaction.Behaviors>
<local:ChangeAngleToEnterPointerBehavior />
<local:EllipseProgressBehavior x:Name="EllipseProgressBehavior" />
</interactivity:Interaction.Behaviors>
</Ellipse>
</Grid>
</ControlTemplate>

注意:我没有鼓励任何人自定义按钮外观的意思,能用系统自带的动画或样式就尽量用系统自带的,没有设计师的情况下又想UI做得与众不同通常会做得很难看。想要UI好看,合理的布局、合理的颜色、合理的图片就足够了。

6. 结语

在学习Shape的过程中觉得好玩就做了很多尝试,因为以前工作中做过不少等待、进度的动画,所以这次就试着做出本文的动画。

XAML的传统动画并没有提供太多功能,主要是ColorAnimation、DoubleAnimation、PointAnimation三种,不过靠Binding和Converter可以弥补这方面的不足,实现很多需要的功能。

本文的一些动画效果参考了SVG的动画。话说回来,Windows 10 1703新增了Svg​Image​Source,不过看起来只是简单地将SVG翻译成对应的Shape,然后用Shape呈现,不少高级特性都不支持(如下图阴影的滤镜),用法如下:

<Image>
<Image.Source>
<SvgImageSource UriSource="feoffset_1.svg" />
</Image.Source>
</Image>

SvgImageSource:

原本的Svg:

7. 参考

情节提要动画

SVG奇思妙想

[UWP]用Shape做动画的更多相关文章

  1. [UWP]用Shape做动画(2):使用与扩展PointAnimation

    上一篇几乎都在说DoubleAnimation的应用,这篇说说PointAnimation. 1. 使用PointAnimation 使用PointAnimation可以让Shape变形,但实际上没看 ...

  2. [WPF] 用 OpacityMask 模仿 UWP 的 Text Shimmer 动画

    1. UWP 的 Text Shimmer 动画 在 UWP 的 Windows Composition Samples 中有一个 Text Shimmer 动画,它用于展示如何使用 Composit ...

  3. 使用requestAnimationFrame做动画效果二

    3月是个好日子,渐渐地开始忙起来了,我做事还是不够细心,加上感冒,没精神,今天差点又出事了,做过的事情还是要检查一遍才行,哎呀. 使用requestAnimationFrame做动画,我做了很久,终于 ...

  4. 让CALayer的shadowPath跟随bounds一起做动画改变-b

    在iOS开发中,我们经常需要给视图添加阴影效果,最简单的方法就是通过设置CALayer的shadowColor.shadowOpacity.shadowOffset和shadowRadius这几个属性 ...

  5. Android使用XML做动画UI

    在Android应用程序,使用动画效果,能带给用户更好的感觉.做动画可以通过XML或Android代码.本教程中,介绍使用XML来做动画.在这里,介绍基本的动画,如淡入,淡出,旋转等. 效果: htt ...

  6. transition和animation做动画(css动画二)

    前言:这是笔者学习之后自己的理解与整理.如果有错误或者疑问的地方,请大家指正,我会持续更新! translate:平移:是transform的一个属性: transform:变形:是一个静态属性,可以 ...

  7. 沿着path路径做动画

    沿着path路径做动画 路径 效果 源码 // // ViewController.m // PathAnimation // // Created by YouXianMing on 16/1/26 ...

  8. animation和transition做动画的区别

    animation做动画,是不需要去触发的,可以定义一开始就执行 transition做动画,是需要人为触发,才能执行的

  9. css3 transform做动画

    css3 transform做动画第一种用关键帧 这里就不说了 就说第二种方法用 transition属性 ps:1jquery anim不支持transform动画 但css还是支. 2 css3关 ...

随机推荐

  1. 主机ping通虚拟机,虚拟机ping通主机解决方法(NAT模式)

    有时候需要用虚拟机和宿主机模拟做数据交互,ping不通是件很烦人的事,本文以net模式解决这一问题. 宿主机系统:window7 虚拟机系统:CentOs7 连接方式:NAT模式 主机ping通虚拟机 ...

  2. IE和其他浏览器用JS新窗口打开的问题

    Chrome中 window.open(pageURL,name,parameters) pageURL 为子窗口路径 name 为子窗口句柄 parameters 为窗口参数(各参数用逗号分隔) 例 ...

  3. java类集框架(ArrayList,LinkedList,Vector区别)

    主要分两个接口:collection和Map 主要分三类:集合(set).列表(List).映射(Map)1.集合:没有重复对象,没有特定排序方式2.列表:对象按索引位置排序,可以有重复对象3.映射: ...

  4. sqoop关系型数据迁移原理以及map端内存为何不会爆掉窥探

    序:map客户端使用jdbc向数据库发送查询语句,将会拿到所有数据到map的客户端,安装jdbc的原理,数据全部缓存在内存中,但是内存没有出现爆掉情况,这是因为1.3以后,对jdbc进行了优化,改进j ...

  5. ICDM评选:数据挖掘十大经典算法

    原文地址:http://blog.csdn.net/aladdina/article/details/4141177 国际权威的学术组织the IEEE International Conferenc ...

  6. js 数字递增特效 仿支付宝我的财富 HTML5

    上周五应着公司临时需求,一天的时间解决掉官网(ps:比较简单哈哈),需求里面有一个特效就是数字递增到指定的数值,其实JS写也不复杂的,但是我发现一个js小插件,这个插件轻巧简单,用起来也非常简单实用. ...

  7. laytpl--前端数据绑定

    发现一枚前端数据绑定导弹:laytpl,官网:http://www.layui.com/laytpl/ 为了不用angularJS等较为重量级的,和繁琐的配置,所以就用了laytpl,可以配合JQ使用 ...

  8. 简介vsftpd及搭建配置

    一.简介 FTP(文件传输协议)全称是:Very Secure FTP Server.   Vsftpd是linux类操作系统上运行的ftp服务器软件. vsftp提供三种登陆方式:1.匿名登录  2 ...

  9. java线程(四)

    java5线程并发库 线程并发库是JDK 1.5版本级以上才有的针对线程并发编程提供的一些常用工具类,这些类被封装在java.concurrent包下. 该包下又有两个子包,分别是atomic和loc ...

  10. SOM网络聚类完整示例(利用python和java)

    下面是几个城市的GDP等信息,根据这些信息,写一个SOM网络,使之对下面城市进行聚类.并且,将结果画在一个二维平面上. //表1中,X.为人均GDP(元):X2为工业总产值(亿元):X.为社会消费品零 ...