Windows 10 的创造者更新为开发者们带来了 Connected Animation 连接动画,这也是 Fluent Design System 的一部分。它的视觉引导性很强,用户能够在它的帮助下迅速定位操作的对象。

不过,这是 UWP,而且还是 Windows 10 Creator’s Update 中才带来的特性,WPF 当然没有。于是,我自己写了一个“简易版本”。



▲ Connected Animation 连接动画

模拟 UWP 中的 API

UWP 中的连接动画能跑起来的最简单代码包含下面两个部分。

准备动画 PrepareToAnimate()

ConnectedAnimationService.GetForCurrentView().PrepareToAnimate(/*string */key, /*UIElement */source);

开始动画 TryStart

var animation = ConnectedAnimationService.GetForCurrentView().GetAnimation(/*string */key);
animation?.TryStart(/*UIElement */destination);

于是,我们至少需要实现这些 API:

  • ConnectedAnimationService.GetForCurrentView();
  • ConnectedAnimationService.PrepareToAnimate(string key, UIElement source);
  • ConnectedAnimationService.GetAnimation(string key);
  • ConnectedAnimation.TryStart(UIElement destination);

实现这个 API

现在,我们需要写两个类才能实现上面那些方法:

  • ConnectedAnimationService - 用来管理一个窗口内的所有连接动画
  • ConnectedAnimation - 用来管理和播放一个指定 Key 的连接动画

ConnectedAnimationService

我选用窗口作为一个 ConnectedAnimationService 的管理单元是因为我可以在一个窗口内实现这样的动画,而跨窗口的动画就非常麻烦了。所以,我试用附加属性为 Window 附加一个 ConnectedAnimationService 属性,用于在任何一个 View 所在的地方获取 ConnectedAnimationService 的实例。

每次 PrepareToAnimate 时我创建一个 ConnectedAnimation 实例来管理此次的连接动画。为了方便此后根据 Key 查找 ConnectedAnimation 的实例,我使用字典存储这些实例。

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using Walterlv.Annotations;namespace Walterlv.Demo.Media.Animation
{public class ConnectedAnimationService{private ConnectedAnimationService(){}private readonly Dictionary<string, ConnectedAnimation> _connectingAnimations =new Dictionary<string, ConnectedAnimation>();public void PrepareToAnimate([NotNull] string key, [NotNull] UIElement source){if (key == null){throw new ArgumentNullException(nameof(key));}if (source == null){throw new ArgumentNullException(nameof(source));}if (_connectingAnimations.TryGetValue(key, out var info)){throw new ArgumentException("指定的 key 已经做好动画准备,不应该重复进行准备。", nameof(key));}info = new ConnectedAnimation(key, source, OnAnimationCompleted);_connectingAnimations.Add(key, info);}private void OnAnimationCompleted(object sender, EventArgs e){var key = ((ConnectedAnimation) sender).Key;if (_connectingAnimations.ContainsKey(key)){_connectingAnimations.Remove(key);}}[CanBeNull]public ConnectedAnimation GetAnimation([NotNull] string key){if (key == null){throw new ArgumentNullException(nameof(key));}if (_connectingAnimations.TryGetValue(key, out var info)){return info;}return null;}private static readonly DependencyProperty AnimationServiceProperty =DependencyProperty.RegisterAttached("AnimationService",typeof(ConnectedAnimationService), typeof(ConnectedAnimationService),new PropertyMetadata(default(ConnectedAnimationService)));public static ConnectedAnimationService GetForCurrentView(Visual visual){var window = Window.GetWindow(visual);if (window == null){throw new ArgumentException("此 Visual 未连接到可见的视觉树中。", nameof(visual));}var service = (ConnectedAnimationService) window.GetValue(AnimationServiceProperty);if (service == null){service = new ConnectedAnimationService();window.SetValue(AnimationServiceProperty, service);}return service;}}
}

ConnectedAnimation

这是连接动画的关键实现。

我创建了一个内部类 ConnectedAnimationAdorner 用于在 AdornerLayer 上承载连接动画。AdornerLayer 是 WPF 中的概念,用于在其他控件上叠加显示一些 UI,UWP 中没有这样的特性。

