|
4 | 4 |
|
5 | 5 | #nullable enable
|
6 | 6 |
|
| 7 | +using System; |
7 | 8 | using System.Collections.Generic;
|
8 | 9 | using System.Diagnostics.Contracts;
|
| 10 | +using System.Runtime.CompilerServices; |
9 | 11 | using System.Threading;
|
10 | 12 | using System.Threading.Tasks;
|
11 | 13 | using Windows.UI.Composition;
|
@@ -98,6 +100,152 @@ public void Start(UIElement element)
|
98 | 100 | }
|
99 | 101 | }
|
100 | 102 |
|
| 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 | + |
101 | 249 | /// <summary>
|
102 | 250 | /// Starts the animations present in the current <see cref="AnimationBuilder"/> instance, and
|
103 | 251 | /// registers a given cancellation token to stop running animations before they complete.
|
|
0 commit comments