zoukankan      html  css  js  c++  java
  • [UWP]用Shape做动画

    相对于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奇思妙想

  • 相关阅读:
    全栈的苦逼和崛起
    Swift内部类调用外部类方法、属性的变通
    Assets.xcassets误删后的恢复
    UITableViewCell嵌套UITableView的正确姿势
    GPU Accelerated Computing with Python
    Windows 10创意者更新ISO发布!官方下载
    Amazing iOS Tips
    self-sizing cell的一个问题
    buf.swap32()
    buf.swap16()
  • 原文地址:https://www.cnblogs.com/dino623/p/UWP_Shape_Animation.html
Copyright © 2011-2022 走看看