上一篇幾乎都在說DoubleAnimation的應用,這篇說說PointAnimation。
1. 使用PointAnimation
使用PointAnimation可以讓Shape變形,但實際上沒看到多少人會這麼用,畢竟WPF做的軟體多數不需要這麼花俏。
1.1 在XAML上使用PointAnimation
<Storyboard x:Name="Storyboard2" RepeatBehavior="Forever" AutoReverse="True" Duration="0:0:4"><PointAnimation Storyboard.TargetProperty="(Path.Data).(PathGeometry.Figures)[0].(PathFigure.StartPoint)" Storyboard.TargetName="Path2" To="0,0" EnableDependentAnimation="True" /><PointAnimation Storyboard.TargetProperty="(Path.Data).(PathGeometry.Figures)[0].(PathFigure.Segments)[0].(LineSegment.Point)" Storyboard.TargetName="Path2" To="100,0" EnableDependentAnimation="True" /><ColorAnimation To="#FF85C82E" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" Storyboard.TargetName="Path2" /></Storyboard>…<Path Margin="0,20,0,0" x:Name="Path2" Fill="GreenYellow"><Path.Data><PathGeometry><PathFigure StartPoint="50,0"><LineSegment Point="50,0" /><LineSegment Point="0,100" /><LineSegment Point="0,100" /><LineSegment Point="100,100" /><LineSegment Point="100,100" /></PathFigure></PathGeometry></Path.Data></Path>
在這個例子裡最頭痛的地方是Property-path 文法,如果不能熟記的話最好依賴Blend產生。
1.2 在代碼中使用PointAnimation
如果Point數量很多,例表,通常會在C#代碼中使用PointAnimation:
_storyboard = new Storyboard();Random random = new Random();for (int i = 0; i < _pathFigure.Segments.Count; i++){var animation = new PointAnimation { Duration = TimeSpan.FromSeconds(3) }; Storyboard.SetTarget(animation, _pathFigure.Segments[i]); Storyboard.SetTargetProperty(animation, "(LineSegment.Point)"); animation.EnableDependentAnimation = true; animation.EasingFunction = new QuarticEase { EasingMode = EasingMode.EaseOut }; animation.To = new Point((_pathFigure.Segments[i] as LineSegment).Point.X, (i % 2 == 0 ? 1 : -1) * i * 1.2 + 60); _storyboard.Children.Add(animation);}_storyboard.Begin();
因為可以直接SetTarget
,所以Property-path文法就可以很簡單。
2. 擴充PointAnimation
上面兩個例子的動畫都還算簡單,如果更複雜些,XAML或C#代碼都需要寫到很複雜。我參考了這個網頁 想做出類似的動畫,但發現需要寫很多XAML所以放棄用PointAnimation實現。這個頁面的動畫核心是這段HTML:
<polygon fill="#FFD41D" points="97.3,0 127.4,60.9 194.6,70.7 145.9,118.1 157.4,185.1 97.3,153.5 37.2,185.1 48.6,118.1 0,70.7 67.2,60.9"> <animate id="animation-to-check" begin="indefinite" fill="freeze" attributeName="points" dur="500ms" to="110,58.2 147.3,0 192.1,29 141.7,105.1 118.7,139.8 88.8,185.1 46.1,156.5 0,125 23.5,86.6 71.1,116.7"/> <animate id="animation-to-star" begin="indefinite" fill="freeze" attributeName="points" dur="500ms" to="97.3,0 127.4,60.9 194.6,70.7 145.9,118.1 157.4,185.1 97.3,153.5 37.2,185.1 48.6,118.1 0,70.7 67.2,60.9"/> </polygon>
只需一組Point的集合就可以控制所有Point的動畫,確實比PointAnimation高效很多。 在WPF中可以通過繼承Timeline實現一個PointCollectionAnimamtion,具體可以參考這個項目。可惜的是雖然UWP的Timeline類並不封閉,但完全不知道如何繼承並派生一個自訂的Animation。
這時候需要稍微變通一下思維。可以將DoubleAnimation理解成這樣:Storyboard將TimeSpan傳遞給DoubleAnimation,DoubleAnimation通過這個TimeSpan(有時還需要結合EasingFunction)計算出目標屬性的當前值最後傳遞給目標屬性,如所示:
既然這樣,也可以接收到這個計算出來的Double,再通過Converter計算出目標的PointCollection值:
假設告訴這個Converter當傳入的Double值(命名為Progress)為0的時候,PointCollection是{0,0 1,1 …},Progress為100時PointCollection是{1,1 2,2 …},當Progress處於其中任何值時的計算方法則是:
private PointCollection GetCurrentPoints(PointCollection fromPoints, PointCollection toPoints, double percentage){var result = new PointCollection();for (var i = 0; i < Math.Min(fromPoints.Count, toPoints.Count); i++) { var x = (1 - percentage / 100d) * fromPoints[i].X + percentage / 100d * toPoints[i].X; var y = (1 - percentage / 100d) * fromPoints[i].Y + percentage / 100d * toPoints[i].Y; result.Add(new Point(x, y)); }return result;}
這樣就完成了從TimeSpan到PointCollection的轉換過程。然後就是定義在XAML上的使用方式。參考上面PointCollectionAnimation,雖然多了個Converter,但XAML也應該足夠簡潔:
<local:ProgressToPointCollectionBridge x:Name="ProgressToPointCollectionBridge"><PointCollection>97.3,0 127.4,60.9 194.6,70.7 145.9,118.1 157.4,185.1 97.3,153.5 37.2,185.1 48.6,118.1 0,70.7 67.2,60.9</PointCollection><PointCollection>110,58.2 147.3,0 192.1,29 141.7,105.1 118.7,139.8 88.8,185.1 46.1,156.5 0,125 23.5,86.6 71.1,116.7</PointCollection></local:ProgressToPointCollectionBridge><Storyboard x:Name="Storyboard1" FillBehavior="HoldEnd"><DoubleAnimation Duration="0:0:2" To="100" FillBehavior="HoldEnd" Storyboard.TargetProperty="(local:ProgressToPointCollectionBridge.Progress)" Storyboard.TargetName="ProgressToPointCollectionBridge" EnableDependentAnimation="True"/></Storyboard>…<Polygon x:Name="polygon" Points="{Binding Source={StaticResource ProgressToPointCollectionBridge},Path=Points}" Stroke="DarkOliveGreen" StrokeThickness="2" Height="250" Width="250" Stretch="Fill" />
最終我選擇了將這個Converter命名為ProgressToPointCollectionBridge
。可以看出Polygon 將Points綁定到ProgressToPointCollectionBridge,DoubleAnimation 改變ProgressToPointCollectionBridge.Progress,從而改變Points。XAML的簡潔程度還算令人滿意,如果需要操作多個點的話相對於PointAnimation的優勢就很大。
運行結果如下:
完整的XAML:
<UserControl.Resources><local:ProgressToPointCollectionBridge x:Name="ProgressToPointCollectionBridge"><PointCollection>97.3,0 127.4,60.9 194.6,70.7 145.9,118.1 157.4,185.1 97.3,153.5 37.2,185.1 48.6,118.1 0,70.7 67.2,60.9</PointCollection><PointCollection>110,58.2 147.3,0 192.1,29 141.7,105.1 118.7,139.8 88.8,185.1 46.1,156.5 0,125 23.5,86.6 71.1,116.7</PointCollection></local:ProgressToPointCollectionBridge><Storyboard x:Name="Storyboard1" FillBehavior="HoldEnd"><DoubleAnimation Duration="0:0:2" To="100" FillBehavior="HoldEnd" Storyboard.TargetProperty="(local:ProgressToPointCollectionBridge.Progress)" Storyboard.TargetName="ProgressToPointCollectionBridge" EnableDependentAnimation="True"><DoubleAnimation.EasingFunction><ElasticEase EasingMode="EaseInOut" /></DoubleAnimation.EasingFunction></DoubleAnimation><ColorAnimation Duration="0:0:2" To="#FF48F412" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" Storyboard.TargetName="polygon" d:IsOptimized="True"><ColorAnimation.EasingFunction><ElasticEase EasingMode="EaseInOut" /></ColorAnimation.EasingFunction></ColorAnimation></Storyboard></UserControl.Resources><Grid x:Name="LayoutRoot" Background="White"><Polygon x:Name="polygon" Points="{Binding Source={StaticResource ProgressToPointCollectionBridge},Path=Points}" Stroke="DarkOliveGreen" StrokeThickness="2" Height="250" Width="250" Stretch="Fill" Fill="#FFEBF412" /></Grid>
ProgressToPointCollectionBridge:
[ContentProperty(Name = nameof(Children))]public class ProgressToPointCollectionBridge : DependencyObject{public ProgressToPointCollectionBridge() { Children = new ObservableCollection<PointCollection>(); }/// <summary>/// 擷取或設定Points的值/// </summary>public PointCollection Points {get { return (PointCollection) GetValue(PointsProperty); }set { SetValue(PointsProperty, value); } }/// <summary>/// 擷取或設定Progress的值/// </summary>public double Progress {get { return (double) GetValue(ProgressProperty); }set { SetValue(ProgressProperty, value); } }/// <summary>/// 擷取或設定Children的值/// </summary>public Collection<PointCollection> Children {get { return (Collection<PointCollection>) GetValue(ChildrenProperty); }set { SetValue(ChildrenProperty, value); } }protected virtual void OnProgressChanged(double oldValue, double newValue) {UpdatePoints(); }protected virtual void OnChildrenChanged(Collection<PointCollection> oldValue, Collection<PointCollection> newValue) {var oldCollection = oldValue as INotifyCollectionChanged;if (oldCollection != null) oldCollection.CollectionChanged -= OnChildrenCollectionChanged;var newCollection = newValue as INotifyCollectionChanged;if (newCollection != null) newCollection.CollectionChanged += OnChildrenCollectionChanged;UpdatePoints(); }private void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {UpdatePoints(); }private void UpdatePoints() {if (Children == null || Children.Any() == false) { Points = null; }else if (Children.Count == 1) {var fromPoints = new PointCollection();for (var i = 0; i < Children[0].Count; i++) fromPoints.Add(new Point(0, 0));var toPoints = Children[0]; Points = GetCurrentPoints(fromPoints, toPoints, Progress); }else{var rangePerSection = 100d / (Children.Count - 1);var fromIndex = Math.Min(Children.Count - 2, Convert.ToInt32(Math.Floor(Progress / rangePerSection))); fromIndex = Math.Max(fromIndex, 0);var toIndex = fromIndex + 1; PointCollection fromPoints;if (fromIndex == toIndex) { fromPoints = new PointCollection();for (var i = 0; i < Children[0].Count; i++) fromPoints.Add(new Point(0, 0)); }else{ fromPoints = Children.ElementAt(fromIndex); }var toPoints = Children.ElementAt(toIndex); var percentage = (Progress / rangePerSection - fromIndex) * 100; Points = GetCurrentPoints(fromPoints, toPoints, percentage); } }private PointCollection GetCurrentPoints(PointCollection fromPoints, PointCollection toPoints, double percentage) {var result = new PointCollection();for (var i = 0; i < Math.Min(fromPoints.Count, toPoints.Count); i++) { var x = (1 - percentage / 100d) * fromPoints[i].X + percentage / 100d * toPoints[i].X; var y = (1 - percentage / 100d) * fromPoints[i].Y + percentage / 100d * toPoints[i].Y; result.Add(new Point(x, y)); }return result; }#region DependencyProperties#endregion}
3. 結語
如果將DoubleAnimation說成“對目標的Double屬性做動畫”,那PointAnimation可以說成“對目標的Point.X和Point.Y兩個Double屬性同時做動畫”,ColorAnimation則是“對目標的Color.A、R、G、B四個Int屬性同時做動畫”。這樣理解的話PointAnimation和ColorAnimation只不過是DoubleAnimation的延伸而已,進一步的說,通過DoubleAnimation應該可以延伸出所有類型屬性的動畫。不過我並不清楚怎麼在UWP上自訂動畫,只能通過本文的折衷方式擴充。雖然XAML需要寫複雜些,但這樣也有它的好處:
不需要瞭解太多Animation相關類的知識,只需要有相依性屬性、綁定等基礎知識就夠了。
不會因為動畫API的改變而更改,可以相容WPF、Silverlight和UWP(大概吧,我沒有真的在WPF上測試這些代碼)。
代碼足夠簡單,省去了計算TimeSpan及EasingFunction的步驟。 稍微修改下還可以做成泛型的AnimationBridge < T >
,提供PointCollection以外資料類型的支援。
結合上一篇文章再發散一下,總覺得將來遇到什麼UWP沒有提供的功能都可以通過變通的方法實現,Binding和DependencyProperty真是UWP開發人員最好的朋友。
4. 參考
How SVG Shape Morphing Works
Gadal MetaSyllabus