-
Notifications
You must be signed in to change notification settings - Fork 145
Expand file tree
/
Copy pathAnimationSet.cs
More file actions
250 lines (215 loc) · 10.2 KB
/
AnimationSet.cs
File metadata and controls
250 lines (215 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
#if WINUI2
using Windows.UI.Xaml.Media.Animation;
#else
using Microsoft.UI.Xaml.Media.Animation;
#endif
namespace CommunityToolkit.WinUI.Animations;
/// <summary>
/// A collection of animations that can be grouped together. This type represents a composite animation
/// (such as <see cref="Storyboard"/>) that can be executed on a given element.
/// </summary>
public sealed partial class AnimationSet : DependencyObjectCollection
{
/// <summary>
/// A conditional weak table storing <see cref="CancellationTokenSource"/> instances associated with animations
/// that have been started from the current set. This can be used to defer stopping running animations for any
/// target <see cref="UIElement"/> instance that originated from the current <see cref="AnimationSet"/>.
/// </summary>
private readonly ConditionalWeakTable<UIElement, CancellationTokenSource> cancellationTokenMap = new();
/// <summary>
/// Raised whenever the current animation is started.
/// </summary>
public event EventHandler? Started;
/// <summary>
/// Raised whenever the current animation completes.
/// </summary>
public event EventHandler? Completed;
/// <summary>
/// Gets or sets a value indicating whether top level animation nodes in this collection are invoked
/// sequentially. This applies to both <see cref="AnimationScope"/> nodes (which will still trigger
/// contained animations at the same time), and other top level animation nodes. The default value
/// is <see langword="false"/>, which means that all contained animations will start at the same time.
/// <para>
/// Note that this property will also cause a change in behavior for the animation. With the default
/// configuration, with all animations starting at the same time, it's not possible to use multiple
/// animations targeting the same property (as they'll cause a conflict and be ignored when on the
/// composition layer, or cause a crash when on the XAML layer). When animations are started sequentially
/// instead, each sequential block will be able to share target properties with animations from other
/// sequential blocks, without issues. Note that especially for simple scenarios (eg. an opacity animation
/// that just transitions to a state and then back, or between two states), it is recommended to use a single
/// keyframe animation instead, which will result in less overhead when creating and starting the animation.
/// </para>
/// </summary>
public bool IsSequential { get; set; }
/// <summary>
/// Gets or sets the weak reference to the parent that owns the current animation collection.
/// </summary>
internal WeakReference<UIElement>? ParentReference { get; set; }
/// <inheritdoc cref="AnimationBuilder.Start(UIElement)"/>
/// <exception cref="InvalidOperationException">Thrown when there is no attached <see cref="UIElement"/> instance.</exception>
public async void Start()
{
// Here we're using an async void method on purpose, in order to be able to await
// the completion of the animation and rethrow exceptions. We can't just use the
// synchronous AnimationBuilder.Start method here, as we also need to await for the
// animation to complete in either case in order to raise the Completed event when that
// happens. So we add an async state machine here to work around this.
await StartAsync();
}
/// <inheritdoc cref="AnimationBuilder.Start(UIElement)"/>
public async void Start(UIElement? element)
{
await StartAsync(element!);
}
/// <inheritdoc cref="AnimationBuilder.Start(UIElement)"/>
/// <exception cref="InvalidOperationException">Thrown when there is no attached <see cref="UIElement"/> instance.</exception>
public async void Start(CancellationToken token)
{
await StartAsync(token);
}
/// <inheritdoc cref="AnimationBuilder.Start(UIElement)"/>
/// <exception cref="InvalidOperationException">Thrown when there is no attached <see cref="UIElement"/> instance.</exception>
public Task StartAsync()
{
return StartAsync(GetParent());
}
/// <inheritdoc cref="AnimationBuilder.Start(UIElement)"/>
public Task StartAsync(UIElement element)
{
Stop(element);
CancellationTokenSource cancellationTokenSource = new();
#if !NETSTANDARD2_0
this.cancellationTokenMap.AddOrUpdate(element, cancellationTokenSource);
#else
// If we have a token, remove it first, before adding new one.
if (this.cancellationTokenMap.TryGetValue(element, out _))
{
this.cancellationTokenMap.Remove(element);
}
this.cancellationTokenMap.Add(element, cancellationTokenSource);
#endif
return StartAsync(element, cancellationTokenSource.Token);
}
/// <inheritdoc cref="AnimationBuilder.Start(UIElement)"/>
/// <exception cref="InvalidOperationException">Thrown when there is no attached <see cref="UIElement"/> instance.</exception>
public Task StartAsync(CancellationToken token)
{
return StartAsync(GetParent(), token);
}
/// <inheritdoc cref="AnimationBuilder.Start(UIElement)"/>
public async Task StartAsync(UIElement element, CancellationToken token)
{
Started?.Invoke(this, EventArgs.Empty);
if (IsSequential)
{
foreach (object node in this)
{
if (node is IAttachedTimeline attachedTimeline)
{
var builder = AnimationBuilder.Create();
attachedTimeline.AppendToBuilder(builder, element);
await builder.StartAsync(element, token);
}
else if (node is ITimeline timeline)
{
var builder = AnimationBuilder.Create();
timeline.AppendToBuilder(builder);
await builder.StartAsync(element, token);
}
else if (node is IActivity activity)
{
try
{
// Unlike with animations, activities can potentially throw if they execute
// an await operation on a task that was linked to a cancellation token. For
// instance, this is the case for the await operation for the initial delay,
// and the same can apply to 3rd party activities that would just integrate
// the input token into their logic. We can just catch these exceptions and
// stop the sequential execution immediately from the handler.
await activity.InvokeAsync(element, token);
}
catch (OperationCanceledException)
{
break;
}
}
else
{
ThrowArgumentException();
}
// This should in theory only be necessary in the timeline branch, but doing this check
// after running activities too help guard against 3rd party activities that might not
// properly monitor the token being in use, and still run fine after a cancellation.
if (token.IsCancellationRequested)
{
break;
}
}
}
else
{
var builder = AnimationBuilder.Create();
foreach (object node in this)
{
switch (node)
{
case IAttachedTimeline attachedTimeline:
builder = attachedTimeline.AppendToBuilder(builder, element);
break;
case ITimeline timeline:
builder = timeline.AppendToBuilder(builder);
break;
case IActivity activity:
_ = activity.InvokeAsync(element, token);
break;
default:
ThrowArgumentException();
break;
}
}
await builder.StartAsync(element, token);
}
Completed?.Invoke(this, EventArgs.Empty);
static void ThrowArgumentException() => throw new ArgumentException($"An animation set can only contain nodes implementing either ITimeline or IActivity");
}
/// <summary>
/// Cancels the current animation on the attached <see cref="UIElement"/> instance.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when there is no attached <see cref="UIElement"/> instance.</exception>
public void Stop()
{
Stop(GetParent());
}
/// <summary>
/// Cancels the current animation for a target <see cref="UIElement"/> instance.
/// </summary>
/// <param name="element">The target <see cref="UIElement"/> instance to stop the animation for.</param>
public void Stop(UIElement? element)
{
if (this.cancellationTokenMap.TryGetValue(element!, out CancellationTokenSource? value))
{
value.Cancel();
}
}
/// <summary>
/// Gets the current parent <see cref="UIElement"/> instance.
/// </summary>
/// <returns>The <see cref="UIElement"/> reference from <see cref="ParentReference"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown if there is no parent available.</exception>
[Pure]
private UIElement GetParent()
{
UIElement? parent = null;
if (ParentReference?.TryGetTarget(out parent) != true)
{
ThrowInvalidOperationException();
}
return parent!;
static void ThrowInvalidOperationException() => throw new InvalidOperationException("The current AnimationSet object isn't bound to a parent UIElement instance.");
}
}