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,正在努力编写中……


参考资料

实现一个 WPF 版本的 ConnectedAnimation的更多相关文章

  1. 搭建QQ聊天通信的程序:(1)基于 networkcomms.net 创建一个WPF聊天客户端服务器应用程序 (1)

    搭建QQ聊天通信的程序:(1)基于 networkcomms.net 创建一个WPF聊天客户端服务器应用程序 原文地址(英文):http://www.networkcomms.net/creating ...

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

    WPF入门教程系列(一) 创建你的第一个WPF项目 WPF基础知识 快速学习绝不是从零学起的,良好的基础是快速入手的关键,下面先为大家摞列以下自己总结的学习WPF的几点基础知识: 1) C#基础语法知 ...

  3. [Aaronyang] 写给自己的WPF4.5 笔记15[AyArc诞生-WPF版本绚丽的环状图,Ay制作,AyWindow强势预览]

    原文:[Aaronyang] 写给自己的WPF4.5 笔记15[AyArc诞生-WPF版本绚丽的环状图,Ay制作,AyWindow强势预览]  我的文章一定要做到对读者负责,否则就是失败的文章  -- ...

  4. 演练:我的第一个 WPF 桌面应用程序 https://docs.microsoft.com/zh-cn/dotnet/framework/wpf/getting-started/walkthrough-my-first-wpf-desktop-application

    这篇文章演示如何开发简单的 Windows Presentation Foundation (WPF) 应用程序包括元素所共有的大多数 WPF 应用程序: 可扩展应用程序标记语言 (XAML) 标记. ...

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

    原文:将 C++/WinRT 中的线程切换体验带到 C# 中来(WPF 版本) 如果你要在 WPF 程序中使用线程池完成一个特殊的任务,那么使用 .NET 的 API Task.Run 并传入一个 L ...

  6. 一个WPF控件 诡异的MouseEvent 。

    背景: private System.Windows.Controls.Border _borderTouch; private bool _mouseDown = false;  private S ...

  7. 无法启动调试--未安装 Silverlight Developer 运行时。请安装一个匹配版本。

    引自:http://www.cnblogs.com/chillsrc/archive/2010/06/28/1766816.html 安装完VS2010中文版之后,又安装了Silverlight4_T ...

  8. 意外地解决了一个WPF布局问题

    原文:意外地解决了一个WPF布局问题 今天做了一个小测试,意外地将之前的一个困扰解决了,原问题见<WPF疑难杂症会诊>中的“怎么才能禁止内容撑大容器?” 以前我是在外侧嵌套Canvas容器 ...

  9. 51ak带你看MYSQL5.7源码3:修改代码实现你的第一个Mysql版本

    从事DBA工作多年 MYSQL源码也是头一次接触 尝试记录下自己看MYSQL5.7源码的历程 目录: 51ak带你看MYSQL5.7源码1:main入口函数 51ak带你看MYSQL5.7源码2:编译 ...

随机推荐

  1. Memory Manager surface area changes in SQL Server 2012

    here were various changes to memory related DMVs, DBCC memory status, and Perfmon counters in SQL Se ...

  2. Oracle中 如何用一个表的数据更新另一个表中的数据

    准备阶段 1.建表语句: create table table1( idd varchar2(10) , val varchar2(20) ); create table table2( idd va ...

  3. [转]基于Visual Studio 2010 进行敏捷/Scrum模式开发

    http://www.infoq.com/cn/articles/visual-studio-2010-agile-scrum-development 根据Forrester Research今年第二 ...

  4. fio使用

    关于lat(I/O completion latency).clat(complete latency).slat(summit latency): lat is the total latency. ...

  5. 到底啥是平台,到底啥是中台?李鬼太多,不得不说(ZT)

    (1)哪些不是中台,而是应该叫平台 做开发,有所谓的三层技术架构:前端展示层.中间逻辑层.后端数据层.我们现在讲的中台不在这个维度上. 做开发,还有所谓的技术中间件.一开始我们没有中间件的概念,只有操 ...

  6. MySQL mha 高可用集群搭建

    [mha] MHA作为MySQL故障切换和主从提升的高可用软件,在故障切换过程中,MHA能做到在0~30秒之内自动完成数据库的故障切换操作,并且在进行故障切换的过程中,MHA能在最大程度上保证数据的一 ...

  7. LeetCode OJ:Longest Substring Without Repeating Characters(最长无重复字符子串)

    Given a string, find the length of the longest substring without repeating characters. For example, ...

  8. 第1课:接口测试和jmeter总结

    接口测试 1. 接口的分类:webService和http api接口 1) webService接口:是按照soap协议通过http传输,请求报文和返回报文都是xml格式,一般要借助工具来测试接口: ...

  9. New Concept English three(15)

    31w/m 43 Children always appreciate small gifts of money. Father, of course, provides a regular supp ...

  10. DataTable和实体类通过反射相互转换

    using System.Runtime.Serialization; using System.Data; using System.Reflection; using System.Collect ...