Skip to content

Commit e859b10

Browse files
committed
Added AnimationBuilder.Start(UIElement, Action) overload
1 parent 385e4fb commit e859b10

File tree

1 file changed

+148
-0
lines changed

1 file changed

+148
-0
lines changed

Microsoft.Toolkit.Uwp.UI.Animations/Builders/AnimationBuilder.cs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
#nullable enable
66

7+
using System;
78
using System.Collections.Generic;
89
using System.Diagnostics.Contracts;
10+
using System.Runtime.CompilerServices;
911
using System.Threading;
1012
using System.Threading.Tasks;
1113
using Windows.UI.Composition;
@@ -98,6 +100,152 @@ public void Start(UIElement element)
98100
}
99101
}
100102

103+
/// <summary>
104+
/// Starts the animations present in the current <see cref="AnimationBuilder"/> instance.
105+
/// </summary>
106+
/// <param name="element">The target <see cref="UIElement"/> to animate.</param>
107+
/// <param name="callback">The callback to invoke when the animation completes.</param>
108+
public void Start(UIElement element, Action callback)
109+
{
110+
// The point of this overload is to allow consumers to invoke a callback when an animation
111+
// completes, without having to create an async state machine. There are three different possible
112+
// scenarios to handle, and each can have a specialized code path to ensure the implementation
113+
// is as lean and efficient as possible. Specifically, for a given AnimationBuilder instance:
114+
// 1) There are only Composition animations
115+
// 2) There are only XAML animations
116+
// 3) There are both Composition and XAML animations
117+
// The implementation details of each of these paths is described below.
118+
if (this.compositionAnimationFactories.Count > 0)
119+
{
120+
if (this.xamlAnimationFactories.Count == 0)
121+
{
122+
// There are only Composition animations. In this case we can just use a Composition scoped batch,
123+
// capture the user-provided callback and invoke it directly when the batch completes. There is no
124+
// additional overhead here, since we would've had to create a closure regardless to be able to monitor
125+
// the completion of the animation (eg. to capture a TaskCompletionSource like we're doing below).
126+
static void Start(AnimationBuilder builder, UIElement element, Action callback)
127+
{
128+
ElementCompositionPreview.SetIsTranslationEnabled(element, true);
129+
130+
Visual visual = ElementCompositionPreview.GetElementVisual(element);
131+
CompositionScopedBatch batch = visual.Compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
132+
133+
batch.Completed += (_, _) => callback();
134+
135+
foreach (var factory in builder.compositionAnimationFactories)
136+
{
137+
var animation = factory.GetAnimation(visual, out var target);
138+
139+
if (target is null)
140+
{
141+
visual.StartAnimation(animation.Target, animation);
142+
}
143+
else
144+
{
145+
target.StartAnimation(animation.Target, animation);
146+
}
147+
}
148+
149+
batch.End();
150+
}
151+
152+
Start(this, element, callback);
153+
}
154+
else
155+
{
156+
// In this case we need to wait for both the Composition and XAML animation groups to complete. These two
157+
// groups use different APIs and can have a different duration, so we need to synchronize between them
158+
// without creating an async state machine (as that'd defeat the point of this separate overload).
159+
//
160+
// The code below relies on a mutable boxed counter that's shared across the two closures for the Completed
161+
// events for both the Composition scoped batch and the XAML Storyboard. The counter is initialized to 2, and
162+
// when each group completes, the counter is decremented (we don't need an interlocked decrement as the delegates
163+
// will already be invoked on the current DispatcherQueue instance, which acts as the synchronization context here.
164+
// The handlers for the Composition batch and the Storyboard will never execute concurrently). If the counter has
165+
// reached zero, it means that both groups have completed, so the user-provided callback is triggered, otherwise
166+
// the handler just does nothing. This ensures that the callback is executed exactly once when all the animation
167+
// complete, but without the need to create TaskCompletionSource-s and an async state machine to await for that.
168+
//
169+
// Note: we're using StrongBox<T> here because that exposes a mutable field of the type we need (int).
170+
// We can't just mutate a boxed int in-place with Unsafe.Unbox<T> as that's against the ECMA spec, since
171+
// that API uses the unbox IL opcode (§III.4.32) which returns a "controlled-mutability managed pointer"
172+
// (§III.1.8.1.2.2), which is not "verifier-assignable-to" (ie. directly assigning to it is not legal).
173+
static void Start(AnimationBuilder builder, UIElement element, Action callback)
174+
{
175+
StrongBox<int> counter = new(2);
176+
177+
ElementCompositionPreview.SetIsTranslationEnabled(element, true);
178+
179+
Visual visual = ElementCompositionPreview.GetElementVisual(element);
180+
CompositionScopedBatch batch = visual.Compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
181+
182+
batch.Completed += (_, _) =>
183+
{
184+
if (--counter.Value == 0)
185+
{
186+
callback();
187+
}
188+
};
189+
190+
foreach (var factory in builder.compositionAnimationFactories)
191+
{
192+
var animation = factory.GetAnimation(visual, out var target);
193+
194+
if (target is null)
195+
{
196+
visual.StartAnimation(animation.Target, animation);
197+
}
198+
else
199+
{
200+
target.StartAnimation(animation.Target, animation);
201+
}
202+
}
203+
204+
batch.End();
205+
206+
Storyboard storyboard = new();
207+
208+
foreach (var factory in builder.xamlAnimationFactories)
209+
{
210+
storyboard.Children.Add(factory.GetAnimation(element));
211+
}
212+
213+
storyboard.Completed += (_, _) =>
214+
{
215+
if (--counter.Value == 0)
216+
{
217+
callback();
218+
}
219+
};
220+
storyboard.Begin();
221+
}
222+
223+
Start(this, element, callback);
224+
}
225+
}
226+
else
227+
{
228+
// There are only XAML animations. This case is extremely similar to that where we only have Composition
229+
// animations, with the main difference being that the Completed event is directly exposed from the
230+
// Storyboard type, so we don't need a separate type to track the animation completion. The same
231+
// considerations regarding the closure to capture the provided callback apply here as well.
232+
static void Start(AnimationBuilder builder, UIElement element, Action callback)
233+
{
234+
Storyboard storyboard = new();
235+
236+
foreach (var factory in builder.xamlAnimationFactories)
237+
{
238+
storyboard.Children.Add(factory.GetAnimation(element));
239+
}
240+
241+
storyboard.Completed += (_, _) => callback();
242+
storyboard.Begin();
243+
}
244+
245+
Start(this, element, callback);
246+
}
247+
}
248+
101249
/// <summary>
102250
/// Starts the animations present in the current <see cref="AnimationBuilder"/> instance, and
103251
/// registers a given cancellation token to stop running animations before they complete.

0 commit comments

Comments
 (0)