private class ConnectedAnimationAdorner : Adorner
{private ConnectedAnimationAdorner([NotNull] UIElement adornedElement): base(adornedElement){Children = new VisualCollection(this);IsHitTestVisible = false;}internal VisualCollection Children { get; }protected override int VisualChildrenCount => Children.Count;protected override Visual GetVisualChild(int index) => Children[index];protected override Size ArrangeOverride(Size finalSize){foreach (var child in Children.OfType<UIElement>()){child.Arrange(new Rect(child.DesiredSize));}return finalSize;}internal static ConnectedAnimationAdorner FindFrom([NotNull] Visual visual){if (Window.GetWindow(visual)?.Content is UIElement root){var layer = AdornerLayer.GetAdornerLayer(root);if (layer != null){var adorner = layer.GetAdorners(root)?.OfType<ConnectedAnimationAdorner>().FirstOrDefault();if (adorner == null){adorner = new ConnectedAnimationAdorner(root);layer.Add(adorner);}return adorner;}}throw new InvalidOperationException("指定的 Visual 尚未连接到可见的视觉树中,找不到用于承载动画的容器。");}internal static void ClearFor([NotNull] Visual visual){if (Window.GetWindow(visual)?.Content is UIElement root){var layer = AdornerLayer.GetAdornerLayer(root);var adorner = layer?.GetAdorners(root)?.OfType<ConnectedAnimationAdorner>().FirstOrDefault();if (adorner != null){layer.Remove(adorner);}}}
}

ConnectedAnimationAdorner 的作用是显示一个 ConnectedVisualConnectedVisual 包含一个源和一个目标,根据 Progress(进度)属性决定应该分别将源和目标显示到哪个位置,其不透明度分别是多少。

private class ConnectedVisual : DrawingVisual
{public static readonly DependencyProperty ProgressProperty = DependencyProperty.Register("Progress", typeof(double), typeof(ConnectedVisual),new PropertyMetadata(0.0, OnProgressChanged), ValidateProgress);public double Progress{get => (double) GetValue(ProgressProperty);set => SetValue(ProgressProperty, value);}private static bool ValidateProgress(object value) =>value is double progress && progress >= 0 && progress <= 1;private static void OnProgressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){((ConnectedVisual) d).Render((double) e.NewValue);}public ConnectedVisual([NotNull] Visual source, [NotNull] Visual destination){_source = source ?? throw new ArgumentNullException(nameof(source));_destination = destination ?? throw new ArgumentNullException(nameof(destination));_sourceBrush = new VisualBrush(source) {Stretch = Stretch.Fill};_destinationBrush = new VisualBrush(destination) {Stretch = Stretch.Fill};}private readonly Visual _source;private readonly Visual _destination;private readonly Brush _sourceBrush;private readonly Brush _destinationBrush;private Rect _sourceBounds;private Rect _destinationBounds;protected override void OnVisualParentChanged(DependencyObject oldParent){if (VisualTreeHelper.GetParent(this) == null){return;}var sourceBounds = VisualTreeHelper.GetContentBounds(_source);if (sourceBounds.IsEmpty){sourceBounds = VisualTreeHelper.GetDescendantBounds(_source);}_sourceBounds = new Rect(_source.PointToScreen(sourceBounds.TopLeft),_source.PointToScreen(sourceBounds.BottomRight));_sourceBounds = new Rect(PointFromScreen(_sourceBounds.TopLeft),PointFromScreen(_sourceBounds.BottomRight));var destinationBounds = VisualTreeHelper.GetContentBounds(_destination);if (destinationBounds.IsEmpty){destinationBounds = VisualTreeHelper.GetDescendantBounds(_destination);}_destinationBounds = new Rect(_destination.PointToScreen(destinationBounds.TopLeft),_destination.PointToScreen(destinationBounds.BottomRight));_destinationBounds = new Rect(PointFromScreen(_destinationBounds.TopLeft),PointFromScreen(_destinationBounds.BottomRight));}private void Render(double progress){var bounds = new Rect((_destinationBounds.Left - _sourceBounds.Left) * progress + _sourceBounds.Left,(_destinationBounds.Top - _sourceBounds.Top) * progress + _sourceBounds.Top,(_destinationBounds.Width - _sourceBounds.Width) * progress + _sourceBounds.Width,(_destinationBounds.Height - _sourceBounds.Height) * progress + _sourceBounds.Height);using (var dc = RenderOpen()){dc.DrawRectangle(_sourceBrush, null, bounds);dc.PushOpacity(progress);dc.DrawRectangle(_destinationBrush, null, bounds);dc.Pop();}}
}

最后,用一个 DoubleAnimation 控制 Progress 属性,来实现连接动画。

