0

0

用Shape做动画实例代码

零下一度

零下一度

发布时间:2018-05-11 17:33:43

|

3163人浏览过

|

来源于php中文网

原创

相对于wpf/silverlight,uwp的动画系统可以说有大幅提高,不过本文无意深入讨论这些动画api,本文将介绍使用shape做一些进度、等待方面的动画,除此之外也会介绍一些相关技巧。

1. 使用StrokeDashOffset做等待提示动画

圆形的等待提示动画十分容易做,只要让它旋转就可以了:

用Shape做动画实例代码

但是圆形以外的形状就不容易做了,例如三角形,总不能让它单纯地旋转吧:

用Shape做动画实例代码

要解决这个问题可以使用StrokeDashOffset。StrokeDashOffset用于控制虚线边框的第一个短线相对于Shape开始点的位移,使用动画控制这个数值可以做出边框滚动的效果:

用Shape做动画实例代码

需要注意的是Shape的边长要正好能被StrokeDashArray中短线和缺口的和整除,即 满足边长 / StrokeThickness % Sum( StrokeDashArray ) = 0,这是因为在StrokeDashOffset=0的地方会截断短线,如下图所示:

用Shape做动画实例代码

另外注意的是边长的计算,如Rectangle,边长并不是(Height + Width) * 2,而是(Height - StrokeThickness) * 2 + (Width- StrokeThickness) * 2,如下图所示,边长应该从边框正中间开始计算:

用Shape做动画实例代码

有一些Shape的边长计算还会受到Stretch影响,如上一篇中自定义的Triangle:

用Shape做动画实例代码

2. 使用StrokeDashArray做进度提示动画

StrokeDashArray用于将Shape的边框变成虚线,StrokeDashArray的值是一个double类型的有序集合,里面的数值指定虚线中每一段以StrokeThickness为单位的长度。用StrokeDashArray做进度提示的基本做法就是将进度Progress通过Converter转换为分成两段的StrokeDashArray,第一段为实线,表示当前进度,第二段为空白。假设一个Shape的边长是100,当前进度为50,则将StrokeDashArray设置成{50,double.MaxValue}两段。

做成动画如下图所示:

用Shape做动画实例代码


其中ProgressToStrokeDashArrayConverter和ProgressToStrokeDashArrayConverter2的代码如下:

