| 
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