完整的包含内部类的代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Animation;
using Walterlv.Annotations;namespace Walterlv.Demo.Media.Animation
{public class ConnectedAnimation{internal ConnectedAnimation([NotNull] string key, [NotNull] UIElement source, [NotNull] EventHandler completed){Key = key ?? throw new ArgumentNullException(nameof(key));_source = source ?? throw new ArgumentNullException(nameof(source));_reportCompleted = completed ?? throw new ArgumentNullException(nameof(completed));}public string Key { get; }private readonly UIElement _source;private readonly EventHandler _reportCompleted;public bool TryStart([NotNull] UIElement destination){return TryStart(destination, Enumerable.Empty<UIElement>());}public bool TryStart([NotNull] UIElement destination, [NotNull] IEnumerable<UIElement> coordinatedElements){if (destination == null){throw new ArgumentNullException(nameof(destination));}if (coordinatedElements == null){throw new ArgumentNullException(nameof(coordinatedElements));}if (Equals(_source, destination)){return false;}// 正在播动画?动画播完废弃了?false// 准备播放连接动画。var adorner = ConnectedAnimationAdorner.FindFrom(destination);var connectionHost = new ConnectedVisual(_source, destination);adorner.Children.Add(connectionHost);var storyboard = new Storyboard();var animation = new DoubleAnimation(0.0, 1.0, new Duration(TimeSpan.FromSeconds(10.6))){EasingFunction = new CubicEase {EasingMode = EasingMode.EaseInOut},};Storyboard.SetTarget(animation, connectionHost);Storyboard.SetTargetProperty(animation, new PropertyPath(ConnectedVisual.ProgressProperty.Name));storyboard.Children.Add(animation);storyboard.Completed += (sender, args) =>{_reportCompleted(this, EventArgs.Empty);//destination.ClearValue(UIElement.VisibilityProperty);adorner.Children.Remove(connectionHost);};//destination.Visibility = Visibility.Hidden;storyboard.Begin();return true;}private class ConnectedVisual : DrawingVisual{public static readonly DependencyProperty ProgressProperty = DependencyProperty.Register("Progress", typeof(double), typeof(ConnectedVisual),new PropertyMetadata(0.0, OnProgressChanged), ValidateProgress);public double Progress{get => (double) GetValue(ProgressProperty);set => SetValue(ProgressProperty, value);}private static bool ValidateProgress(object value) =>value is double progress && progress >= 0 && progress <= 1;private static void OnProgressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){((ConnectedVisual) d).Render((double) e.NewValue);}public ConnectedVisual([NotNull] Visual source, [NotNull] Visual destination){_source = source ?? throw new ArgumentNullException(nameof(source));_destination = destination ?? throw new ArgumentNullException(nameof(destination));_sourceBrush = new VisualBrush(source) {Stretch = Stretch.Fill};_destinationBrush = new VisualBrush(destination) {Stretch = Stretch.Fill};}private readonly Visual _source;private readonly Visual _destination;private readonly Brush _sourceBrush;private readonly Brush _destinationBrush;private Rect _sourceBounds;private Rect _destinationBounds;protected override void OnVisualParentChanged(DependencyObject oldParent){if (VisualTreeHelper.GetParent(this) == null){return;}var sourceBounds = VisualTreeHelper.GetContentBounds(_source);if (sourceBounds.IsEmpty){sourceBounds = VisualTreeHelper.GetDescendantBounds(_source);}_sourceBounds = new Rect(_source.PointToScreen(sourceBounds.TopLeft),_source.PointToScreen(sourceBounds.BottomRight));_sourceBounds = new Rect(PointFromScreen(_sourceBounds.TopLeft),PointFromScreen(_sourceBounds.BottomRight));var destinationBounds = VisualTreeHelper.GetContentBounds(_destination);if (destinationBounds.IsEmpty){destinationBounds = VisualTreeHelper.GetDescendantBounds(_destination);}_destinationBounds = new Rect(_destination.PointToScreen(destinationBounds.TopLeft),_destination.PointToScreen(destinationBounds.BottomRight));_destinationBounds = new Rect(PointFromScreen(_destinationBounds.TopLeft),PointFromScreen(_destinationBounds.BottomRight));}private void Render(double progress){var bounds = new Rect((_destinationBounds.Left - _sourceBounds.Left) * progress + _sourceBounds.Left,(_destinationBounds.Top - _sourceBounds.Top) * progress + _sourceBounds.Top,(_destinationBounds.Width - _sourceBounds.Width) * progress + _sourceBounds.Width,(_destinationBounds.Height - _sourceBounds.Height) * progress + _sourceBounds.Height);using (var dc = RenderOpen()){dc.DrawRectangle(_sourceBrush, null, bounds);dc.PushOpacity(progress);dc.DrawRectangle(_destinationBrush, null, bounds);dc.Pop();}}}private class ConnectedAnimationAdorner : Adorner{private ConnectedAnimationAdorner([NotNull] UIElement adornedElement): base(adornedElement){Children = new VisualCollection(this);IsHitTestVisible = false;}internal VisualCollection Children { get; }protected override int VisualChildrenCount => Children.Count;protected override Visual GetVisualChild(int index) => Children[index];protected override Size ArrangeOverride(Size finalSize){foreach (var child in Children.OfType<UIElement>()){child.Arrange(new Rect(child.DesiredSize));}return finalSize;}internal static ConnectedAnimationAdorner FindFrom([NotNull] Visual visual){if (Window.GetWindow(visual)?.Content is UIElement root){var layer = AdornerLayer.GetAdornerLayer(root);if (layer != null){var adorner = layer.GetAdorners(root)?.OfType<ConnectedAnimationAdorner>().FirstOrDefault();if (adorner == null){adorner = new ConnectedAnimationAdorner(root);layer.Add(adorner);}return adorner;}}throw new InvalidOperationException("指定的 Visual 尚未连接到可见的视觉树中,找不到用于承载动画的容器。");}internal static void ClearFor([NotNull] Visual visual){if (Window.GetWindow(visual)?.Content is UIElement root){var layer = AdornerLayer.GetAdornerLayer(root);var adorner = layer?.GetAdorners(root)?.OfType<ConnectedAnimationAdorner>().FirstOrDefault();if (adorner != null){layer.Remove(adorner);}}}}}
}

调用

我在一个按钮的点击事件里面尝试调用上面的代码:

private int index;private void AnimationButton_Click(object sender, RoutedEventArgs e)
{BeginConnectedAnimation((UIElement)sender, ConnectionDestination);
}private async void BeginConnectedAnimation(UIElement source, UIElement destination)
{var service = ConnectedAnimationService.GetForCurrentView(this);service.PrepareToAnimate($"Test{index}", source);// 这里特意写在了同一个方法中,以示效果。事实上,只要是同一个窗口中的两个对象都可以实现。var animation = service.GetAnimation($"Test{index}");animation?.TryStart(destination);// 每次点击都使用不同的 Key。index++;
}


▲ 上面的代码做的连接动画

目前的局限性以及改进计划

然而稍微试试不难发现,这段代码很难将控件本身隐藏起来(设置 VisibilityCollapsed),也就是说如果源控件和目标控件一直显示,那么动画期间就不允许隐藏(不同时显示就没有这个问题)。这样也就出不来“连接”的感觉,而是覆盖的感觉。

通过修改调用方的代码,可以规避这个问题。而做法是隐藏控件本身,但对控件内部的可视元素子级进行动画。这样,动画就仅限继承自 Control 的那些元素(例如 ButtonUserControl 了)。

private async void BeginConnectedAnimation(UIElement source, UIElement destination)
{source.Visibility = Visibility.Hidden;ConnectionDestination.Visibility = Visibility.Hidden;var animatingSource = (UIElement) VisualTreeHelper.GetChild(source, 0);var animatingDestination = (UIElement) VisualTreeHelper.GetChild(destination, 0);var service = ConnectedAnimationService.GetForCurrentView(this);service.PrepareToAnimate($"Test{index}", animatingSource);var animation = service.GetAnimation($"Test{index}");animation?.TryStart(animatingDestination);index++;await Task.Delay(600);source.ClearValue(VisibilityProperty);ConnectionDestination.ClearValue(VisibilityProperty);
}


▲ 修改后的代码做的连接动画

现在,我正试图通过截图和像素着色器(Shader Effect)来实现更加通用的 ConnectedAnimation,正在努力编写中……


参考资料

  • Connected animation - UWP app developer - Microsoft Docs
  • UWP Connected Animations updates with Windows Creators release – Varun Shandilya
  • 实现Fluent Design中的Connected Animation - ^ _ ^ .io

转载于:https://www.cnblogs.com/walterlv/p/10236511.html

实现一个 WPF 版本的 ConnectedAnimation相关推荐

  1. 从零开始搭建一个GIS开发小框架(五)——GMap.Net组件WPF版本使用体验

    目录 1.试用情况介绍 2.规划功能 3.Demo实现效果演示 4.传送门(其它后续添加的内容) 5.多边形绘制和注册鼠标事件的代码讲解 1.试用情况介绍 现在windows平台的开发越来越流行WPF ...

  2. prism项目搭建 wpf_Prism完成的一个WPF项目

    本着每天记录一点成长一点的原则,打算将目前完成的一个WPF项目相关的技术分享出来,供团队学习与总结. 总共分三个部分: 基础篇主要针对C#初学者,巩固C#常用知识点: 中级篇主要针对WPF布局与Mat ...

  3. VS2010 教程:创建一个 WPF 应用程序 (第一节)

    来自:https://msdn.microsoft.com/zh-cn/library/ff629048.aspx [原文发表地址] VS2010 Tutorial: Build a WPF App ...

  4. WPF入门教程系列(一) 创建你的第一个WPF项目

    WPF基础知识 快速学习绝不是从零学起的,良好的基础是快速入手的关键,下面先为大家摞列以下自己总结的学习WPF的几点基础知识: 1) C#基础语法知识(或者其他.NET支持的语言):这个是当然的了,虽 ...

  5. 将 C++/WinRT 中的线程切换体验带到 C# 中来(WPF 版本)

    如果你要在 WPF 程序中使用线程池完成一个特殊的任务,那么使用 .NET 的 API Task.Run 并传入一个 Lambda 表达式可以完成.不过,使用 Lambda 表达式会带来变量捕获的一些 ...

  6. svn 回归某一个特定版本

    svn回归某一个特定版本: 先用svn log查看回归版本的版本号 version 然后用命令 svn up -r version

  7. 一起学WPF系列(2):第一个WPF应用程序

    概述 Windows Presentation Foundation (WPF) 是下一代显示系统,用于生成能带给用户震撼视觉体验的 Windows 客户端应用程序.使用 WPF,您可以创建广泛的独立 ...

  8. Windows 10或成为最后一个Windows版本

    据The Verge报道,在近日举行的微软Ignite大会上,微软开发大使Jerry Nixon透露了一则消息,"Windows 10将是Windows的最后一个版本",未来将会是 ...

  9. Android Hook Java的的一个改进版本

    目录(?)[-] Hook Java的的一个改进版本 改进点一更简单地修改java方法为本地方法 改进点二方法回调避免线程安全问题 最后 Hook Java的的一个改进版本 <注入安卓进程,并H ...

最新文章

  1. ROS系统开发——ROS,realsense风险和解决方案备忘录
  2. 因一个计算机故障而“停工”!观测宇宙 30 多年的哈勃太空望远镜还能坚持多久?...
  3. APP投资 历史 十万到 十亿元的项目
  4. MySQL:Innodb DB_ROLL_PTR指针解析
  5. mysql修改表结构(拆分)
  6. 【learning】洲阁筛
  7. 如何计算k段流水线执行n条指令的执行时间
  8. pythontcp文件传输_python socket实现文件传输(防粘包)
  9. springmvc跳转html_SpringMVC基础(三)
  10. 时速云入选2018中国企业服务创新成长50强
  11. Android学习之Image操作及时间日期选择器
  12. Kafka Consumer端的一些解惑
  13. join为什么每个字符都分割了 js_为什么 webpack4 默认支持 ES6 语法的压缩?
  14. php中读取文件内容的几种方法。(file_get_contents:将文件内容读入一个字符串)...
  15. sql:Mysql create view,function,procedure
  16. MapReduce原理转
  17. JDK的环境变量配置(详细步骤)
  18. 利用tensorflow加载VGG19
  19. word转pdf实现,POIXMLDocumentPart.getPackageRelationship()Lorg...问题,以及NotOfficeXmlFileException...问题
  20. 《web课程设计》期末网页制作 基于HTML+CSS+JavaScript制作公司官网页面精美

热门文章

  1. POJ 3522 Slim Span (Kruskal枚举最小边)
  2. 单链表的几个基本操作
  3. WebService安全 身份验证与访问控制
  4. Extjs4.0 开发笔记-desktop开始菜单动态生成方法
  5. jenkins安装插件一直不动
  6. javascript常用验证大全
  7. ubuntu vscode上使用cmake、编译、调试
  8. oracle11g exp导出问题:部分表导不出来
  9. 开源项目JacpFX
  10. ASP.NET编程中的十大技巧【转载】