public class ProgressToStrokeDashArrayConverter : DependencyObject, IValueConverter
{/// /// 获取或设置TargetPath的值///   public Path TargetPath
    {
    get { return (Path)GetValue(TargetPathProperty);
     }
    set {
     SetValue(TargetPathProperty, value); 
    }
    }/// /// 标识 TargetPath 依赖属性。/// 
    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
{/// /// 获取或设置Progress的值///   public double Progress
    {get { return (double)GetValue(ProgressProperty); }set { SetValue(ProgressProperty, value); }
    }/// /// 标识 Progress 依赖属性。/// 
    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用法如下:

台讯电子企业网站管理系统  简繁全功能版
台讯电子企业网站管理系统 简繁全功能版

超级适合代理建设企业站点的企业源码,超方面实用!程序说明: 1.特色:简繁中文切换、产品展示系统、新闻发布系统、会员管理系统、留言本计数器、网站信息统计、强大后台操作 功能等; 2.页面包括:首页、企业介绍、滚动公告通知发布系统、企业新闻系统、产品展示系统、企业案例发布展示系 统、企业招聘信息发布系统、信息资源下载系统、在线定单系统、在线客服系统、在线留言本系统、网站调查投票系统、友情连接系统、会

下载

  
  

但其实这是行不通的,XAML有一个存在了很久的限制:However, an existing limitation of the Windows Runtime XAML implementation is that you cannot animate a custom attached property.。这个限制决定了XAML不能对自定义附加属性做动画。不过,这个限制只限制了不能对自定义附加属性本身做动画,但对附加属性中的类的属性则可以,例如以下这种写法应该是行得通的:


  
      

更优雅的写法是利用XamlBehaviors,这篇文章很好地解释了XamlBehaviors的作用:

XAML Behaviors非常重要,因为它们提供了一种方法,让开发人员能够以一种简洁、可重复的方式轻松地向UI对象添加功能。
他们无需创建控件的子类或重复编写逻辑代码,只要简单地增加一个XAML代码片段。

要使用Behavior改进现有代码,只需实现一个PathProgressBehavior:

public class PathProgressBehavior : Behavior
{protected override void OnAttached()
    {base.OnAttached();UpdateStrokeDashArray();
    }/// /// 获取或设置Progress的值///   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中如下使用:


  
  
    
  
  

这样看起来就清爽多了。

4. 模仿背景填充动画

先看看效果:

用Shape做动画实例代码

其实这篇文章里并不会讨论填充动画,不过首先声明做填充动画会更方便快捷,这一段只是深入学习过程中的产物,实用价值不高。
上图三角形的填充的效果只需要叠加两个同样大小的Shape,前面那个设置Stretch="Uniform",再通过DoubleAnimation改变它的高度就可以了。文字也是相同的原理,叠加两个相同的TextBlock,将前面那个放在一个无边框的ScrollViewer里再去改变ScrollViewer的高度。

ProgressToHeightConverter和ReverseProgressToHeightConverter的代码如下:

public class ProgressToHeightConverter : DependencyObject, IValueConverter
{/// /// 获取或设置TargetContentControl的值///   public ContentControl TargetContentControl
    {
    get { 
    return (ContentControl)GetValue(TargetContentControlProperty); 
    }
    set { 
    SetValue(TargetContentControlProperty, value); 
    }
    }/// /// 标识 TargetContentControl 依赖属性。///
     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
{/// /// 获取或设置TargetContentControl的值///   
public ContentControl TargetContentControl
    {
    get { 
    return (ContentControl)GetValue(TargetContentControlProperty);
     }
    set { 
    SetValue(TargetContentControlProperty, value); 
    }
    }/// /// 标识 TargetContentControl 依赖属性。/// 
    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可以制作很有趣的按钮:

用Shape做动画实例代码

PointerEntered时,按钮的边框从进入点向反方向延伸。PointerExited时,边框从反方向向移出点消退。要做到这点需要在PointerEntered时改变边框的方向,使用了ChangeAngleToEnterPointerBehavior:

public class ChangeAngleToEnterPointerBehavior : Behavior
{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
{/// /// 获取或设置Progress的值///   
public double Progress
    {
    get { 
    return (double)GetValue(ProgressProperty); 
    }
    set { 
    SetValue(ProgressProperty, value); 
    }
    }/// /// 标识 Progress 依赖属性。/// 
    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如下:

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

6. 结语

在学习Shape的过程中觉得好玩就做了很多尝试,因为以前工作中做过不少等待、进度的动画,所以这次就试着做出本文的动画。
XAML的传统动画并没有提供太多功能,主要是ColorAnimation、DoubleAnimation、PointAnimation三种,不过靠Binding和Converter可以弥补这方面的不足,实现很多需要的功能。
本文的一些动画效果参考了SVG的动画。话说回来,Windows 10 1703新增了SvgImageSource,不过看起来只是简单地将SVG翻译成对应的Shape,然后用Shape呈现,不少高级特性都不支持(如下图阴影的滤镜),用法如下:

SvgImageSource:
用Shape做动画实例代码

原本的Svg:
用Shape做动画实例代码

相关专题

更多
云朵浏览器入口合集
云朵浏览器入口合集

本专题整合了云朵浏览器入口合集,阅读专题下面的文章了解更多详细地址。

0

2026.01.20

Java JVM 原理与性能调优实战
Java JVM 原理与性能调优实战

本专题系统讲解 Java 虚拟机(JVM)的核心工作原理与性能调优方法,包括 JVM 内存结构、对象创建与回收流程、垃圾回收器(Serial、CMS、G1、ZGC)对比分析、常见内存泄漏与性能瓶颈排查,以及 JVM 参数调优与监控工具(jstat、jmap、jvisualvm)的实战使用。通过真实案例,帮助学习者掌握 Java 应用在生产环境中的性能分析与优化能力。

20

2026.01.20

PS使用蒙版相关教程
PS使用蒙版相关教程

本专题整合了ps使用蒙版相关教程,阅读专题下面的文章了解更多详细内容。

62

2026.01.19

java用途介绍
java用途介绍

本专题整合了java用途功能相关介绍,阅读专题下面的文章了解更多详细内容。

87

2026.01.19

java输出数组相关教程
java输出数组相关教程

本专题整合了java输出数组相关教程,阅读专题下面的文章了解更多详细内容。

39

2026.01.19

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

10

2026.01.19

xml格式相关教程
xml格式相关教程

本专题整合了xml格式相关教程汇总,阅读专题下面的文章了解更多详细内容。

13

2026.01.19

PHP WebSocket 实时通信开发
PHP WebSocket 实时通信开发

本专题系统讲解 PHP 在实时通信与长连接场景中的应用实践,涵盖 WebSocket 协议原理、服务端连接管理、消息推送机制、心跳检测、断线重连以及与前端的实时交互实现。通过聊天系统、实时通知等案例,帮助开发者掌握 使用 PHP 构建实时通信与推送服务的完整开发流程,适用于即时消息与高互动性应用场景。

19

2026.01.19

微信聊天记录删除恢复导出教程汇总
微信聊天记录删除恢复导出教程汇总

本专题整合了微信聊天记录相关教程大全,阅读专题下面的文章了解更多详细内容。

160

2026.01.18

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Node.js 教程
Node.js 教程

共57课时 | 9万人学习

Vue 教程
Vue 教程

共42课时 | 6.8万人学习

Go 教程
Go 教程

共32课时 | 4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号