相對於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新增了SvgImageSource,不過看起來只是簡單地將SVG翻譯成對應的Shape,然後用Shape呈現,不少高級特性都不支持(如下圖陰影的濾鏡),用法如下:
<Image> <Image.Source> <SvgImageSource UriSource="feoffset_1.svg" /> </Image.Source> </Image>
SvgImageSource:
原本的Svg: