实现一个 WPF 版本的 ConnectedAnimation
?
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 的作用是顯示一個 ConnectedVisual。ConnectedVisual 包含一個源和一個目標,根據 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++; }
▲ 上面的代碼做的連接動畫
目前的局限性以及改進計劃
然而稍微試試不難發現,這段代碼很難將控件本身隱藏起來(設置 Visibility 為 Collapsed),也就是說如果源控件和目標控件一直顯示,那么動畫期間就不允許隱藏(不同時顯示就沒有這個問題)。這樣也就出不來“連接”的感覺,而是覆蓋的感覺。
通過修改調用方的代碼,可以規避這個問題。而做法是隱藏控件本身,但對控件內部的可視元素子級進行動畫。這樣,動畫就僅限繼承自 Control 的那些元素(例如 Button,UserControl 了)。
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的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 导出CSV文件
- 下一篇: 条件数:逆矩阵与线性方程组