diff --git a/README.md b/README.md index 45e199e..5cdd86c 100644 --- a/README.md +++ b/README.md @@ -69,17 +69,9 @@ await MyControl.StartCompositionFadeScaleAnimationAsync( EasingFunctionNames.Linear); // Easing function ``` -#### ColorBrush animation -```C# -MyBrush.AnimateColor( - #FFFF2B1C, // Target color - 250, // Duration in ms - EasingFunctionNames.Linear); // Easing function -``` - ## `UI.Composition` effects -The library provides several ways to use `UI.Composition` effects. There's a custom acrylic brush that can be used when running Windows 10 build 15063.x or greater, and other "attached" effects. An attached effect (created using the `AttachedCompositionEffectsFactory` class) is an effect that is loaded and then applied to the underlying `Visual` object behing a target `UIElement`. The main advantage of brushes is that they can be initialized and used in XAML and don't need any code-behind. Here are some examples: +The library provides several ways to use `UI.Composition` effects. There are ready to use XAML brushes, a `CompositionBrushBuilder` class to create complex composition effect pipelines, an `AttachedCompositionEffectsFactory` class that provides an alternative way to attach commonly used effects to visual elements, and much more. #### Declare a shared acrylic brush in XAML @@ -91,7 +83,7 @@ The library provides several ways to use `UI.Composition` effects. There's a cus ``` -**Note**: the `NoiseTextureUri` parameter must be set to a .png image with a noise texture. It is up to the developer to create his own noise texture and to import it into the app. An easy plugin to create a custom noise texture is [NoiseChoice](https://forums.getpaint.net/topic/22500-red-ochre-plug-in-pack-v9-updated-30th-july-2014/) for [Paint.NET](https://www.getpaint.net/). +**Note**: the `NoiseTextureUri` parameter must be set to a .png image with a noise texture. It is up to the developer to create his own noise texture and to import it into the app. An easy plugin to create one is [NoiseChoice](https://forums.getpaint.net/topic/22500-red-ochre-plug-in-pack-v9-updated-30th-july-2014/) for [Paint.NET](https://www.getpaint.net/). + +#### Create and assign an acrylic brush in C# +```C# +control.Background = CompositionBrushBuilder.FromHostBackdropAcrylic(Colors.DarkOrange, 0.6f, new Uri("ms-appx:///Assets/noise.png")).AsBrush(); +``` -#### Get a custom acrylic brush effect: +#### Build an acrylic effect pipeline from scratch: ```C# -AttachedStaticCompositionEffect attached = await BlurBorder.AttachCompositionInAppCustomAcrylicEffectAsync( - BlurBorder, // The target host control for the effect visual (can be the same as the source) - 8, // The amount of blur to apply - 800, // The milliseconds to initially apply the blur effect with an automatic animation - Color.FromArgb(byte.MaxValue, 0x1B, 0x1B, 0x1B), // The tint overlay color - 0.8f, // The ratio of tint overlay over the source effect (the strength of the tint effect) - null, // Use the default saturation value for the effect (1) - Win2DCanvas, // A CanvasControl in the current visual tree, used to render parts of the acrylic brush - new Uri("ms-appx:///Assets/Misc/noise.png"), // A Uri to a custom noise texture to use to create the effect - BitmapCacheMode.EnableCaching, // The cache mode for the Win2D image to load - false, // Indicates whether to fade the effect it or to display it as soon as possible - true); // Indicates whether or not to automatically dispose the effect when the target `UIElement` is unloaded +Brush brush = CompositionBrushBuilder.FromHostBackdropBrush() + .Effect(source => new LuminanceToAlphaEffect { Source = source }) + .Opacity(0.4f) + .Blend(CompositionBrushBuilder.FromHostBackdropBrush(), BlendEffectMode.Multiply) + .Tint(Color.FromArgb(0xFF, 0x14, 0x14, 0x14), 0.8f) + .Blend(CompositionBrushBuilder.FromTiles(new Uri("ms-appx:///Assets/noise.png")), BlendEffectMode.Overlay, EffectPlacement.Background) + .AsBrush(); ``` -**Note**: in order to remove the effect from the target `UIElement`, it is possible to call the `Dispose` method on the returned `AttachedStaticCompositionEffect` object - calling that method will remove the effect from the object `Visual`. +The `CompositionBrushBuilder` class can also be used to quickly implement custom XAML brushes with an arbitrary effects pipeline. To do so, just inherit from `XamlCompositionEffectBrushBase` and setup your own effects pipeline in the `OnBrushRequested` method. -#### Get an attached blur effect that can be animated (using composition and Win2D effects): +#### Get a custom effect that can be animated: ```C# -AttachedAnimatableCompositionEffect attached = await MyBorder.AttachCompositionAnimatableBlurEffectAsync( - 14f, // The amount of blur to apply when the effect is enabled - 0f, // The default amount of blur - false); // Indicates whether or not to immediately apply the effect to the target amount +// Build the effects pipeline +XamlCompositionBrush acrylic = CompositionBrushBuilder.FromHostBackdropAcrylic(Colors.Orange, 0.6f, new Uri("ms-appx:///Assets/noise.png")) + .Saturation(1, out EffectAnimation animation) + .AsBrush(); +acrylic.Bind(animation, out XamlEffectAnimation saturation); // Bind the effect animation to the target brush // Later on, when needed -await attached.AnimateAsync( - FixedAnimationType.In, // Indicates whether to fade the blur effect in or out - TimeSpan.FromMilliseconds(500)); // The animation duration +saturation(0.2f, 250); // Animate the opacity to 0.2 in 250ms ``` ## Reveal highlight effect @@ -182,8 +173,7 @@ Many utility methods are also available, here are some useful classes: - `XAMLTransformToolkit`: exposes methods to manually create, start and wait for `DoubleAnimation`(s) and `Storyboard`(s), as well as for quickly assigning a certain `RenderTransform` object to a `UIElement`. - `DispatcherHelper`: exposes methods to easily execute code on the UI thread or on a target `CoreDispatcher` object - `Win2DImageHelper`: exposes APIs to quickly load a Win2D image on a `CompositionSurfaceBrush` object -- `ApiInformationHelper`: provides useful methods to check the capabilities of the current device - `PointerHelper`: exposes APIs to quickly setup pointer event handlers for `UIElement`s # Requirements -At least Windows 10 November Update (10586.x) +At least Windows 10 April Update (17134.x) diff --git a/UICompositionAnimations/Behaviours/AcrylicEffectHelper.cs b/UICompositionAnimations/Behaviours/AcrylicEffectHelper.cs index b007dce..f0f9ccb 100644 --- a/UICompositionAnimations/Behaviours/AcrylicEffectHelper.cs +++ b/UICompositionAnimations/Behaviours/AcrylicEffectHelper.cs @@ -28,15 +28,14 @@ internal static class AcrylicEffectHelper /// The amount of tint color to apply /// The optional to use to generate the image for the /// The path to the source image to use for the - /// Indicates whether or not to force the reload of the Win2D image /// The resulting effect through the pipeline /// The method does side effect on the variable [MustUseReturnValue, ItemNotNull] public static async Task ConcatenateEffectWithTintAndBorderAsync( [NotNull] Compositor compositor, - [NotNull] IGraphicsEffectSource source, [NotNull] IDictionary parameters, + [NotNull] IGraphicsEffectSource source, [NotNull] IDictionary parameters, Color color, float colorMix, - [CanBeNull] CanvasControl canvas, [NotNull] Uri uri, BitmapCacheMode options) + [CanBeNull] CanvasControl canvas, [NotNull] Uri uri) { // Setup the tint effect ArithmeticCompositeEffect tint = new ArithmeticCompositeEffect @@ -50,8 +49,8 @@ internal static class AcrylicEffectHelper // Get the noise brush using Win2D CompositionSurfaceBrush noiseBitmap = canvas == null - ? await Win2DImageHelper.LoadImageAsync(compositor, uri, options, BitmapDPIMode.CopyDisplayDPISettingsWith96AsLowerBound) - : await Win2DImageHelper.LoadImageAsync(compositor, canvas, uri, options, BitmapDPIMode.CopyDisplayDPISettingsWith96AsLowerBound); + ? await Win2DImageHelper.LoadImageAsync(compositor, uri, BitmapDPIMode.CopyDisplayDPISettingsWith96AsLowerBound, BitmapCacheMode.Default) + : await Win2DImageHelper.LoadImageAsync(compositor, canvas, uri, BitmapDPIMode.CopyDisplayDPISettingsWith96AsLowerBound, BitmapCacheMode.Default); // Make sure the Win2D brush was loaded correctly if (noiseBitmap != null) @@ -82,17 +81,16 @@ internal static class AcrylicEffectHelper /// A dictionary to use to keep track of reference parameters to add when creating a /// The tint color /// The path to the source image to use for the - /// Indicates whether or not to force the reload of the Win2D image /// The resulting effect through the pipeline /// The method does side effect on the variable [MustUseReturnValue, ItemNotNull] public static async Task LoadTextureEffectWithTintAsync( - [NotNull] Compositor compositor, [NotNull] IDictionary parameters, - Color color, [NotNull] Uri uri, BitmapCacheMode options) + [NotNull] Compositor compositor, [NotNull] IDictionary parameters, + Color color, [NotNull] Uri uri) { // Initial setup ColorSourceEffect colorEffect = new ColorSourceEffect { Color = color }; - CompositionSurfaceBrush noiseBitmap = await Win2DImageHelper.LoadImageAsync(compositor, uri, options, BitmapDPIMode.CopyDisplayDPISettingsWith96AsLowerBound); + CompositionSurfaceBrush noiseBitmap = await Win2DImageHelper.LoadImageAsync(compositor, uri, BitmapDPIMode.CopyDisplayDPISettingsWith96AsLowerBound, BitmapCacheMode.Default); if (noiseBitmap == null) return colorEffect; // Blend the effects @@ -112,4 +110,4 @@ internal static class AcrylicEffectHelper return blendEffect; } } -} +} \ No newline at end of file diff --git a/UICompositionAnimations/Behaviours/AttachedCompositionEffectFactory.cs b/UICompositionAnimations/Behaviours/AttachedCompositionEffectFactory.cs deleted file mode 100644 index 548734c..0000000 --- a/UICompositionAnimations/Behaviours/AttachedCompositionEffectFactory.cs +++ /dev/null @@ -1,740 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using System.Threading.Tasks; -using Windows.Graphics.Effects; -using Windows.UI; -using Windows.UI.Composition; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Hosting; -using JetBrains.Annotations; -using Microsoft.Graphics.Canvas.Effects; -using Microsoft.Graphics.Canvas.UI.Xaml; -using UICompositionAnimations.Behaviours.Effects; -using UICompositionAnimations.Behaviours.Misc; -using UICompositionAnimations.Composition; -using UICompositionAnimations.Enums; -using UICompositionAnimations.Helpers; - -namespace UICompositionAnimations.Behaviours -{ - /// - /// A static class that manages the creation of attached composition effects - /// - - public static class AttachedCompositionEffectsFactory - { - #region Static effects - - /// - /// Creates a new instance for the target element - /// - /// The type of element to blur - /// The target element - /// The amount of blur to apply to the element - /// The duration of the initial blur animation, in milliseconds - /// Indicates whether or not to automatically dispose and remove the effect when the target element is unloaded - /// This method returns a instance and runs synchronously if called on the UI thread - [ItemNotNull] - public static async ValueTask> AttachCompositionBlurEffect( - [NotNull] this T element, float blur, int ms, bool disposeOnUnload = false) where T : FrameworkElement - { - // Get the visual and the compositor - Visual visual = element.GetVisual(); - Compositor compositor = visual.Compositor; - - // Create the blur effect and the effect factory - GaussianBlurEffect blurEffect = new GaussianBlurEffect - { - Name = "Blur", - BlurAmount = 0f, - BorderMode = EffectBorderMode.Hard, - Optimization = EffectOptimization.Balanced, - Source = new CompositionEffectSourceParameter("source") - }; - CompositionEffectFactory effectFactory = compositor.CreateEffectFactory(blurEffect, new[] { "Blur.BlurAmount" }); - - // Setup the rest of the effect - CompositionEffectBrush effectBrush = effectFactory.CreateBrush(); - effectBrush.SetSourceParameter("source", compositor.CreateBackdropBrush()); - - // Assign the effect to a brush and display it - SpriteVisual sprite = compositor.CreateSpriteVisual(); - sprite.Brush = effectBrush; - sprite.Size = new Vector2((float)element.ActualWidth, (float)element.ActualHeight); - await AddToTreeAndBindSizeAsync(visual, element, sprite); - - // Animate the blur amount - effectBrush.StartAnimationAsync("Blur.BlurAmount", blur, TimeSpan.FromMilliseconds(ms)).Forget(); - - // Prepare and return the manager - return new AttachedStaticCompositionEffect(element, sprite, disposeOnUnload); - } - - /// - /// Creates an effect brush that's similar to the official Acrylic brush in the Fall Creator's Update. - /// The pipeline uses the following effects: BackdropBrush > > - /// > with customizable blend factors for each couple of layers - /// - /// The type of the element that will be the source for the composition effect - /// The type of the target element that will host the resulting - /// The that will be the source of the effect - /// The target host for the resulting effect - /// The amount of blur to apply to the element - /// The duration of the initial blur animation, in milliseconds - /// The tint color for the effect - /// The opacity of the color over the blurred background - /// An optional parameter to set the overall saturation of the effect (if null, it will default to 1) - /// The optional source to generate the noise image using Win2D - /// The path of the noise image to use - /// Indicates whether or not to force the reload of the Win2D image - /// Indicates whether or not to fade the effect in - /// Indicates whether or not to automatically dispose and remove the effect when the target element is unloaded - [ItemNotNull] - public static async Task> AttachCompositionInAppCustomAcrylicEffectAsync( - [NotNull] this TSource element, [NotNull] T target, float blur, int ms, Color color, float colorMix, float? saturation, - [CanBeNull] CanvasControl canvas, [NotNull] Uri uri, - BitmapCacheMode options = BitmapCacheMode.EnableCaching, bool fadeIn = false, bool disposeOnUnload = false) - where TSource : FrameworkElement - where T : FrameworkElement - { - // Percentage check - if (saturation < 0 || saturation > 1) throw new ArgumentOutOfRangeException("The input saturation value must be in the [0,1] range"); - if (colorMix <= 0 || colorMix >= 1) throw new ArgumentOutOfRangeException("The mix factors must be in the [0,1] range"); - - // Setup the compositor - Visual visual = ElementCompositionPreview.GetElementVisual(element); - Compositor compositor = visual.Compositor; - - // Prepare a luminosity to alpha effect to adjust the background contrast - CompositionBackdropBrush backdropBrush = compositor.CreateBackdropBrush(); - const String - blurName = "Blur", - blurParameterName = "Blur.BlurAmount"; - GaussianBlurEffect blurEffect = new GaussianBlurEffect - { - Name = blurName, - BlurAmount = 0f, - BorderMode = EffectBorderMode.Hard, - Optimization = EffectOptimization.Balanced, - Source = new CompositionEffectSourceParameter(nameof(backdropBrush)) - }; - - // Background with blur and tint overlay - IDictionary sourceParameters = new Dictionary - { - { nameof(backdropBrush), backdropBrush } - }; - - // Get the noise brush using Win2D - IGraphicsEffect source = await AcrylicEffectHelper.ConcatenateEffectWithTintAndBorderAsync(compositor, - blurEffect, sourceParameters, color, colorMix, canvas, uri, options); - - // Add the final saturation effect if needed - if (saturation != null) - { - SaturationEffect saturationEffect = new SaturationEffect - { - Saturation = saturation.Value, - Source = source - }; - source = saturationEffect; - } - - // Make sure the Win2D brush was loaded correctly - CompositionEffectFactory factory = compositor.CreateEffectFactory(source, new[] { blurParameterName }); - - // Create the effect factory and apply the final effect - CompositionEffectBrush effectBrush = factory.CreateBrush(); - foreach (KeyValuePair pair in sourceParameters) - { - effectBrush.SetSourceParameter(pair.Key, pair.Value); - } - - // Create the sprite to display and add it to the visual tree - SpriteVisual sprite = compositor.CreateSpriteVisual(); - sprite.Brush = effectBrush; - - // Assign the visual - if (fadeIn) - { - sprite.StopAnimation("Opacity"); - sprite.Opacity = 0; - } - await AddToTreeAndBindSizeAsync(target.GetVisual(), target, sprite); - if (fadeIn) - { - // Fade the effect in - ScalarKeyFrameAnimation opacityAnimation = sprite.Compositor.CreateScalarKeyFrameAnimation(0, - 1, TimeSpan.FromMilliseconds(ms), null, sprite.GetEasingFunction(EasingFunctionNames.Linear)); - sprite.StartAnimation("Opacity", opacityAnimation); - } - - // Animate the blur and return the result - effectBrush.StartAnimationAsync(blurParameterName, blur, TimeSpan.FromMilliseconds(ms)).Forget(); - return new AttachedStaticCompositionEffect(target, sprite, disposeOnUnload); - } - - /// - /// Creates a new instance for the target element - /// - /// The type of element to use to host the effect - /// The target element - /// Indicates whether or not to automatically dispose and remove the effect when the target element is unloaded - /// This method returns a instance and runs synchronously if called on the UI thread - [ItemNotNull] - public static async ValueTask> AttachCompositionHostBackdropBlurEffect( - [NotNull] this T element, bool disposeOnUnload = false) where T : FrameworkElement - { - // Setup the host backdrop effect - Visual visual = ElementCompositionPreview.GetElementVisual(element); - Compositor compositor = visual.Compositor; - CompositionBackdropBrush brush = compositor.CreateHostBackdropBrush(); - SpriteVisual sprite = compositor.CreateSpriteVisual(); - sprite.Brush = brush; - await AddToTreeAndBindSizeAsync(visual, element, sprite); - return new AttachedStaticCompositionEffect(element, sprite, disposeOnUnload); - } - - /// - /// Creates an effect brush that's similar to the official Acrylic brush in the Fall Creator's Update. - /// The pipeline uses the following effects: HostBackdropBrush > > - /// > > > - /// > with customizable blend factors for each couple of layers - /// - /// The type of the target element that will host the resulting - /// The target element that will host the effect - /// The tint color for the effect - /// The opacity of the color over the blurred background - /// The optional source to generate the noise image using Win2D - /// The path of the noise image to use - /// Indicates whether or not to force the reload of the Win2D image - /// Indicates whether or not to automatically dispose and remove the effect when the target element is unloaded - [ItemNotNull] - public static async Task> AttachCompositionCustomAcrylicEffectAsync( - [NotNull] this T element, Color color, float colorMix, - [CanBeNull] CanvasControl canvas, [NotNull] Uri uri, BitmapCacheMode options = BitmapCacheMode.EnableCaching, bool disposeOnUnload = false) - where T : FrameworkElement - { - // Percentage check - if (colorMix <= 0 || colorMix >= 1) throw new ArgumentOutOfRangeException("The mix factors must be in the [0,1] range"); - - // Setup the compositor - Visual visual = ElementCompositionPreview.GetElementVisual(element); - Compositor compositor = visual.Compositor; - - // Prepare a luminosity to alpha effect to adjust the background contrast - CompositionBackdropBrush hostBackdropBrush = compositor.CreateHostBackdropBrush(); - CompositionEffectSourceParameter backgroundParameter = new CompositionEffectSourceParameter(nameof(hostBackdropBrush)); - LuminanceToAlphaEffect alphaEffect = new LuminanceToAlphaEffect { Source = backgroundParameter }; - OpacityEffect opacityEffect = new OpacityEffect - { - Source = alphaEffect, - Opacity = 0.4f // Reduce the amount of the effect to avoid making bright areas completely black - }; - - // Layer [0,1,3] - Desktop background with blur and tint overlay - BlendEffect alphaBlend = new BlendEffect - { - Background = backgroundParameter, - Foreground = opacityEffect, - Mode = BlendEffectMode.Overlay - }; - IDictionary sourceParameters = new Dictionary - { - { nameof(hostBackdropBrush), hostBackdropBrush } - }; - - // Get the noise brush using Win2D - IGraphicsEffect source = await AcrylicEffectHelper.ConcatenateEffectWithTintAndBorderAsync(compositor, - alphaBlend, sourceParameters, color, colorMix, canvas, uri, options); - - // Make sure the Win2D brush was loaded correctly - CompositionEffectFactory factory = compositor.CreateEffectFactory(source); - - // Create the effect factory and apply the final effect - CompositionEffectBrush effectBrush = factory.CreateBrush(); - foreach (KeyValuePair pair in sourceParameters) - { - effectBrush.SetSourceParameter(pair.Key, pair.Value); - } - - // Create the sprite to display and add it to the visual tree - SpriteVisual sprite = compositor.CreateSpriteVisual(); - sprite.Brush = effectBrush; - await AddToTreeAndBindSizeAsync(visual, element, sprite); - return new AttachedStaticCompositionEffect(element, sprite, disposeOnUnload); - } - - /// - /// Creates an effect brush that's similar to the official Acrylic brush in the Fall Creator's Update and can be toggled - /// between the host backdrop blur effect and the in-app acrylic brush effect. - /// The pipeline uses the following effects: HostBackdropBrush > > - /// > > > - /// > with customizable blend factors for each couple of layers - /// - /// The type of the target element that will host the resulting - /// The target element that will host the effect - /// The tint color for the effect - /// The opacity of the color over the in-app blurred contents - /// The opacity of the color over the blurred background - /// Indicates the initial mode for the custom effect - /// The amount of blur to apply to the element - /// The duration of the initial blur animation, in milliseconds - /// The optional source to generate the noise image using Win2D - /// The path of the noise image to use - /// Indicates whether or not to force the reload of the Win2D image - /// Indicates whether or not to automatically dispose and remove the effect when the target element is unloaded - [ItemNotNull] - public static async Task> AttachCompositionCustomAcrylicToggleEffectAsync( - [NotNull] this T element, Color color, float inAppColorMix, float hostColorMix, - AcrylicEffectMode mode, float blur, int ms, - [CanBeNull] CanvasControl canvas, [NotNull] Uri uri, BitmapCacheMode options = BitmapCacheMode.EnableCaching, bool disposeOnUnload = false) - where T : FrameworkElement - { - // Percentage check - if (hostColorMix <= 0 || hostColorMix >= 1 || - inAppColorMix <= 0 || inAppColorMix >= 1) throw new ArgumentOutOfRangeException("The mix factors must be in the [0,1] range"); - - // Setup the compositor - Visual visual = ElementCompositionPreview.GetElementVisual(element); - Compositor compositor = visual.Compositor; - - // Prepare a luminosity to alpha effect to adjust the background contrast - CompositionBackdropBrush hostBackdropBrush = compositor.CreateHostBackdropBrush(); - CompositionEffectSourceParameter backgroundParameter = new CompositionEffectSourceParameter(nameof(hostBackdropBrush)); - LuminanceToAlphaEffect alphaEffect = new LuminanceToAlphaEffect { Source = backgroundParameter }; - OpacityEffect opacityEffect = new OpacityEffect - { - Source = alphaEffect, - Opacity = 0.4f // Reduce the amount of the effect to avoid making bright areas completely black - }; - - // Layer [0,1,3] - Desktop background with blur and tint overlay - BlendEffect alphaBlend = new BlendEffect - { - Background = backgroundParameter, - Foreground = opacityEffect, - Mode = BlendEffectMode.Overlay - }; - - // In-app backdrop effect - CompositionBackdropBrush backdropBrush = compositor.CreateBackdropBrush(); - const String - blurName = "Blur", - blurParameterName = "Blur.BlurAmount"; - GaussianBlurEffect blurEffect = new GaussianBlurEffect - { - Name = blurName, - BlurAmount = 0f, - BorderMode = EffectBorderMode.Hard, - Optimization = EffectOptimization.Balanced, - Source = new CompositionEffectSourceParameter(nameof(backdropBrush)) - }; - - // Setup the switch effect - ArithmeticCompositeEffect switchEffect = new ArithmeticCompositeEffect - { - Name = "Switch", - MultiplyAmount = 0, - Source1Amount = mode == AcrylicEffectMode.InAppBlur ? 1 : 0, - Source2Amount = mode == AcrylicEffectMode.InAppBlur ? 0 : 1, - Source1 = blurEffect, - Source2 = alphaBlend - }; - - // Get the tint and noise brushes using Win2D - IDictionary sourceParameters = new Dictionary - { - { nameof(hostBackdropBrush), hostBackdropBrush }, - { nameof(backdropBrush), backdropBrush } - }; - IGraphicsEffect source = await AcrylicEffectHelper.ConcatenateEffectWithTintAndBorderAsync(compositor, - switchEffect, sourceParameters, color, mode == AcrylicEffectMode.InAppBlur ? inAppColorMix : hostColorMix, canvas, uri, options); - - // Setup the tint effect - ArithmeticCompositeEffect tint = source as ArithmeticCompositeEffect ?? source.To().Background as ArithmeticCompositeEffect; - if (tint == null) throw new InvalidOperationException("Error while retrieving the tint effect"); - tint.Name = "Tint"; - const String - tint1Name = "Tint.Source1Amount", - tint2Name = "Tint.Source2Amount"; - - // Make sure the Win2D brush was loaded correctly - const String - source1Name = "Switch.Source1Amount", - source2Name = "Switch.Source2Amount"; - CompositionEffectFactory factory = compositor.CreateEffectFactory(source, new[] - { - blurParameterName, - tint1Name, - tint2Name, - source1Name, - source2Name - }); - - // Create the effect factory and apply the final effect - CompositionEffectBrush effectBrush = factory.CreateBrush(); - foreach (KeyValuePair pair in sourceParameters) - { - effectBrush.SetSourceParameter(pair.Key, pair.Value); - } - - // Setup the toggle function - void Toggle(AcrylicEffectMode m) - { - effectBrush.SetInstantValue(source1Name, m == AcrylicEffectMode.InAppBlur ? 1 : 0); - effectBrush.SetInstantValue(source2Name, m == AcrylicEffectMode.InAppBlur ? 0 :1); - float - mix = m == AcrylicEffectMode.InAppBlur ? inAppColorMix : hostColorMix, - source1 = 1 - mix, - source2 = mix; - effectBrush.SetInstantValue(tint1Name, source1); - effectBrush.SetInstantValue(tint2Name, source2); - } - - // Create the sprite to display and add it to the visual tree - SpriteVisual sprite = compositor.CreateSpriteVisual(); - sprite.Brush = effectBrush; - await AddToTreeAndBindSizeAsync(visual, element, sprite); - if (mode == AcrylicEffectMode.InAppBlur) effectBrush.StartAnimationAsync(blurParameterName, blur, TimeSpan.FromMilliseconds(ms)).Forget(); - return new AttachedToggleAcrylicEffect(element, mode, - mode == AcrylicEffectMode.InAppBlur - ? (Action)null - : () => effectBrush.StartAnimationAsync(blurParameterName, blur, TimeSpan.FromMilliseconds(ms)).Forget(), - Toggle, sprite, disposeOnUnload); - } - - #endregion - - #region Animated effects - - /// - /// Creates a new instance for the target element - /// - /// The type of element to blur - /// The target element - /// The amount of saturation effect to apply - /// The default amount of saturation effect to apply - /// Indicates whether or not to apply the effect right away - /// Indicates whether or not to automatically dispose and remove the effect when the target element is unloaded - [ItemNotNull] - public static async Task> AttachCompositionAnimatableSaturationEffectAsync( - [NotNull] this T element, float on, float off, bool initiallyVisible, bool disposeOnUnload = false) - where T : FrameworkElement - { - // Get the compositor - Visual visual = await element.Dispatcher.GetAsync(element.GetVisual); - Compositor compositor = visual.Compositor; - - // Create the saturation effect and the effect factory - SaturationEffect saturationEffect = new SaturationEffect - { - Name = "SEffect", - Saturation = initiallyVisible ? off : on, - Source = new CompositionEffectSourceParameter("source") - }; - const String animationPropertyName = "SEffect.Saturation"; - CompositionEffectFactory effectFactory = compositor.CreateEffectFactory(saturationEffect, new[] { animationPropertyName }); - - // Setup the rest of the effect - CompositionEffectBrush effectBrush = effectFactory.CreateBrush(); - effectBrush.SetSourceParameter("source", compositor.CreateBackdropBrush()); - - // Assign the effect to a brush and display it - SpriteVisual sprite = compositor.CreateSpriteVisual(); - sprite.Brush = effectBrush; - await AddToTreeAndBindSizeAsync(visual, element, sprite); - if (initiallyVisible) await element.Dispatcher.RunAsync(() => element.Opacity = 1); - return new AttachedAnimatableCompositionEffect(element, sprite, new CompositionAnimationParameters(animationPropertyName, on, off), disposeOnUnload); - } - - /// - /// Creates a new instance for the target element - /// - /// The type of element to blur - /// The target element - /// The amount of blur effect to apply - /// The default amount of blur effect to apply - /// Indicates whether or not to apply the effect right away - /// Indicates whether or not to automatically dispose and remove the effect when the target element is unloaded - [ItemNotNull] - public static async Task> AttachCompositionAnimatableBlurEffectAsync( - [NotNull] this T element, float on, float off, bool initiallyVisible, bool disposeOnUnload = false) where T : FrameworkElement - { - // Get the compositor - Visual visual = await element.Dispatcher.GetAsync(element.GetVisual); - Compositor compositor = visual.Compositor; - - // Create the blur effect and the effect factory - GaussianBlurEffect blurEffect = new GaussianBlurEffect - { - Name = "Blur", - BlurAmount = 0f, - BorderMode = EffectBorderMode.Hard, - Optimization = EffectOptimization.Balanced, - Source = new CompositionEffectSourceParameter("source") - }; - const String animationPropertyName = "Blur.BlurAmount"; - CompositionEffectFactory effectFactory = compositor.CreateEffectFactory(blurEffect, new[] { animationPropertyName }); - - // Setup the rest of the effect - CompositionEffectBrush effectBrush = effectFactory.CreateBrush(); - effectBrush.SetSourceParameter("source", compositor.CreateBackdropBrush()); - - // Assign the effect to a brush and display it - SpriteVisual sprite = compositor.CreateSpriteVisual(); - sprite.Brush = effectBrush; - await AddToTreeAndBindSizeAsync(visual, element, sprite); - if (initiallyVisible) await element.Dispatcher.RunAsync(() => element.Opacity = 1); - return new AttachedAnimatableCompositionEffect(element, sprite, new CompositionAnimationParameters(animationPropertyName, on, off), disposeOnUnload); - } - - /// - /// Creates a new instance with blur, tint and noise effects - /// - /// The type of the element that will be the source for the composition effect - /// The type of the target element that will host the resulting - /// The that will be the source of the effect - /// The target host for the resulting effect - /// The amount of blur effect to apply - /// The default amount of blur effect to apply - /// Indicates whether or not to apply the effect right away - /// The tint color for the effect - /// The opacity of the color over the blurred background - /// The optional source to generate the noise image using Win2D - /// The path of the noise image to use - /// Indicates whether or not to force the reload of the Win2D image - /// Indicates whether or not to automatically dispose and remove the effect when the target element is unloaded - [ItemNotNull] - public static async Task> AttachCompositionAnimatableInAppCustomAcrylicEffectAsync( - [NotNull] this TSource element, [NotNull] T target, - float on, float off, bool initiallyVisible, - Color color, float colorMix, [CanBeNull] CanvasControl canvas, [NotNull] Uri uri, - BitmapCacheMode options = BitmapCacheMode.EnableCaching, bool disposeOnUnload = false) - where TSource : FrameworkElement - where T : FrameworkElement - { - // Get the compositor - Visual visual = await element.Dispatcher.GetAsync(element.GetVisual); - Compositor compositor = visual.Compositor; - - // Create the blur effect and the effect factory - CompositionBackdropBrush backdropBrush = compositor.CreateBackdropBrush(); - GaussianBlurEffect blurEffect = new GaussianBlurEffect - { - Name = "Blur", - BlurAmount = 0f, - BorderMode = EffectBorderMode.Hard, - Optimization = EffectOptimization.Balanced, - Source = new CompositionEffectSourceParameter(nameof(backdropBrush)) - }; - const String animationPropertyName = "Blur.BlurAmount"; - - // Prepare the dictionary with the parameters to add - IDictionary sourceParameters = new Dictionary - { - { nameof(backdropBrush), backdropBrush } - }; - - // Get the noise brush using Win2D - IGraphicsEffect source = await AcrylicEffectHelper.ConcatenateEffectWithTintAndBorderAsync(compositor, - blurEffect, sourceParameters, color, colorMix, canvas, uri, options); - - // Make sure the Win2D brush was loaded correctly - CompositionEffectFactory effectFactory = compositor.CreateEffectFactory(source, new[] { animationPropertyName }); - - // Create the effect factory and apply the final effect - CompositionEffectBrush effectBrush = effectFactory.CreateBrush(); - foreach (KeyValuePair pair in sourceParameters) - { - effectBrush.SetSourceParameter(pair.Key, pair.Value); - } - - // Assign the effect to a brush and display it - SpriteVisual sprite = compositor.CreateSpriteVisual(); - sprite.Brush = effectBrush; - await AddToTreeAndBindSizeAsync(target.GetVisual(), target, sprite); - if (initiallyVisible) await element.Dispatcher.RunAsync(() => element.Opacity = 1); - return new AttachedAnimatableCompositionEffect(target, sprite, new CompositionAnimationParameters(animationPropertyName, on, off), disposeOnUnload); - } - - /// - /// Creates a new instance with blur, tint and noise effects - /// - /// The type of the element that will be the source for the composition effect - /// The type of the target element that will host the resulting - /// The that will be the source of the effect - /// The target host for the resulting effect - /// The amount of blur effect to apply - /// The default amount of blur effect to apply - /// The amount of saturation effect to apply - /// The default amount of saturation effect to apply - /// Indicates whether or not to apply the effect right away - /// The tint color for the effect - /// The opacity of the color over the blurred background - /// The optional source to generate the noise image using Win2D - /// The path of the noise image to use - /// Indicates whether or not to force the reload of the Win2D image - /// Indicates whether or not to automatically dispose and remove the effect when the target element is unloaded - [ItemNotNull] - public static async Task> AttachCompositionAnimatableInAppCustomAcrylicAndSaturationEffectAsync( - [NotNull] this TSource element, [NotNull] T target, - float onBlur, float offBlur, - float onSaturation, float offSaturation, - bool initiallyVisible, - Color color, float colorMix, [CanBeNull] CanvasControl canvas, [NotNull] Uri uri, - BitmapCacheMode options = BitmapCacheMode.EnableCaching, bool disposeOnUnload = false) - where TSource : FrameworkElement - where T : FrameworkElement - { - // Get the compositor - Visual visual = await element.Dispatcher.GetAsync(element.GetVisual); - Compositor compositor = visual.Compositor; - - // Create the blur effect and the effect factory - CompositionBackdropBrush backdropBrush = compositor.CreateBackdropBrush(); - GaussianBlurEffect blurEffect = new GaussianBlurEffect - { - Name = "Blur", - BlurAmount = 0f, - BorderMode = EffectBorderMode.Hard, - Optimization = EffectOptimization.Balanced, - Source = new CompositionEffectSourceParameter(nameof(backdropBrush)) - }; - const String animationPropertyName = "Blur.BlurAmount"; - - // Prepare the dictionary with the parameters to add - IDictionary sourceParameters = new Dictionary - { - { nameof(backdropBrush), backdropBrush } - }; - - // Get the noise brush using Win2D - IGraphicsEffect source = await AcrylicEffectHelper.ConcatenateEffectWithTintAndBorderAsync(compositor, - blurEffect, sourceParameters, color, colorMix, canvas, uri, options); - - // Add the final saturation effect - SaturationEffect saturationEffect = new SaturationEffect - { - Name = "SEffect", - Saturation = initiallyVisible ? offSaturation : onSaturation, - Source = source - }; - const String saturationParameter = "SEffect.Saturation"; - - // Make sure the Win2D brush was loaded correctly - CompositionEffectFactory effectFactory = compositor.CreateEffectFactory(saturationEffect, new[] - { - animationPropertyName, - saturationParameter - }); - - // Create the effect factory and apply the final effect - CompositionEffectBrush effectBrush = effectFactory.CreateBrush(); - foreach (KeyValuePair pair in sourceParameters) - { - effectBrush.SetSourceParameter(pair.Key, pair.Value); - } - - // Assign the effect to a brush and display it - SpriteVisual sprite = compositor.CreateSpriteVisual(); - sprite.Brush = effectBrush; - await AddToTreeAndBindSizeAsync(target.GetVisual(), target, sprite); - if (initiallyVisible) await element.Dispatcher.RunAsync(() => element.Opacity = 1); - return new AttachedCompositeAnimatableCompositionEffect(target, sprite, - new Dictionary - { - { animationPropertyName, new CompositionAnimationValueParameters(onBlur, offBlur) }, - { saturationParameter, new CompositionAnimationValueParameters(onSaturation, offSaturation) } - }, disposeOnUnload); - } - - /// - /// Creates a new instance for the target element that - /// applies both a blur and a saturation effect to the visual item - /// - /// The type of element to blur - /// The target element - /// The amount of blur effect to apply - /// The default amount of blur effect to apply - /// The amount of saturation effect to apply - /// The default amount of saturation effect to apply - /// Indicates whether or not to apply the effect right away - /// Indicates whether or not to automatically dispose and remove the effect when the target element is unloaded - [ItemNotNull] - public static async Task> AttachCompositionAnimatableBlurAndSaturationEffectAsync( - [NotNull] this T element, float onBlur, float offBlur, float onSaturation, float offSaturation, bool initiallyVisible, bool disposeOnUnload = false) - where T : FrameworkElement - { - // Get the compositor - Visual visual = await element.Dispatcher.GetAsync(element.GetVisual); - Compositor compositor = visual.Compositor; - - // Create the blur effect, the saturation effect and the effect factory - GaussianBlurEffect blurEffect = new GaussianBlurEffect - { - Name = "Blur", - BlurAmount = 0f, - BorderMode = EffectBorderMode.Hard, - Optimization = EffectOptimization.Balanced, - Source = new CompositionEffectSourceParameter("source") - }; - SaturationEffect saturationEffect = new SaturationEffect - { - Name = "SEffect", - Saturation = initiallyVisible ? offSaturation : onSaturation, - Source = blurEffect - }; - const String blurParameter = "Blur.BlurAmount", saturationParameter = "SEffect.Saturation"; - CompositionEffectFactory effectFactory = compositor.CreateEffectFactory(saturationEffect, new[] - { - blurParameter, - saturationParameter - }); - - // Setup the rest of the effect - CompositionEffectBrush effectBrush = effectFactory.CreateBrush(); - effectBrush.SetSourceParameter("source", compositor.CreateBackdropBrush()); - - // Assign the effect to a brush and display it - SpriteVisual sprite = compositor.CreateSpriteVisual(); - sprite.Brush = effectBrush; - await AddToTreeAndBindSizeAsync(visual, element, sprite); - if (initiallyVisible) await element.Dispatcher.RunAsync(() => element.Opacity = 1); - - // Prepare and return the wrapped effect - return new AttachedCompositeAnimatableCompositionEffect(element, sprite, - new Dictionary - { - { blurParameter, new CompositionAnimationValueParameters(onBlur, offBlur) }, - { saturationParameter, new CompositionAnimationValueParameters(onSaturation, offSaturation) } - }, disposeOnUnload); - } - - #endregion - - #region Tools - - /// - /// Adds a object on top of the target and binds the size of the two items with an expression animation - /// - /// The object that will host the effect - /// The target (bound to the given visual) that will host the effect - /// The source object to display - private static async Task AddToTreeAndBindSizeAsync([NotNull] Visual host, [NotNull] UIElement element, [NotNull] Visual visual) - { - // Add the shadow as a child of the host in the visual tree - await element.Dispatcher.RunAsync(() => ElementCompositionPreview.SetElementChildVisual(element, visual)); - - // Make sure size of shadow host and shadow visual always stay in sync - ExpressionAnimation bindSizeAnimation = host.Compositor.CreateExpressionAnimation($"{nameof(host)}.Size"); - bindSizeAnimation.SetReferenceParameter(nameof(host), host); - - // Start the animation - visual.StartAnimation("Size", bindSizeAnimation); - } - - #endregion - } -} \ No newline at end of file diff --git a/UICompositionAnimations/Behaviours/CompositionBrushBuilder.cs b/UICompositionAnimations/Behaviours/CompositionBrushBuilder.cs new file mode 100644 index 0000000..87f0069 --- /dev/null +++ b/UICompositionAnimations/Behaviours/CompositionBrushBuilder.cs @@ -0,0 +1,693 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Windows.Graphics.Effects; +using Windows.UI; +using Windows.UI.Composition; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Hosting; +using JetBrains.Annotations; +using Microsoft.Graphics.Canvas; +using Microsoft.Graphics.Canvas.Effects; +using UICompositionAnimations.Brushes; +using UICompositionAnimations.Brushes.Cache; +using UICompositionAnimations.Enums; +using UICompositionAnimations.Helpers; + +namespace UICompositionAnimations.Behaviours +{ + /// + /// A that represents a custom effect animation that can be applied to a + /// + /// The target instance to use to start the animation + /// The animation target value + /// The animation duration, in milliseconds + public delegate Task EffectAnimation([NotNull] CompositionBrush brush, float value, int ms); + + /// + /// A that allows to build custom effects pipelines and create instances from them + /// + [PublicAPI] + public sealed class CompositionBrushBuilder + { + /// + /// The instance used to produce the output for this pipeline + /// + [NotNull] + private readonly Func> SourceProducer; + + /// + /// The collection of animation properties present in the current pipeline + /// + [NotNull, ItemNotNull] + private readonly IReadOnlyCollection AnimationProperties; + + /// + /// The collection of info on the parameters that need to be initialized after creating the final + /// + [NotNull] + private readonly IReadOnlyDictionary>> LazyParameters; + + #region Constructors + + /// + /// Constructor used to initialize a pipeline from a , for example using the method + /// + /// A instance that will return the initial + private CompositionBrushBuilder([NotNull] Func> factory) + { + string + guid = Guid.NewGuid().ToString("N"), + replaced = Regex.Replace(guid, "[0-9]", "_"), + id = new string(replaced.ToCharArray().Select((c, i) => c == '_' ? char.ToUpper((char)('a' + i % 26)) : c).ToArray()); + SourceProducer = () => Task.FromResult(new CompositionEffectSourceParameter(id).To()); + LazyParameters = new Dictionary>> { { id, factory } }; + AnimationProperties = new string[0]; + } + + /// + /// Base constructor used to create a new instance from scratch + /// + /// A instance that will produce the new to add to the pipeline + /// The collection of animation properties for the new effect + /// The collection of instances that needs to be initialized for the new effect + private CompositionBrushBuilder([NotNull] Func> factory, [NotNull] [ItemNotNull] IReadOnlyCollection animations, [NotNull] IReadOnlyDictionary>> lazy) + { + SourceProducer = factory; + AnimationProperties = animations; + LazyParameters = lazy; + } + + /// + /// Constructor used to initialize a pipeline from a custom instance + /// + /// A instance that will return the initial + private CompositionBrushBuilder([NotNull] Func> factory) + : this(factory, new string[0], new Dictionary>>()) + { } + + /// + /// Constructor used to create a new instance obtained by concatenation between the current pipeline and the input effect info + /// + /// The source pipeline to attach the new effect to + /// A instance that will produce the new to add to the pipeline + /// The collection of animation properties for the new effect + /// The collection of instances that needs to be initialized for the new effect + private CompositionBrushBuilder([NotNull] CompositionBrushBuilder source, + [NotNull] Func> factory, + [CanBeNull] [ItemNotNull] IReadOnlyCollection animations = null, [CanBeNull] IReadOnlyDictionary>> lazy = null) + : this(factory, animations?.Merge(source.AnimationProperties) ?? source.AnimationProperties, lazy?.Merge(source.LazyParameters) ?? source.LazyParameters) + { } + + /// + /// Constructor used to create a new instance obtained by merging two separate pipelines + /// + /// A instance that will produce the new to add to the pipeline + /// The first pipeline to merge + /// The second pipeline to merge + /// The collection of animation properties for the new effect + /// The collection of instances that needs to be initialized for the new effect + private CompositionBrushBuilder([NotNull] Func> factory, + [NotNull] CompositionBrushBuilder a, [NotNull] CompositionBrushBuilder b, + [CanBeNull] [ItemNotNull] IReadOnlyCollection animations = null, [CanBeNull] IReadOnlyDictionary>> lazy = null) + : this(factory, animations?.Merge(a.AnimationProperties.Merge(b.AnimationProperties)) ?? a.AnimationProperties.Merge(b.AnimationProperties), lazy?.Merge(a.LazyParameters.Merge(b.LazyParameters)) ?? a.LazyParameters.Merge(b.LazyParameters)) + { } + + #endregion + + #region Initialization + + // The cache manager for backdrop brushes + [NotNull] + private static readonly ThreadSafeCompositionCache BackdropBrushCache = new ThreadSafeCompositionCache(); + + /// + /// Starts a new pipeline from the returned by + /// + [Pure, NotNull] + public static CompositionBrushBuilder FromBackdropBrush() => new CompositionBrushBuilder(() => BackdropBrushCache.TryGetInstanceAsync(Window.Current.Compositor.CreateBackdropBrush)); + + // The cache manager for host backdrop brushes + [NotNull] + private static readonly ThreadSafeCompositionCache HostBackdropBrushCache = new ThreadSafeCompositionCache(); + + /// + /// Starts a new pipeline from the returned by + /// + [Pure, NotNull] + public static CompositionBrushBuilder FromHostBackdropBrush() => new CompositionBrushBuilder(() => HostBackdropBrushCache.TryGetInstanceAsync(Window.Current.Compositor.CreateHostBackdropBrush)); + + /// + /// Starts a new pipeline from a solid with the specified color + /// + /// The desired color for the initial + [Pure, NotNull] + public static CompositionBrushBuilder FromColor(Color color) => new CompositionBrushBuilder(() => Task.FromResult(new ColorSourceEffect { Color = color }.To())); + + /// + /// Starts a new pipeline from the input instance + /// + /// A that synchronously returns a instance to start the pipeline + [Pure, NotNull] + public static CompositionBrushBuilder FromBrush(Func factory) => new CompositionBrushBuilder(() => Task.FromResult(factory())); + + /// + /// Starts a new pipeline from the input instance + /// + /// A that asynchronously returns a instance to start the pipeline + [Pure, NotNull] + public static CompositionBrushBuilder FromBrush(Func> factory) => new CompositionBrushBuilder(factory); + + /// + /// Starts a new pipeline from the input instance + /// + /// A that synchronously returns a instance to start the pipeline + [Pure, NotNull] + public static CompositionBrushBuilder FromEffect(Func factory) => new CompositionBrushBuilder(() => Task.FromResult(factory())); + + /// + /// Starts a new pipeline from the input instance + /// + /// A that asynchronously returns a instance to start the pipeline + [Pure, NotNull] + public static CompositionBrushBuilder FromEffect(Func> factory) => new CompositionBrushBuilder(factory); + + /// + /// Starts a new pipeline from a Win2D image + /// + /// The path for the image to load + /// Indicates the desired DPI mode to use when loading the image + /// The cache mode to use to load the image + [Pure, NotNull] + public static CompositionBrushBuilder FromImage([NotNull] Uri uri, BitmapDPIMode dpiMode = BitmapDPIMode.CopyDisplayDPISettingsWith96AsLowerBound, BitmapCacheMode cache = BitmapCacheMode.Default) + { + return new CompositionBrushBuilder(() => Win2DImageHelper.LoadImageAsync(Window.Current.Compositor, uri, dpiMode, cache).ContinueWith(t => t.Result as CompositionBrush)); + } + + /// + /// Starts a new pipeline from a Win2D image tiled to cover the available space + /// + /// The path for the image to load + /// Indicates the desired DPI mode to use when loading the image + /// The cache mode to use to load the image + [Pure, NotNull] + public static CompositionBrushBuilder FromTiles([NotNull] Uri uri, BitmapDPIMode dpiMode = BitmapDPIMode.CopyDisplayDPISettingsWith96AsLowerBound, BitmapCacheMode cache = BitmapCacheMode.Default) + { + CompositionBrushBuilder image = FromImage(uri, dpiMode, cache); + + async Task Factory() => new BorderEffect + { + ExtendX = CanvasEdgeBehavior.Wrap, + ExtendY = CanvasEdgeBehavior.Wrap, + Source = await image.SourceProducer() + }; + + return new CompositionBrushBuilder(image, Factory); + } + + /// + /// Starts a new pipeline from the returned by on the input + /// + /// The source to use to create the pipeline + [Pure, NotNull] + public static CompositionBrushBuilder FromUIElement([NotNull] UIElement element) + { + return new CompositionBrushBuilder(() => Task.FromResult(element.GetVisual().Compositor.CreateBackdropBrush().To())); + } + + #endregion + + #region Prebuilt pipelines + + /// + /// Returns a new instance that implements the host backdrop acrylic effect + /// + /// The tint color to use + /// The amount of tint to apply over the current effect + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + [Pure, NotNull] + public static CompositionBrushBuilder FromHostBackdropAcrylic(Color tint, float mix, [NotNull] Uri noiseUri, BitmapCacheMode cache = BitmapCacheMode.Default) + { + return FromHostBackdropBrush() + .Effect(source => new LuminanceToAlphaEffect { Source = source }) + .Opacity(0.4f) + .Blend(FromHostBackdropBrush(), BlendEffectMode.Multiply) + .Tint(tint, mix) + .Blend(FromTiles(noiseUri, cache: cache), BlendEffectMode.Overlay, EffectPlacement.Background); + } + + /// + /// Returns a new instance that implements the host backdrop acrylic effect + /// + /// The tint color to use + /// The animation to apply on the tint color of the effect + /// The amount of tint to apply over the current effect + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + [Pure, NotNull] + public static CompositionBrushBuilder FromHostBackdropAcrylic(Color tint, float mix, out EffectAnimation tintAnimation, [NotNull] Uri noiseUri, BitmapCacheMode cache = BitmapCacheMode.Default) + { + return FromHostBackdropBrush() + .Effect(source => new LuminanceToAlphaEffect { Source = source }) + .Opacity(0.4f) + .Blend(FromHostBackdropBrush(), BlendEffectMode.Multiply) + .Tint(tint, mix, out tintAnimation) + .Blend(FromTiles(noiseUri, cache: cache), BlendEffectMode.Overlay, EffectPlacement.Background); + } + + /// + /// Returns a new instance that implements the in-app backdrop acrylic effect + /// + /// The tint color to use + /// The amount of tint to apply over the current effect + /// The amount of blur to apply to the acrylic brush + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + [Pure, NotNull] + public static CompositionBrushBuilder FromBackdropAcrylic(Color tint, float mix, float blur, [NotNull] Uri noiseUri, BitmapCacheMode cache = BitmapCacheMode.Default) + { + return FromBackdropBrush() + .Tint(tint, mix) + .Blur(blur) + .Blend(FromTiles(noiseUri, cache: cache), BlendEffectMode.Overlay, EffectPlacement.Background); + } + + /// + /// Returns a new instance that implements the in-app backdrop acrylic effect + /// + /// The tint color to use + /// The amount of tint to apply over the current effect + /// The animation to apply on the tint color of the effect + /// The amount of blur to apply to the acrylic brush + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + [Pure, NotNull] + public static CompositionBrushBuilder FromBackdropAcrylic( + Color tint, float mix, out EffectAnimation tintAnimation, + float blur, + [NotNull] Uri noiseUri, BitmapCacheMode cache = BitmapCacheMode.Default) + { + return FromBackdropBrush() + .Tint(tint, mix, out tintAnimation) + .Blur(blur) + .Blend(FromTiles(noiseUri, cache: cache), BlendEffectMode.Overlay, EffectPlacement.Background); + } + + /// + /// Returns a new instance that implements the in-app backdrop acrylic effect + /// + /// The tint color to use + /// The amount of tint to apply over the current effect + /// The amount of blur to apply to the acrylic brush + /// The animation to apply on the blur effect in the pipeline + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + [Pure, NotNull] + public static CompositionBrushBuilder FromBackdropAcrylic( + Color tint, float mix, + float blur, out EffectAnimation blurAnimation, + [NotNull] Uri noiseUri, BitmapCacheMode cache = BitmapCacheMode.Default) + { + return FromBackdropBrush() + .Tint(tint, mix) + .Blur(blur, out blurAnimation) + .Blend(FromTiles(noiseUri, cache: cache), BlendEffectMode.Overlay, EffectPlacement.Background); + } + + /// + /// Returns a new instance that implements the in-app backdrop acrylic effect + /// + /// The tint color to use + /// The amount of tint to apply over the current effect + /// The animation to apply on the tint color of the effect + /// The amount of blur to apply to the acrylic brush + /// The animation to apply on the blur effect in the pipeline + /// The for the noise texture to load for the acrylic effect + /// The cache mode to use to load the image + [Pure, NotNull] + public static CompositionBrushBuilder FromBackdropAcrylic( + Color tint, float mix, out EffectAnimation tintAnimation, + float blur, out EffectAnimation blurAnimation, + [NotNull] Uri noiseUri, BitmapCacheMode cache = BitmapCacheMode.Default) + { + return FromBackdropBrush() + .Tint(tint, mix, out tintAnimation) + .Blur(blur, out blurAnimation) + .Blend(FromTiles(noiseUri, cache: cache), BlendEffectMode.Overlay, EffectPlacement.Background); + } + + #endregion + + #region Blends + + /// + /// Blends two pipelines using a instance with the specified mode + /// + /// The second instance to blend + /// The desired to use to blend the input pipelines + /// The sorting mode to use with the two input pipelines + [Pure, NotNull] + public CompositionBrushBuilder Blend([NotNull] CompositionBrushBuilder pipeline, BlendEffectMode mode, EffectPlacement sorting = EffectPlacement.Foreground) + { + (var foreground, var background) = sorting == EffectPlacement.Foreground ? (this, pipeline) : (pipeline, this); + + async Task Factory() => new BlendEffect + { + Foreground = await foreground.SourceProducer(), + Background = await background.SourceProducer(), + Mode = mode + }; + + return new CompositionBrushBuilder(Factory, foreground, background); + } + + /// + /// Blends two pipelines using an instance + /// + /// The second instance to blend + /// The cross fade factor to blend the input effects + /// The sorting mode to use with the two input pipelines + [Pure, NotNull] + public CompositionBrushBuilder Mix([NotNull] CompositionBrushBuilder pipeline, float factor = 0.5f, EffectPlacement sorting = EffectPlacement.Foreground) + { + if (factor < 0 || factor > 1) throw new ArgumentOutOfRangeException(nameof(factor), "The factor must be in the [0,1] range"); + (var foreground, var background) = sorting == EffectPlacement.Foreground ? (this, pipeline) : (pipeline, this); + + async Task Factory() => new CrossFadeEffect + { + CrossFade = factor, + Source1 = await foreground.SourceProducer(), + Source2 = await background.SourceProducer() + }; + + return new CompositionBrushBuilder(Factory, foreground, background); + } + + /// + /// Blends two pipelines using an instance + /// + /// The second instance to blend + /// The cross fade factor to blend the input effects + /// The optional blur animation for the effect + /// The sorting mode to use with the two input pipelines + /// Note that each pipeline can only contain a single instance of any of the built-in effects with animation support + [Pure, NotNull] + public CompositionBrushBuilder Mix([NotNull] CompositionBrushBuilder pipeline, float factor, out EffectAnimation animation, EffectPlacement sorting = EffectPlacement.Foreground) + { + if (factor < 0 || factor > 1) throw new ArgumentOutOfRangeException(nameof(factor), "The factor must be in the [0,1] range"); + (var foreground, var background) = sorting == EffectPlacement.Foreground ? (this, pipeline) : (pipeline, this); + + async Task Factory() => new CrossFadeEffect + { + CrossFade = factor, + Source1 = await foreground.SourceProducer(), + Source2 = await background.SourceProducer(), + Name = "Fade" + }; + + animation = (brush, value, ms) => + { + if (value < 0 || value > 1) throw new ArgumentOutOfRangeException(nameof(value), "The factor must be in the [0,1] range"); + return brush.StartAnimationAsync("Fade.CrossFade", value, TimeSpan.FromMilliseconds(ms)); + }; + + return new CompositionBrushBuilder(Factory, foreground, background, new[] { "Fade.CrossFade" }); + } + + /// + /// Blends two pipelines using the provided to do so + /// + /// The blend function to use + /// The background pipeline to blend with the current instance + /// The list of optional animatable properties in the returned effect + /// The list of source parameters that require deferred initialization (see for more info) + [Pure, NotNull] + public CompositionBrushBuilder Merge( + [NotNull] Func factory, + [NotNull] CompositionBrushBuilder background, + IEnumerable animations = null, IEnumerable initializers = null) + { + async Task Factory() => factory(await SourceProducer(), await background.SourceProducer()); + + return new CompositionBrushBuilder(Factory, this, background, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } + + /// + /// Blends two pipelines using the provided asynchronous to do so + /// + /// The asynchronous blend function to use + /// The background pipeline to blend with the current instance + /// The list of optional animatable properties in the returned effect + /// The list of source parameters that require deferred initialization (see for more info) + [Pure, NotNull] + public CompositionBrushBuilder Merge( + [NotNull] Func> factory, + [NotNull] CompositionBrushBuilder background, + IEnumerable animations = null, IEnumerable initializers = null) + { + async Task Factory() => await factory(await SourceProducer(), await background.SourceProducer()); + + return new CompositionBrushBuilder(Factory, this, background, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } + + #endregion + + #region Built-in effects + + /// + /// Adds a new to the current pipeline + /// + /// The blur amount to apply + /// The parameter for the effect, defaults to + /// The parameter to use, defaults to + [Pure, NotNull] + public CompositionBrushBuilder Blur(float blur, EffectBorderMode mode = EffectBorderMode.Hard, EffectOptimization optimization = EffectOptimization.Balanced) + { + // Blur effect + async Task Factory() => new GaussianBlurEffect + { + BlurAmount = blur, + BorderMode = mode, + Optimization = optimization, + Source = await SourceProducer() + }; + + return new CompositionBrushBuilder(this, Factory); + } + + /// + /// Adds a new to the current pipeline + /// + /// The initial blur amount + /// The optional blur animation for the effect + /// The parameter for the effect, defaults to + /// The parameter to use, defaults to + /// Note that each pipeline can only contain a single instance of any of the built-in effects with animation support + [Pure, NotNull] + public CompositionBrushBuilder Blur(float blur, out EffectAnimation animation, EffectBorderMode mode = EffectBorderMode.Hard, EffectOptimization optimization = EffectOptimization.Balanced) + { + // Blur effect + async Task Factory() => new GaussianBlurEffect + { + BlurAmount = blur, + BorderMode = mode, + Optimization = optimization, + Source = await SourceProducer(), + Name = "Blur" + }; + + animation = (brush, value, ms) => brush.StartAnimationAsync("Blur.BlurAmount", value, TimeSpan.FromMilliseconds(ms)); + + return new CompositionBrushBuilder(this, Factory, new[] { "Blur.BlurAmount" }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The saturation amount for the new effect + [Pure, NotNull] + public CompositionBrushBuilder Saturation(float saturation) + { + if (saturation < 0 || saturation > 1) throw new ArgumentOutOfRangeException(nameof(saturation), "The saturation must be in the [0,1] range"); + async Task Factory() => new SaturationEffect + { + Saturation = saturation, + Source = await SourceProducer() + }; + + return new CompositionBrushBuilder(this, Factory); + } + + /// + /// Adds a new to the current pipeline + /// + /// The initial saturation amount for the new effect + /// The optional saturation animation for the effect + /// Note that each pipeline can only contain a single instance of any of the built-in effects with animation support + [Pure, NotNull] + public CompositionBrushBuilder Saturation(float saturation, out EffectAnimation animation) + { + if (saturation < 0 || saturation > 1) throw new ArgumentOutOfRangeException(nameof(saturation), "The saturation must be in the [0,1] range"); + async Task Factory() => new SaturationEffect + { + Saturation = saturation, + Source = await SourceProducer(), + Name = "Saturation" + }; + + animation = (brush, value, ms) => + { + if (value < 0 || value > 1) throw new ArgumentOutOfRangeException(nameof(value), "The saturation must be in the [0,1] range"); + return brush.StartAnimationAsync("Saturation.Saturation", value, TimeSpan.FromMilliseconds(ms)); + }; + + return new CompositionBrushBuilder(this, Factory, new[] { "Saturation.Saturation" }); + } + + /// + /// Adds a new to the current pipeline + /// + /// The opacity value to apply to the pipeline + [Pure, NotNull] + public CompositionBrushBuilder Opacity(float opacity) + { + if (opacity < 0 || opacity > 1) throw new ArgumentOutOfRangeException(nameof(opacity), "The opacity must be in the [0,1] range"); + async Task Factory() => new OpacityEffect + { + Opacity = opacity, + Source = await SourceProducer() + }; + + return new CompositionBrushBuilder(this, Factory); + } + + /// + /// Adds a new to the current pipeline + /// + /// The opacity value to apply to the pipeline + /// The optional opacity animation for the effect + /// Note that each pipeline can only contain a single instance of any of the built-in effects with animation support + [Pure, NotNull] + public CompositionBrushBuilder Opacity(float opacity, out EffectAnimation animation) + { + if (opacity < 0 || opacity > 1) throw new ArgumentOutOfRangeException(nameof(opacity), "The opacity must be in the [0,1] range"); + async Task Factory() => new OpacityEffect + { + Opacity = opacity, + Source = await SourceProducer(), + Name = "Opacity" + }; + + animation = (brush, value, ms) => + { + if (value < 0 || value > 1) throw new ArgumentOutOfRangeException(nameof(value), "The opacity must be in the [0,1] range"); + return brush.StartAnimationAsync("Opacity.Opacity", value, TimeSpan.FromMilliseconds(ms)); + }; + + return new CompositionBrushBuilder(this, Factory, new[] { "Opacity.Opacity" }); + } + + /// + /// Applies a tint color on the current pipeline + /// + /// The tint color to use + /// The amount of tint to apply over the current effect + [Pure, NotNull] + public CompositionBrushBuilder Tint(Color color, float mix) => FromColor(color).Mix(this, mix); + + /// + /// Applies a tint color on the current pipeline + /// + /// The tint color to use + /// The initial amount of tint to apply over the current effect + /// The optional tint animation for the effect + /// Note that each pipeline can only contain a single instance of any of the built-in effects with animation support + [Pure, NotNull] + public CompositionBrushBuilder Tint(Color color, float mix, out EffectAnimation animation) => FromColor(color).Mix(this, mix, out animation); + + #endregion + + #region Custom effects + + /// + /// Applies a custom effect to the current pipeline + /// + /// A that takes the current instance and produces a new effect to display + /// The list of optional animatable properties in the returned effect + /// The list of source parameters that require deferred initialization (see for more info) + [Pure, NotNull] + public CompositionBrushBuilder Effect([NotNull] Func factory, IEnumerable animations = null, IEnumerable initializers = null) + { + async Task Factory() => factory(await SourceProducer()); + + return new CompositionBrushBuilder(this, Factory, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } + + /// + /// Applies a custom effect to the current pipeline + /// + /// An asynchronous that takes the current instance and produces a new effect to display + /// The list of optional animatable properties in the returned effect + /// The list of source parameters that require deferred initialization (see for more info) + [Pure, NotNull] + public CompositionBrushBuilder Effect([NotNull] Func> factory, IEnumerable animations = null, IEnumerable initializers = null) + { + async Task Factory() => await factory(await SourceProducer()); + + return new CompositionBrushBuilder(this, Factory, animations?.ToArray(), initializers?.ToDictionary(item => item.Name, item => item.Initializer)); + } + + #endregion + + #region Results + + /// + /// Builds a instance from the current effects pipeline + /// + [Pure, NotNull, ItemNotNull] + public async Task BuildAsync() + { + // Validate the pipeline and build the effects factory + if (!(await SourceProducer() is IGraphicsEffect effect)) throw new InvalidOperationException("The pipeline doesn't contain a valid effects sequence"); + CompositionEffectFactory factory = AnimationProperties.Count > 0 + ? Window.Current.Compositor.CreateEffectFactory(effect, AnimationProperties) + : Window.Current.Compositor.CreateEffectFactory(effect); + + // Create the effect factory and apply the final effect + CompositionEffectBrush effectBrush = factory.CreateBrush(); + foreach (KeyValuePair>> pair in LazyParameters) + effectBrush.SetSourceParameter(pair.Key, await pair.Value()); + + // Cleanup + BackdropBrushCache.Cleanup(); + HostBackdropBrushCache.Cleanup(); + return effectBrush; + } + + /// + /// Builds the current pipeline and creates a that is applied to the input + /// + /// The target to apply the brush to + /// An optional to use to bind the size of the created brush + [ItemNotNull] + public async Task AttachAsync([NotNull] UIElement target, [CanBeNull] UIElement reference = null) + { + SpriteVisual visual = Window.Current.Compositor.CreateSpriteVisual(); + visual.Brush = await BuildAsync(); + ElementCompositionPreview.SetElementChildVisual(target, visual); + if (reference != null) visual.BindSize(reference); + return visual; + } + + /// + /// Creates a new from the current effects pipeline + /// + [Pure, NotNull] + public XamlCompositionBrush AsBrush() => new XamlCompositionBrush(this); + + #endregion + } +} \ No newline at end of file diff --git a/UICompositionAnimations/Behaviours/CompositionSourceInitializer.cs b/UICompositionAnimations/Behaviours/CompositionSourceInitializer.cs new file mode 100644 index 0000000..7ab97a6 --- /dev/null +++ b/UICompositionAnimations/Behaviours/CompositionSourceInitializer.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Windows.UI.Composition; +using JetBrains.Annotations; + +namespace UICompositionAnimations.Behaviours +{ + /// + /// A simple container used to store info on a custom composition effect to create + /// + [PublicAPI] + public sealed class CompositionSourceInitializer + { + /// + /// Gets the stored effect initializer + /// + [NotNull] + internal Func> Initializer { get; } + + /// + /// Gets the name of the target + /// + [NotNull] + internal string Name { get; } + + private CompositionSourceInitializer([NotNull] string name, [NotNull] Func> initializer) + { + Name = name; + Initializer = initializer; + } + + /// + /// Creates a new instance with the info on a given to initialize + /// + /// The target effect name + /// A instance that will produce the to use to initialize the effect + [Pure, NotNull] + public static CompositionSourceInitializer New([NotNull] string name, [NotNull] Func initializer) => new CompositionSourceInitializer(name, () => Task.FromResult(initializer())); + + /// + /// Creates a new instance with the info on a given to initialize + /// + /// The target effect name + /// An asynchronous instance that will produce the to use to initialize the effect + [Pure, NotNull] + public static CompositionSourceInitializer New([NotNull] string name, [NotNull] Func> initializer) => new CompositionSourceInitializer(name, initializer); + } +} diff --git a/UICompositionAnimations/Behaviours/Effects/AttachedAnimatableCompositionEffect.cs b/UICompositionAnimations/Behaviours/Effects/AttachedAnimatableCompositionEffect.cs deleted file mode 100644 index 1c9bac6..0000000 --- a/UICompositionAnimations/Behaviours/Effects/AttachedAnimatableCompositionEffect.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Threading.Tasks; -using Windows.UI.Composition; -using Windows.UI.Xaml; -using JetBrains.Annotations; -using UICompositionAnimations.Behaviours.Effects.Base; -using UICompositionAnimations.Behaviours.Misc; - -namespace UICompositionAnimations.Behaviours.Effects -{ - /// - /// An attached composition effect that supports a single in/out animation - /// - /// The type of the visual element the effect will be applied to - public sealed class AttachedAnimatableCompositionEffect : AttachedAnimatableCompositionEffectBase where T : FrameworkElement - { - // The animation parameters - private readonly CompositionAnimationParameters Parameters; - - // Internal constructor - internal AttachedAnimatableCompositionEffect( - [NotNull] T element, [NotNull] SpriteVisual sprite, - [NotNull] CompositionAnimationParameters parameters, bool disposeOnUnload) : base(element, sprite, disposeOnUnload) - { - Parameters = parameters; - } - - /// - protected override void DisposeCore() - { - EffectBrush.StopAnimation(Parameters.Property); - base.DisposeCore(); - } - - /// - public override Task AnimateAsync(FixedAnimationType animationType, TimeSpan duration) - { - return EffectBrush.StartAnimationAsync(Parameters.Property, - animationType == FixedAnimationType.In ? Parameters.On : Parameters.Off, duration); - } - } -} diff --git a/UICompositionAnimations/Behaviours/Effects/AttachedCompositeAnimatableCompositionEffect.cs b/UICompositionAnimations/Behaviours/Effects/AttachedCompositeAnimatableCompositionEffect.cs deleted file mode 100644 index 052f7f5..0000000 --- a/UICompositionAnimations/Behaviours/Effects/AttachedCompositeAnimatableCompositionEffect.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Windows.UI.Composition; -using Windows.UI.Xaml; -using JetBrains.Annotations; -using UICompositionAnimations.Behaviours.Effects.Base; -using UICompositionAnimations.Behaviours.Misc; - -namespace UICompositionAnimations.Behaviours.Effects -{ - /// - /// An attached composition effect that supports multiple in/out animations - /// - /// The type of the visual element the effects will be applied to - public sealed class AttachedCompositeAnimatableCompositionEffect : AttachedAnimatableCompositionEffectBase where T : FrameworkElement - { - // Private animations parameters - [NotNull] - private readonly IDictionary PropertiesAnimationValues; - - // Internal constructor - internal AttachedCompositeAnimatableCompositionEffect( - [NotNull] T element, [NotNull] SpriteVisual sprite, - [NotNull] IDictionary propertyValues, bool disposeOnUnload) : base(element, sprite, disposeOnUnload) - { - PropertiesAnimationValues = propertyValues; - } - - /// - protected override void DisposeCore() - { - foreach (String key in PropertiesAnimationValues.Keys) EffectBrush.StopAnimation(key); - base.DisposeCore(); - } - - /// - public override Task AnimateAsync(FixedAnimationType animationType, TimeSpan duration) - { - // Apply all the animations in parallel and wait for their completion - return Task.WhenAll( - from pair in PropertiesAnimationValues - let target = animationType == FixedAnimationType.In ? pair.Value.On : pair.Value.Off - select EffectBrush.StartAnimationAsync(pair.Key, target, duration)); - } - } -} \ No newline at end of file diff --git a/UICompositionAnimations/Behaviours/Effects/AttachedStaticCompositionEffect.cs b/UICompositionAnimations/Behaviours/Effects/AttachedStaticCompositionEffect.cs deleted file mode 100644 index 49a2f48..0000000 --- a/UICompositionAnimations/Behaviours/Effects/AttachedStaticCompositionEffect.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using Windows.UI.Composition; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Hosting; -using JetBrains.Annotations; -using UICompositionAnimations.Helpers; - -namespace UICompositionAnimations.Behaviours.Effects -{ - /// - /// An base class for an attached composition effect - /// - /// Tye type of the target element - public class AttachedStaticCompositionEffect : IDisposable where T : FrameworkElement - { - /// - /// Gets the element used to apply the blur effect - /// - [NotNull] - public T Element { get; } - - /// - /// Gets the actual blur sprite shown above the visual element - /// - [NotNull] - public SpriteVisual Sprite { get; } - - /// - /// Gets the composition effect brush applied to the visual element - /// - [NotNull] - public CompositionEffectBrush EffectBrush => Sprite.Brush.To(); - - // Internal constructor - internal AttachedStaticCompositionEffect([NotNull] T element, [NotNull] SpriteVisual sprite, bool disposeOnUnload) - { - // Store the parameters - Element = element; - Sprite = sprite; - if (disposeOnUnload) element.Unloaded += (s, e) => Dispose(); - } - - /// - /// Disposes the resources in the current instance - /// - protected virtual void DisposeCore() - { - Sprite.StopAnimation("Size"); - ElementCompositionPreview.SetElementChildVisual(Element, null); - EffectBrush.Dispose(); - Sprite.Dispose(); - } - - // Indicates whether or not the wrapped effect has already been disposed - private bool _Disposed; - - /// - /// Stops the size animation, removes the effect from the visual tree and disposes it - /// - public void Dispose() - { - if (_Disposed) return; - _Disposed = true; - try - { - DisposeCore(); - } - catch - { - // Never trust the framework - } - } - } -} diff --git a/UICompositionAnimations/Behaviours/Effects/AttachedToggleAcrylicEffect.cs b/UICompositionAnimations/Behaviours/Effects/AttachedToggleAcrylicEffect.cs deleted file mode 100644 index b5b8523..0000000 --- a/UICompositionAnimations/Behaviours/Effects/AttachedToggleAcrylicEffect.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using Windows.UI.Composition; -using Windows.UI.Xaml; -using JetBrains.Annotations; - -namespace UICompositionAnimations.Behaviours.Effects -{ - /// - /// A custom acrylic brush effect that can quickly toggle between different sources (in-app blur or host backdrop effect) - /// - /// The the host that will display the effect visual - public sealed class AttachedToggleAcrylicEffect : AttachedStaticCompositionEffect where T : FrameworkElement - { - /// - /// Gets the action that edits the effects pipeline to apply the requested change - /// - [NotNull] - private readonly Action Toggle; - - // An optional fade in animation for the blurred in-app acrylic effect - [CanBeNull] - private readonly Action InitialInAppEffectFadeIn; - - // Internal constructor - internal AttachedToggleAcrylicEffect([NotNull] T element, AcrylicEffectMode mode, [CanBeNull] Action fadeIn, - [NotNull] Action toggle, [NotNull] SpriteVisual sprite, bool disposeOnUnload) - : base(element, sprite, disposeOnUnload) - { - _AcrylicMode = mode; - _FadeInPending = mode == AcrylicEffectMode.HostBackdrop; - InitialInAppEffectFadeIn = fadeIn; - Toggle = toggle; - } - - // Indicates whether or not the fade in animation should be run - private bool _FadeInPending; - - private AcrylicEffectMode _AcrylicMode; - - /// - /// Gets or sets the current effect mode in use - /// - public AcrylicEffectMode AcrylicMode - { - get => _AcrylicMode; - set - { - if (_AcrylicMode != value) - { - Toggle(value); - if (_FadeInPending && InitialInAppEffectFadeIn != null) - { - _FadeInPending = false; - InitialInAppEffectFadeIn(); - } - _AcrylicMode = value; - } - } - } - } -} diff --git a/UICompositionAnimations/Behaviours/Effects/Base/AttachedAnimatableCompositionEffectBase.cs b/UICompositionAnimations/Behaviours/Effects/Base/AttachedAnimatableCompositionEffectBase.cs deleted file mode 100644 index 6ebafe4..0000000 --- a/UICompositionAnimations/Behaviours/Effects/Base/AttachedAnimatableCompositionEffectBase.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Threading.Tasks; -using Windows.UI.Composition; -using Windows.UI.Xaml; -using JetBrains.Annotations; -using UICompositionAnimations.Behaviours.Misc; - -namespace UICompositionAnimations.Behaviours.Effects.Base -{ - /// - /// An base class for an attached composition effect that supports a ready to use animation with fixed states - /// - /// Tye type of the target element the animation will be applied to - public abstract class AttachedAnimatableCompositionEffectBase : AttachedStaticCompositionEffect where T : FrameworkElement - { - // Protected constructor for the implementations - internal AttachedAnimatableCompositionEffectBase([NotNull] T element, [NotNull] SpriteVisual sprite, bool disposeOnUnload) - : base(element, sprite, disposeOnUnload) { } - - /// - /// Executes the animation to the desired destination status and returns a task that completes when the animation ends - /// - /// The target animation status - /// The animation duration - public abstract Task AnimateAsync(FixedAnimationType animationType, TimeSpan duration); - } -} \ No newline at end of file diff --git a/UICompositionAnimations/Behaviours/Misc/CompositionAnimationParameters.cs b/UICompositionAnimations/Behaviours/Misc/CompositionAnimationParameters.cs deleted file mode 100644 index dc1f7bc..0000000 --- a/UICompositionAnimations/Behaviours/Misc/CompositionAnimationParameters.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using JetBrains.Annotations; - -namespace UICompositionAnimations.Behaviours.Misc -{ - /// - /// A class that stores the parameters for two different states for a pre-defined composition animation - /// - internal class CompositionAnimationValueParameters - { - /// - /// Gets the target value when the effect is enabled - /// - public float On { get; } - - /// - /// Gets the target value when the effect is disabled - /// - public float Off { get; } - - /// - /// Creates a new instance for the given states - /// - /// The on parameter - /// The off parameter - public CompositionAnimationValueParameters(float on, float off) - { - On = on; - Off = off; - } - } - - /// - /// A class that stores the parameters for two different states for a pre-defined composition animation, along with the target property to animate - /// - internal sealed class CompositionAnimationParameters : CompositionAnimationValueParameters - { - /// - /// Gets the property to animate - /// - [NotNull] - public String Property { get; } - - /// - /// Creates a new instance for the target property and states - /// - /// The target property to animate - /// The on parameter - /// The off parameter - public CompositionAnimationParameters([NotNull] String property, float on, float off) : base(on, off) - { - Property = property; - } - } -} diff --git a/UICompositionAnimations/Behaviours/Misc/FixedAnimationType.cs b/UICompositionAnimations/Behaviours/Misc/FixedAnimationType.cs deleted file mode 100644 index 5d585eb..0000000 --- a/UICompositionAnimations/Behaviours/Misc/FixedAnimationType.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace UICompositionAnimations.Behaviours.Misc -{ - /// - /// Indicates the direction of a ready to use composition animation - /// - public enum FixedAnimationType - { - /// - /// Applies the effect - /// - In, - - /// - /// Reverts the applied effect - /// - Out - } -} \ No newline at end of file diff --git a/UICompositionAnimations/Brushes/Base/XamlCompositionEffectBrushBase.cs b/UICompositionAnimations/Brushes/Base/XamlCompositionEffectBrushBase.cs new file mode 100644 index 0000000..685bd9a --- /dev/null +++ b/UICompositionAnimations/Brushes/Base/XamlCompositionEffectBrushBase.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using Windows.UI.Xaml.Media; +using JetBrains.Annotations; +using UICompositionAnimations.Behaviours; + +namespace UICompositionAnimations.Brushes.Base +{ + /// + /// A custom that's ready to be used with a custom pipeline. + /// + [PublicAPI] + public abstract class XamlCompositionEffectBrushBase : XamlCompositionBrushBase + { + // Initialization mutex + [NotNull] + private readonly AsyncMutex ConnectedMutex = new AsyncMutex(); + + /// + /// A method that builds and returns the pipeline to use in the current instance. + /// This method can also be used to store any needed instances in local fields, for later use (they will need to be called upon ). + /// + [MustUseReturnValue, NotNull] + protected abstract CompositionBrushBuilder OnBrushRequested(); + + /// + protected override async void OnConnected() + { + using (await ConnectedMutex.LockAsync()) + if (CompositionBrush == null) + CompositionBrush = await OnBrushRequested().BuildAsync(); + base.OnConnected(); + } + + /// + protected override async void OnDisconnected() + { + using (await ConnectedMutex.LockAsync()) + { + if (CompositionBrush != null) + { + CompositionBrush.Dispose(); + CompositionBrush = null; + } + } + base.OnDisconnected(); + } + } +} diff --git a/UICompositionAnimations/Brushes/Cache/ThreadSafeCompositionCache.cs b/UICompositionAnimations/Brushes/Cache/ThreadSafeCompositionCache.cs new file mode 100644 index 0000000..9c4f371 --- /dev/null +++ b/UICompositionAnimations/Brushes/Cache/ThreadSafeCompositionCache.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Windows.UI.Composition; +using Windows.UI.Core; +using JetBrains.Annotations; + +namespace UICompositionAnimations.Brushes.Cache +{ + /// + /// A used to cache reusable instances + /// + /// The type of instances to cache + internal sealed class ThreadSafeCompositionCache where T : CompositionObject + { + /// + /// The cache of weak references, to avoid memory leaks + /// + [NotNull, ItemNotNull] + private readonly List> Cache = new List>(); + + /// + /// The instance used to synchronize concurrent operations on the cache + /// + [NotNull] + private readonly AsyncMutex Mutex = new AsyncMutex(); + + /// + /// Tries to retrieve a valid instance from the cache, and uses the provided factory if an existing item is not found + /// + /// A used when the requested value is not present in the cache + [MustUseReturnValue, NotNull, ItemNotNull] + public async Task TryGetInstanceAsync([NotNull] Func factory) + { + using (await Mutex.LockAsync()) + { + // Try to retrieve an valid instance from the cache + foreach (WeakReference value in Cache) + if (value.TryGetTarget(out T instance) && instance.TryGetDispatcher(out CoreDispatcher dispatcher) && dispatcher.HasThreadAccess) + return instance; + + // Create a new instance when needed + T fallback = factory(); + Cache.Add(new WeakReference(fallback)); + return fallback; + } + } + + /// + /// Performs a cleanup of the cache by removing invalid references + /// + public async void Cleanup() + { + using (await Mutex.LockAsync()) + Cache.RemoveAll(reference => !reference.TryGetTarget(out T target) || !target.TryGetDispatcher(out _)); + } + } +} diff --git a/UICompositionAnimations/Brushes/Cache/ThreadSafeCompositionMapCache.cs b/UICompositionAnimations/Brushes/Cache/ThreadSafeCompositionMapCache.cs new file mode 100644 index 0000000..ec6e7c5 --- /dev/null +++ b/UICompositionAnimations/Brushes/Cache/ThreadSafeCompositionMapCache.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Windows.UI.Composition; +using Windows.UI.Core; +using JetBrains.Annotations; + +namespace UICompositionAnimations.Brushes.Cache +{ + /// + /// A used to cache reusable instances with an associated key + /// + /// The type of key to classify the items in the cache + /// The type of items stored in the cache + internal sealed class ThreadSafeCompositionMapCache where TValue : CompositionObject + { + /// + /// The cache of weak references, to avoid memory leaks + /// + [NotNull] + private readonly Dictionary>> Cache = new Dictionary>>(); + + /// + /// Tries to retrieve a valid instance from the cache, and uses the provided factory if an existing item is not found + /// + /// The key to look for + /// The resulting value, if existing + [MustUseReturnValue] + public bool TryGetInstance(TKey key, out TValue result) + { + // Try to retrieve an valid instance from the cache + if (Cache.TryGetValue(key, out List> values)) + foreach (WeakReference value in values) + if (value.TryGetTarget(out TValue instance) && instance.TryGetDispatcher(out CoreDispatcher dispatcher) && dispatcher.HasThreadAccess) + { + result = instance; + return true; + } + + // Not found + result = null; + return false; + } + + /// + /// Adds a new value with the specified key to the cache + /// + /// The key of the item to add + /// The value to add + public void Add(TKey key, TValue value) + { + if (Cache.TryGetValue(key, out List> list)) list.Add(new WeakReference(value)); + else Cache.Add(key, new List> { new WeakReference(value) }); + } + + /// + /// Adds a new value and removes previous values with the same key, if any + /// + /// The key of the item to add + /// The value to add + public void Overwrite(TKey key, TValue value) + { + Cache.Remove(key); + Cache.Add(key, new List> { new WeakReference(value) }); + } + + /// + /// Performs a cleanup of the cache by removing invalid references + /// + public void Cleanup() + { + foreach (List> list in Cache.Values) + list.RemoveAll(reference => !reference.TryGetTarget(out TValue value) || !value.TryGetDispatcher(out _)); + foreach (TKey key in Cache.Keys.ToArray()) + if (Cache[key].Count == 0) + Cache.Remove(key); + } + + /// + /// Clears the cache by removing all the stored items + /// + public IReadOnlyList Clear() + { + List values = new List(); + foreach (WeakReference reference in Cache.Values.SelectMany(list => list)) + if (reference.TryGetTarget(out TValue value)) + values.Add(value); + Cache.Clear(); + return values; + } + } +} \ No newline at end of file diff --git a/UICompositionAnimations/Brushes/CustomAcrylicBrush.cs b/UICompositionAnimations/Brushes/CustomAcrylicBrush.cs index 9dd9cbc..4589b85 100644 --- a/UICompositionAnimations/Brushes/CustomAcrylicBrush.cs +++ b/UICompositionAnimations/Brushes/CustomAcrylicBrush.cs @@ -10,7 +10,6 @@ using Windows.UI.Xaml.Media; using JetBrains.Annotations; using UICompositionAnimations.Behaviours; -using UICompositionAnimations.Behaviours.Effects; using UICompositionAnimations.Brushes.Cache; using UICompositionAnimations.Enums; using UICompositionAnimations.Helpers; @@ -21,24 +20,25 @@ namespace UICompositionAnimations.Brushes /// /// A custom XAML brush that includes an acrylic effect that blurs the in-app content /// + [PublicAPI] public sealed class CustomAcrylicBrush : XamlCompositionBrushBase { #region Constants // The name of the animatable blur amount property - private const String BlurAmountParameterName = "Blur.BlurAmount"; + private const string BlurAmountParameterName = "Blur.BlurAmount"; // The name of the animatable source 1 property (the brush) of the tint effect - private const String TintColor1ParameterName = "Tint.Source1Amount"; + private const string TintColor1ParameterName = "Tint.Source1Amount"; // The name of the animatable source 2 property (the tint color) of the tint effect - private const String TintColor2ParameterName = "Tint.Source2Amount"; + private const string TintColor2ParameterName = "Tint.Source2Amount"; // The name of the animatable color property of the color effect - private const String ColorSourceParameterName = "ColorSource.Color"; + private const string ColorSourceParameterName = "ColorSource.Color"; // The name of the animatable color property of the fallback color effect - private const String FallbackColorParameterName = "FallbackColor.Color"; + private const string FallbackColorParameterName = "FallbackColor.Color"; #endregion @@ -49,8 +49,8 @@ public sealed class CustomAcrylicBrush : XamlCompositionBrushBase /// public AcrylicEffectMode Mode { - get { return GetValue(ModeProperty).To(); } - set { SetValue(ModeProperty, value); } + get => GetValue(ModeProperty).To(); + set => SetValue(ModeProperty, value); } /// @@ -80,8 +80,8 @@ private static async void OnModePropertyChanged(DependencyObject d, DependencyPr /// This property is ignored when the active mode is public double BlurAmount { - get { return GetValue(BlurAmountProperty).To(); } - set { SetValue(BlurAmountProperty, value); } + get => GetValue(BlurAmountProperty).To(); + set => SetValue(BlurAmountProperty, value); } /// @@ -112,8 +112,8 @@ private static async void OnBlurAmountPropertyChanged(DependencyObject d, Depend /// This property is ignored when the active mode is public int BlurAnimationDuration { - get { return GetValue(BlurAnimationDurationProperty).To(); } - set { SetValue(BlurAnimationDurationProperty, value); } + get => GetValue(BlurAnimationDurationProperty).To(); + set => SetValue(BlurAnimationDurationProperty, value); } /// @@ -127,8 +127,8 @@ public int BlurAnimationDuration /// public Color Tint { - get { return GetValue(TintProperty).To(); } - set { SetValue(TintProperty, value); } + get => GetValue(TintProperty).To(); + set => SetValue(TintProperty, value); } /// @@ -153,8 +153,8 @@ private static async void OnTintPropertyChanged(DependencyObject d, DependencyPr /// public double TintMix { - get { return GetValue(TintMixProperty).To(); } - set { SetValue(TintMixProperty, value); } + get => GetValue(TintMixProperty).To(); + set => SetValue(TintMixProperty, value); } /// @@ -167,7 +167,7 @@ private static async void OnTintMixPropertyChanged(DependencyObject d, Dependenc { double value = e.NewValue.To(); float fvalue = (float)value; - if (value < 0 || value >= 1) throw new ArgumentOutOfRangeException("The tint mix must be in the [0..1) range"); + if (value < 0 || value >= 1) throw new ArgumentOutOfRangeException(nameof(value), "The tint mix must be in the [0..1) range"); CustomAcrylicBrush @this = d.To(); await @this.ConnectedSemaphore.WaitAsync(); if (@this.CompositionBrush != null && @this._State != AcrylicBrushEffectState.FallbackMode) @@ -183,8 +183,8 @@ private static async void OnTintMixPropertyChanged(DependencyObject d, Dependenc /// public Color UnsupportedEffectFallbackColor { - get { return GetValue(UnsupportedEffectFallbackColorProperty).To(); } - set { SetValue(UnsupportedEffectFallbackColorProperty, value); } + get => GetValue(UnsupportedEffectFallbackColorProperty).To(); + set => SetValue(UnsupportedEffectFallbackColorProperty, value); } /// @@ -217,13 +217,6 @@ private static async void OnUnsupportedEffectFallbackColorPropertyChanged(Depend @this._EffectBrush?.Properties.InsertColor(FallbackColorParameterName, value); } } - else if (@this._State == AcrylicBrushEffectState.Default && // The effect is currently disabled - @this.Mode == AcrylicEffectMode.HostBackdrop && // The current settings are not valid - ApiInformationHelper.IsMobileDevice && - !value.Equals(Colors.Transparent)) // The new value allows the fallback mode to be enabled - { - await @this.SetupEffectAsync(); - } @this.ConnectedSemaphore.Release(); } @@ -233,11 +226,6 @@ private static async void OnUnsupportedEffectFallbackColorPropertyChanged(Depend /// This property must be initialized before using the brush public Uri NoiseTextureUri { get; set; } - /// - /// Gets or sets the caching setting for the acrylic brush - /// - public BitmapCacheMode CacheMode { get; set; } = BitmapCacheMode.EnableCaching; - /// /// Indicates whether or not to enable an additional safety procedure when loading the acrylic brush. /// This property should be used for brushes being rendered on secondary windows that use a given acrylic mode for the first @@ -302,7 +290,7 @@ protected override async void OnDisconnected() private static CompositionBackdropBrush _BackdropInstance; // The name to use for the in-app backdrop reference parameter - private const String BackdropReferenceParameterName = "BackdropBrush"; + private const string BackdropReferenceParameterName = "BackdropBrush"; // The synchronization semaphore for the host backdrop brush private static readonly SemaphoreSlim HostBackdropSemaphore = new SemaphoreSlim(1); @@ -311,12 +299,11 @@ protected override async void OnDisconnected() private static HostBackdropInstanceWrapper _HostBackdropCache; // The name to use for the host backdrop reference parameter - private const String HostBackdropReferenceParameterName = "HostBackdropBrush"; + private const string HostBackdropReferenceParameterName = "HostBackdropBrush"; /// /// Clears the internal cache of instances /// - [PublicAPI] public static async Task ClearCacheAsync(AcrylicEffectMode targets) { // In-app backdrop brush @@ -344,30 +331,11 @@ public static async Task ClearCacheAsync(AcrylicEffectMode targets) private async Task SetupEffectAsync() { // Designer check - if (DesignMode.DesignModeEnabled) return; - - // Platform check - if (ApiInformationHelper.IsMobileDevice && Mode == AcrylicEffectMode.HostBackdrop) - { - // Create the fallback brush effect when needed - if (!UnsupportedEffectFallbackColor.Equals(Colors.Transparent)) - { - ColorSourceEffect fallback = new ColorSourceEffect - { - Name = "FallbackColor", - Color = UnsupportedEffectFallbackColor - }; - CompositionEffectFactory fallbackFactory = Window.Current.Compositor.CreateEffectFactory(fallback); - _EffectBrush = fallbackFactory.CreateBrush(); - CompositionBrush = _EffectBrush; - _State = AcrylicBrushEffectState.FallbackMode; - } - return; - } + if (DesignMode.DesignMode2Enabled) return; // Dictionary to track the reference and animatable parameters - IDictionary sourceParameters = new Dictionary(); - List animatableParameters = new List + IDictionary sourceParameters = new Dictionary(); + List animatableParameters = new List { TintColor1ParameterName, TintColor2ParameterName, @@ -444,14 +412,13 @@ private async Task SetupEffectAsync() // Get the noise brush using Win2D IGraphicsEffect source = await AcrylicEffectHelper.ConcatenateEffectWithTintAndBorderAsync(Window.Current.Compositor, - baseEffect, sourceParameters, Tint, (float)TintMix, null, NoiseTextureUri, CacheMode); + baseEffect, sourceParameters, Tint, (float)TintMix, null, NoiseTextureUri); // Extract and setup the tint and color effects ArithmeticCompositeEffect tint = source as ArithmeticCompositeEffect ?? source.To().Background as ArithmeticCompositeEffect; if (tint == null) throw new InvalidOperationException("Error while retrieving the tint effect"); tint.Name = "Tint"; - ColorSourceEffect color = tint.Source2 as ColorSourceEffect; - if (color == null) throw new InvalidOperationException("Error while retrieving the color effect"); + if (!(tint.Source2 is ColorSourceEffect color)) throw new InvalidOperationException("Error while retrieving the color effect"); color.Name = "ColorSource"; // Make sure the Win2D brush was loaded correctly @@ -459,7 +426,7 @@ private async Task SetupEffectAsync() // Create the effect factory and apply the final effect _EffectBrush = factory.CreateBrush(); - foreach (KeyValuePair pair in sourceParameters) + foreach (KeyValuePair pair in sourceParameters) { _EffectBrush.SetSourceParameter(pair.Key, pair.Value); } diff --git a/UICompositionAnimations/Brushes/LightingBrush.cs b/UICompositionAnimations/Brushes/LightingBrush.cs index 3a7d273..0ce927b 100644 --- a/UICompositionAnimations/Brushes/LightingBrush.cs +++ b/UICompositionAnimations/Brushes/LightingBrush.cs @@ -17,8 +17,8 @@ public sealed class LightingBrush : XamlCompositionBrushBase /// public double DiffuseAmount { - get { return (double)GetValue(DiffuseAmountProperty); } - set { SetValue(DiffuseAmountProperty, value); } + get => (double)GetValue(DiffuseAmountProperty); + set => SetValue(DiffuseAmountProperty, value); } /// @@ -38,8 +38,8 @@ private static void OnDiffuseAmountChanged(DependencyObject d, DependencyPropert /// public double SpecularShine { - get { return (double)GetValue(SpecularShineProperty); } - set { SetValue(SpecularShineProperty, value); } + get => (double)GetValue(SpecularShineProperty); + set => SetValue(SpecularShineProperty, value); } /// @@ -58,8 +58,8 @@ private static void OnSpecularShineChanged(DependencyObject d, DependencyPropert /// public double SpecularAmount { - get { return (double)GetValue(SpecularAmountProperty); } - set { SetValue(SpecularAmountProperty, value); } + get => (double)GetValue(SpecularAmountProperty); + set => SetValue(SpecularAmountProperty, value); } /// diff --git a/UICompositionAnimations/Brushes/NoiseTextureBrush.cs b/UICompositionAnimations/Brushes/NoiseTextureBrush.cs index 965edf1..254ac34 100644 --- a/UICompositionAnimations/Brushes/NoiseTextureBrush.cs +++ b/UICompositionAnimations/Brushes/NoiseTextureBrush.cs @@ -9,7 +9,6 @@ using Windows.UI.Xaml; using Windows.UI.Xaml.Media; using UICompositionAnimations.Behaviours; -using UICompositionAnimations.Enums; using UICompositionAnimations.Helpers; using Windows.ApplicationModel; @@ -21,7 +20,7 @@ namespace UICompositionAnimations.Brushes public sealed class NoiseTextureBrush : XamlCompositionBrushBase { // The name of the animatable color property of the color effect - private const String ColorSourceParameterName = "ColorSource.Color"; + private const string ColorSourceParameterName = "ColorSource.Color"; #region Properties @@ -30,8 +29,8 @@ public sealed class NoiseTextureBrush : XamlCompositionBrushBase /// public Color Tint { - get { return GetValue(TintProperty).To(); } - set { SetValue(TintProperty, value); } + get => GetValue(TintProperty).To(); + set => SetValue(TintProperty, value); } /// @@ -57,11 +56,6 @@ private static async void OnTintPropertyChanged(DependencyObject d, DependencyPr /// This property must be initialized before using the brush public Uri TextureUri { get; set; } - /// - /// Gets or sets the caching setting for the acrylic brush - /// - public BitmapCacheMode CacheMode { get; set; } = BitmapCacheMode.EnableCaching; - #endregion // Initialization semaphore (due to the Win2D image loading being asynchronous) @@ -107,14 +101,14 @@ protected override async void OnDisconnected() private async Task SetupEffectAsync() { // Designer check - if (DesignMode.DesignModeEnabled) return; + if (DesignMode.DesignMode2Enabled) return; // Dictionary to track the reference and animatable parameters - IDictionary sourceParameters = new Dictionary(); - List animatableParameters = new List { ColorSourceParameterName }; + IDictionary sourceParameters = new Dictionary(); + List animatableParameters = new List { ColorSourceParameterName }; // Get the noise brush using Win2D - IGraphicsEffect source = await AcrylicEffectHelper.LoadTextureEffectWithTintAsync(Window.Current.Compositor, sourceParameters, Tint, TextureUri, CacheMode); + IGraphicsEffect source = await AcrylicEffectHelper.LoadTextureEffectWithTintAsync(Window.Current.Compositor, sourceParameters, Tint, TextureUri); // Extract and setup the tint and color effects ColorSourceEffect color = source as ColorSourceEffect ?? source.To().Background as ColorSourceEffect; @@ -126,7 +120,7 @@ private async Task SetupEffectAsync() // Create the effect factory and apply the final effect _EffectBrush = factory.CreateBrush(); - foreach (KeyValuePair pair in sourceParameters) + foreach (KeyValuePair pair in sourceParameters) { _EffectBrush.SetSourceParameter(pair.Key, pair.Value); } diff --git a/UICompositionAnimations/Brushes/XamlCompositionBrush.cs b/UICompositionAnimations/Brushes/XamlCompositionBrush.cs new file mode 100644 index 0000000..37a3173 --- /dev/null +++ b/UICompositionAnimations/Brushes/XamlCompositionBrush.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using UICompositionAnimations.Behaviours; +using UICompositionAnimations.Brushes.Base; + +namespace UICompositionAnimations.Brushes +{ + /// + /// A that represents a custom effect animation that can be applied to a instance + /// + /// The animation target value + /// The animation duration, in milliseconds + public delegate Task XamlEffectAnimation(float value, int ms); + + /// + /// A simple that can be used to quickly create XAML brushes from arbitrary pipelines + /// + public sealed class XamlCompositionBrush : XamlCompositionEffectBrushBase + { + /// + /// Gets the pipeline for the current instance + /// + [NotNull] + public CompositionBrushBuilder Pipeline { get; } + + /// + /// Creates a new XAML brush from the input effects pipeline + /// + /// The instance to create the effect + public XamlCompositionBrush([NotNull] CompositionBrushBuilder pipeline) => Pipeline = pipeline; + + /// + /// Binds an to the composition brush in the current instance + /// + /// The input animation + /// The resulting animation + [Pure, NotNull] + public XamlCompositionBrush Bind([NotNull] EffectAnimation animation, out XamlEffectAnimation bound) + { + bound = (value, ms) => animation(CompositionBrush, value, ms); + return this; + } + + /// + protected override CompositionBrushBuilder OnBrushRequested() => Pipeline; + + /// + /// Clones the current instance by rebuilding the source . Use this method to reuse the same effects pipeline on a different + /// + [PublicAPI, Pure, NotNull] + public XamlCompositionBrush Clone() + { + if (Dispatcher.HasThreadAccess) + { + throw new InvalidOperationException("The current thread already has access to the brush dispatcher, so a clone operation is not necessary. " + + "You can just assign this brush to an arbitrary number of controls and it will still work correctly. " + + "This method is only meant to be used to create a new instance of this brush using the same pipeline, " + + "on threads that can't access the current instance, for example in secondary app windows."); + } + return new XamlCompositionBrush(Pipeline); + } + } +} \ No newline at end of file diff --git a/UICompositionAnimations/Composition/CompositionManager.cs b/UICompositionAnimations/Composition/CompositionManager.cs index 06c8fcc..4cba52e 100644 --- a/UICompositionAnimations/Composition/CompositionManager.cs +++ b/UICompositionAnimations/Composition/CompositionManager.cs @@ -9,6 +9,7 @@ namespace UICompositionAnimations.Composition /// /// Create composition animations using this class /// + [PublicAPI] public static class CompositionManager { #region Animations initialization @@ -23,8 +24,7 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] - public static void BeginScalarAnimation([NotNull] UIElement element, [NotNull] String propertyPath, + public static void BeginScalarAnimation([NotNull] UIElement element, [NotNull] string propertyPath, float? from, float to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) { element.GetVisual().BeginScalarAnimation(propertyPath, from, to, duration, delay, ease); @@ -40,8 +40,7 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] - public static void BeginVector2Animation([NotNull] UIElement element, [NotNull] String propertyPath, + public static void BeginVector2Animation([NotNull] UIElement element, [NotNull] string propertyPath, Vector2? from, Vector2 to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) { element.GetVisual().BeginVector2Animation(propertyPath, from, to, duration, delay, ease); @@ -57,7 +56,7 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - public static void BeginVector3Animation([NotNull] UIElement element, [NotNull] String propertyPath, + public static void BeginVector3Animation([NotNull] UIElement element, [NotNull] string propertyPath, Vector3? from, Vector3 to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) { element.GetVisual().BeginVector3Animation(propertyPath, from, to, duration, delay, ease); @@ -73,8 +72,7 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] - public static void BeginScalarAnimation([NotNull] this CompositionObject compObj, [NotNull] String propertyPath, + public static void BeginScalarAnimation([NotNull] this CompositionObject compObj, [NotNull] string propertyPath, float? from, float to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) { compObj.StartAnimation(propertyPath, compObj.Compositor.CreateScalarKeyFrameAnimation(from, to, duration, delay, ease)); @@ -90,8 +88,7 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] - public static void BeginVector2Animation([NotNull]this CompositionObject compObj, [NotNull] String propertyPath, + public static void BeginVector2Animation([NotNull]this CompositionObject compObj, [NotNull] string propertyPath, Vector2? from, Vector2 to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) { compObj.StartAnimation(propertyPath, compObj.Compositor.CreateVector2KeyFrameAnimation(from, to, duration, delay, ease)); @@ -107,8 +104,7 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] - public static void BeginVector3Animation([NotNull] this CompositionObject compObj, [NotNull] String propertyPath, + public static void BeginVector3Animation([NotNull] this CompositionObject compObj, [NotNull] string propertyPath, Vector3? from, Vector3 to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) { compObj.StartAnimation(propertyPath, compObj.Compositor.CreateVector3KeyFrameAnimation(from, to, duration, delay, ease)); @@ -127,7 +123,6 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] [Pure, NotNull] public static ScalarKeyFrameAnimation CreateScalarKeyFrameAnimation([NotNull] this Compositor compositor, float? from, float to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) @@ -152,10 +147,9 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] [Pure, NotNull] public static ScalarKeyFrameAnimation CreateScalarKeyFrameAnimation([NotNull] this Compositor compositor, - float? from, [NotNull] String to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) + float? from, [NotNull] string to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) { // Set duration and delay time ScalarKeyFrameAnimation ani = compositor.CreateScalarKeyFrameAnimation(); @@ -177,7 +171,6 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] [Pure, NotNull] public static Vector2KeyFrameAnimation CreateVector2KeyFrameAnimation([NotNull] this Compositor compositor, Vector2? from, Vector2 to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) @@ -202,10 +195,9 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] [Pure, NotNull] public static Vector2KeyFrameAnimation CreateVector2KeyFrameAnimation([NotNull] this Compositor compositor, - Vector2? from, [NotNull] String to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) + Vector2? from, [NotNull] string to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) { // Set duration and delay time Vector2KeyFrameAnimation ani = compositor.CreateVector2KeyFrameAnimation(); @@ -227,7 +219,6 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] [Pure, NotNull] public static Vector3KeyFrameAnimation CreateVector3KeyFrameAnimation([NotNull] this Compositor compositor, Vector3? from, Vector3 to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) @@ -252,10 +243,9 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] [Pure, NotNull] public static Vector3KeyFrameAnimation CreateVector3KeyFrameAnimation([NotNull] this Compositor compositor, - Vector3? from, [NotNull] String to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) + Vector3? from, [NotNull] string to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) { // Set duration and delay time Vector3KeyFrameAnimation ani = compositor.CreateVector3KeyFrameAnimation(); @@ -277,7 +267,6 @@ public static class CompositionManager /// The animation duration /// The optional initial delay for the animation /// The optional easing function for the animation - [PublicAPI] [Pure, NotNull] public static CompositionAnimation CreateMatrix4x4KeyFrameAnimation([NotNull] this Compositor compositor, Matrix4x4? from, Matrix4x4 to, TimeSpan duration, TimeSpan? delay, [CanBeNull] CompositionEasingFunction ease = null) diff --git a/UICompositionAnimations/Composition/ExpressionAnimationWithScalarParameter.cs b/UICompositionAnimations/Composition/ExpressionAnimationWithScalarParameter.cs index bd77d37..e0770d2 100644 --- a/UICompositionAnimations/Composition/ExpressionAnimationWithScalarParameter.cs +++ b/UICompositionAnimations/Composition/ExpressionAnimationWithScalarParameter.cs @@ -18,7 +18,7 @@ public sealed class ExpressionAnimationWithScalarParameter : DependencyObject public ExpressionAnimation Animation { get; } // Variable parameter name - private readonly String ParameterName; + private readonly string ParameterName; // Target property set with the variable parameter private readonly CompositionPropertySet PropertySet; @@ -30,7 +30,7 @@ public sealed class ExpressionAnimationWithScalarParameter : DependencyObject /// The property set with the custom parameter /// The name of the custom parameter that will be updated when requested internal ExpressionAnimationWithScalarParameter([NotNull] ExpressionAnimation animation, - [NotNull] CompositionPropertySet propertySet, [NotNull] String parameterName) + [NotNull] CompositionPropertySet propertySet, [NotNull] string parameterName) { Animation = animation; PropertySet = propertySet; diff --git a/UICompositionAnimations/CompositionExtensions.cs b/UICompositionAnimations/CompositionExtensions.cs index 0ae33ed..c23ffe5 100644 --- a/UICompositionAnimations/CompositionExtensions.cs +++ b/UICompositionAnimations/CompositionExtensions.cs @@ -6,6 +6,7 @@ using Windows.Foundation; using Windows.UI; using Windows.UI.Composition; +using Windows.UI.Core; using Windows.UI.Xaml.Controls; using JetBrains.Annotations; using UICompositionAnimations.Composition; @@ -19,6 +20,7 @@ namespace UICompositionAnimations /// /// A static class that wraps the animation methods in the Windows.UI.Composition namespace /// + [PublicAPI] public static class CompositionExtensions { #region Internal tools @@ -1522,8 +1524,8 @@ void Handler(object s, RoutedEventArgs e) int ms, int? msDelay, [NotNull] CompositionEasingFunction easingFunction) { // Setup - InsetClip clip = visual.Clip as InsetClip ?? ((InsetClip)(visual.Clip = visual.Compositor.CreateInsetClip())); - String property; + InsetClip clip = visual.Clip as InsetClip ?? (InsetClip)(visual.Clip = visual.Compositor.CreateInsetClip()); + string property; switch (side) { case MarginSide.Top: @@ -1622,7 +1624,7 @@ void Handler(object s, RoutedEventArgs e) TranslationAxis sourceXY, TranslationAxis? targetXY = null, bool invertSourceAxis = false) { CompositionPropertySet scrollSet = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(scroller); - String sign = invertSourceAxis ? "-" : String.Empty; + string sign = invertSourceAxis ? "-" : string.Empty; ExpressionAnimation animation = scrollSet.Compositor.CreateExpressionAnimation($"{sign}scroll.Translation.{sourceXY}"); animation.SetReferenceParameter("scroll", scrollSet); element.GetVisual().StartAnimation($"Offset.{targetXY ?? sourceXY}", animation); @@ -1646,7 +1648,7 @@ void Handler(object s, RoutedEventArgs e) { // Get the property set and setup the scroller offset sign CompositionPropertySet scrollSet = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(scroller); - String sign = invertSourceAxis ? "-" : "+"; + string sign = invertSourceAxis ? "-" : "+"; // Prepare the second property set to insert the additional parameter CompositionPropertySet properties = scroller.GetVisual().Compositor.CreatePropertySet(); @@ -1719,7 +1721,6 @@ void Handler(object s, RoutedEventArgs e) case Shape shape: shadow.Mask = shape.GetAlphaMask(); break; case Image image: shadow.Mask = image.GetAlphaMask(); break; case TextBlock textBlock: shadow.Mask = textBlock.GetAlphaMask(); break; - default: break; } if (apply) ElementCompositionPreview.SetElementChildVisual(target, sprite); return sprite; @@ -1735,7 +1736,7 @@ void Handler(object s, RoutedEventArgs e) /// The target object /// The name of the property to animate /// The final value of the property - public static void SetInstantValue(this CompositionObject compObject, String property, float value) + public static void SetInstantValue([NotNull] this CompositionObject compObject, string property, float value) { // Stop previous animations compObject.StopAnimation(property); @@ -1754,7 +1755,7 @@ public static void SetInstantValue(this CompositionObject compObject, String pro /// The name of the property to animate /// The final value of the property /// The animation duration - public static Task StartAnimationAsync(this CompositionObject compObject, String property, float value, TimeSpan duration) + public static Task StartAnimationAsync([NotNull] this CompositionObject compObject, string property, float value, TimeSpan duration) { // Stop previous animations compObject.StopAnimation(property); @@ -1778,11 +1779,11 @@ public static Task StartAnimationAsync(this CompositionObject compObject, String /// /// The target element /// The names of the animations to stop - public static void StopAnimations(this UIElement element, params String[] properties) + public static void StopAnimations([NotNull] this UIElement element, [NotNull, ItemNotNull] params string[] properties) { - if (properties == null || properties.Length == 0) return; + if (properties.Length == 0) return; Visual visual = element.GetVisual(); - foreach (String property in properties) visual.StopAnimation(property); + foreach (string property in properties) visual.StopAnimation(property); } /// @@ -1792,7 +1793,7 @@ public static void StopAnimations(this UIElement element, params String[] proper /// The X value of the scale property /// The Y value of the scale property /// The Z value of the scale property - public static void SetVisualScale(this UIElement element, float? x, float? y, float? z) + public static void SetVisualScale([NotNull] this UIElement element, float? x, float? y, float? z) { // Get the default values and set the CenterPoint Visual visual = element.GetVisual(); @@ -1815,7 +1816,7 @@ public static void SetVisualScale(this UIElement element, float? x, float? y, fl /// The X value of the scale property /// The Y value of the scale property /// The Z value of the scale property - public static async Task SetVisualScaleAsync(this FrameworkElement element, float? x, float? y, float? z) + public static async Task SetVisualScaleAsync([NotNull] this FrameworkElement element, float? x, float? y, float? z) { // Get the default values and set the CenterPoint Visual visual = element.GetVisual(); @@ -1838,7 +1839,7 @@ public static async Task SetVisualScaleAsync(this FrameworkElement element, floa /// The target element /// The offset axis to edit /// The final offset value to set for that axis - public static void SetVisualOffset(this UIElement element, TranslationAxis axis, float offset) + public static void SetVisualOffset([NotNull] this UIElement element, TranslationAxis axis, float offset) { // Get the element visual and stop the animation Visual visual = element.GetVisual(); @@ -1856,7 +1857,7 @@ public static void SetVisualOffset(this UIElement element, TranslationAxis axis, /// The target element /// The x offset /// The y offset - public static void SetVisualOffset(this UIElement element, float x, float y) + public static void SetVisualOffset([NotNull] this UIElement element, float x, float y) { // Get the element visual and stop the animation Visual visual = element.GetVisual(); @@ -1872,7 +1873,7 @@ public static void SetVisualOffset(this UIElement element, float x, float y) /// The to edit /// The offset axis to set /// The new value for the axis to set - public static Task SetVisualOffsetAsync(this UIElement element, TranslationAxis axis, float value) + public static Task SetVisualOffsetAsync([NotNull] this UIElement element, TranslationAxis axis, float value) { Visual visual = element.GetVisual(); return Task.Run(() => @@ -1890,7 +1891,7 @@ public static Task SetVisualOffsetAsync(this UIElement element, TranslationAxis /// The to edit /// The x offset /// The y offset - public static Task SetVisualOffsetAsync(this UIElement element, float x, float y) + public static Task SetVisualOffsetAsync([NotNull] this UIElement element, float x, float y) { Visual visual = element.GetVisual(); return Task.Run(() => @@ -1904,7 +1905,7 @@ public static Task SetVisualOffsetAsync(this UIElement element, float x, float y /// Returns the visual offset for a target /// /// The input element - public static Vector3 GetVisualOffset(this UIElement element) => element.GetVisual().Offset; + public static Vector3 GetVisualOffset([NotNull] this UIElement element) => element.GetVisual().Offset; /// /// Sets the translation property of the visual object for a given object @@ -1912,7 +1913,7 @@ public static Task SetVisualOffsetAsync(this UIElement element, float x, float y /// The target element /// The translation axis to edit /// The final translation value to set for that axis - public static void SetVisualTranslation(this UIElement element, TranslationAxis axis, float translation) + public static void SetVisualTranslation([NotNull] this UIElement element, TranslationAxis axis, float translation) { // Get the element visual and stop the animation Visual visual = element.GetVisual(); @@ -1940,7 +1941,7 @@ public static void SetVisualTranslation(this UIElement element, TranslationAxis /// The target element /// The x translation /// The y translation - public static void SetVisualTranslation(this UIElement element, float x, float y) + public static void SetVisualTranslation([NotNull] this UIElement element, float x, float y) { // Get the element visual and stop the animation Visual visual = element.GetVisual(); @@ -1954,7 +1955,7 @@ public static void SetVisualTranslation(this UIElement element, float x, float y /// /// The target element /// The desired clip margins to set - public static void SetVisualClip(this UIElement element, Thickness clip) + public static void SetVisualClip([NotNull] this UIElement element, Thickness clip) { // Get the element visual Visual visual = element.GetVisual(); @@ -1974,7 +1975,7 @@ public static void SetVisualClip(this UIElement element, Thickness clip) /// The target element /// The desired clip value to set /// The target clip side to update - public static void SetVisualClip(this UIElement element, float clip, MarginSide side) + public static void SetVisualClip([NotNull] this UIElement element, float clip, MarginSide side) { // Get the element visual Visual visual = element.GetVisual(); @@ -1995,7 +1996,7 @@ public static void SetVisualClip(this UIElement element, float clip, MarginSide /// Resets the scale, offset and opacity properties for a framework element /// /// The element to edit - public static async Task ResetCompositionVisualPropertiesAsync(this FrameworkElement element) + public static async Task ResetCompositionVisualPropertiesAsync([NotNull] this FrameworkElement element) { // Get the default values and set the CenterPoint Visual visual = element.GetVisual(); @@ -2014,20 +2015,73 @@ public static async Task ResetCompositionVisualPropertiesAsync(this FrameworkEle /// Gets the opacity for the Visual object behind a given UIElement /// /// The source UIElement - public static float GetVisualOpacity(this UIElement element) => element.GetVisual().Opacity; + public static float GetVisualOpacity([NotNull] this UIElement element) => element.GetVisual().Opacity; /// /// Sets the opacity for the Visual object behind a given UIElement /// /// The source UIElement /// The new opacity value - public static void SetVisualOpacity(this UIElement element, float value) => element.GetVisual().Opacity = value; + public static void SetVisualOpacity([NotNull] this UIElement element, float value) => element.GetVisual().Opacity = value; /// /// Returns the Visual object for a given UIElement /// /// The source UIElement - public static Visual GetVisual(this UIElement element) => ElementCompositionPreview.GetElementVisual(element); + public static Visual GetVisual([NotNull] this UIElement element) => ElementCompositionPreview.GetElementVisual(element); + + /// + /// Adds a instance on top of the target and binds the size of the two items with an expression animation + /// + /// The instance to display + /// The target that will host the effect + public static void AttachToElement([NotNull] this CompositionBrush brush, [NotNull] FrameworkElement target) + { + // Add the brush to a sprite and attach it to the target element + SpriteVisual sprite = Window.Current.Compositor.CreateSpriteVisual(); + sprite.Brush = brush; + sprite.Size = new Vector2((float)target.ActualWidth, (float)target.ActualHeight); + ElementCompositionPreview.SetElementChildVisual(target, sprite); + + // Keep the sprite size in sync + sprite.BindSize(target); + } + + /// + /// Starts an expression animation to keep the size of the source in sync with the target + /// + /// The composition object to start the animation on + /// The target to read the size updates from + public static void BindSize([NotNull] this CompositionObject source, [NotNull] UIElement target) + { + Visual visual = target.GetVisual(); + ExpressionAnimation bindSizeAnimation = Window.Current.Compositor.CreateExpressionAnimation($"{nameof(visual)}.Size"); + bindSizeAnimation.SetReferenceParameter(nameof(visual), visual); + + // Start the animation + source.StartAnimation("Size", bindSizeAnimation); + } + + /// + /// Tries to retrieve the instance of the input + /// + /// The source instance + /// The resulting , if existing + [MustUseReturnValue] + public static bool TryGetDispatcher([NotNull] this CompositionObject source, out CoreDispatcher dispatcher) + { + try + { + dispatcher = source.Dispatcher; + return true; + } + catch (ObjectDisposedException) + { + // I'm sorry Jack, I was too late! :'( + dispatcher = null; + return false; + } + } #endregion } diff --git a/UICompositionAnimations/Behaviours/Effects/AcrylicEffectMode.cs b/UICompositionAnimations/Enums/AcrylicEffectMode.cs similarity index 89% rename from UICompositionAnimations/Behaviours/Effects/AcrylicEffectMode.cs rename to UICompositionAnimations/Enums/AcrylicEffectMode.cs index d7f3b7d..11b02f1 100644 --- a/UICompositionAnimations/Behaviours/Effects/AcrylicEffectMode.cs +++ b/UICompositionAnimations/Enums/AcrylicEffectMode.cs @@ -1,6 +1,6 @@ using System; -namespace UICompositionAnimations.Behaviours.Effects +namespace UICompositionAnimations.Enums { /// /// Indicates the UI mode for an acrylic effect brush diff --git a/UICompositionAnimations/Enums/BitmapCacheMode.cs b/UICompositionAnimations/Enums/BitmapCacheMode.cs index a50b52b..451a86e 100644 --- a/UICompositionAnimations/Enums/BitmapCacheMode.cs +++ b/UICompositionAnimations/Enums/BitmapCacheMode.cs @@ -1,23 +1,26 @@ -namespace UICompositionAnimations.Enums +using JetBrains.Annotations; + +namespace UICompositionAnimations.Enums { /// - /// Indicates the cache mode to use when loading an item + /// Indicates the cache mode to use when loading a Win2D image /// + [PublicAPI] public enum BitmapCacheMode { /// - /// The new item will be either loaded from the cache when possible, or saved in the cache for future use + /// The default behavior, the cache is enabled /// - EnableCaching, + Default, /// - /// The item will not be loaded from the cache, but it will be stored in the cache for future use if possible + /// Reload the target image and overwrite the cached entry, if it exists /// - DisableCachingOnRead, + Overwrite, /// - /// The new item will not be loaded from the cache and it will not be saved into the cache either + /// The cache is disabled and new images are always reloaded /// - DisableCaching + Disabled } } \ No newline at end of file diff --git a/UICompositionAnimations/Enums/EffectPlacement.cs b/UICompositionAnimations/Enums/EffectPlacement.cs new file mode 100644 index 0000000..5c61248 --- /dev/null +++ b/UICompositionAnimations/Enums/EffectPlacement.cs @@ -0,0 +1,22 @@ +using Windows.Graphics.Effects; +using JetBrains.Annotations; + +namespace UICompositionAnimations.Enums +{ + /// + /// An used to modify the default placement of the input instance in a blend operation + /// + [PublicAPI] + public enum EffectPlacement + { + /// + /// The instance used to call the blend method is placed on top of the other + /// + Foreground, + + /// + /// The instance used to call the blend method is placed behind the other + /// + Background + } +} \ No newline at end of file diff --git a/UICompositionAnimations/Enums/ImplicitAnimationType.cs b/UICompositionAnimations/Enums/ImplicitAnimationType.cs index 932493a..191a086 100644 --- a/UICompositionAnimations/Enums/ImplicitAnimationType.cs +++ b/UICompositionAnimations/Enums/ImplicitAnimationType.cs @@ -1,12 +1,21 @@ -#pragma warning disable 1591 +using JetBrains.Annotations; + namespace UICompositionAnimations.Enums { /// /// Indicates the type of an implicit composition animation /// + [PublicAPI] public enum ImplicitAnimationType { + /// + /// The animation plays when the item becomes visible + /// Show, + + /// + /// The animation plays when the item is collapsed + /// Hide } } \ No newline at end of file diff --git a/UICompositionAnimations/Enums/TranslationAxis.cs b/UICompositionAnimations/Enums/TranslationAxis.cs index 71d4e94..8c68a6d 100644 --- a/UICompositionAnimations/Enums/TranslationAxis.cs +++ b/UICompositionAnimations/Enums/TranslationAxis.cs @@ -1,12 +1,21 @@ -#pragma warning disable 1591 +using JetBrains.Annotations; + namespace UICompositionAnimations.Enums { /// /// Indicates the translation axis to use in an animation /// + [PublicAPI] public enum TranslationAxis { + /// + /// Horizontal translation + /// X, + + /// + /// Vertical translation + /// Y } } \ No newline at end of file diff --git a/UICompositionAnimations/Helpers/ApiInformationHelper.cs b/UICompositionAnimations/Helpers/ApiInformationHelper.cs deleted file mode 100644 index 4a3790a..0000000 --- a/UICompositionAnimations/Helpers/ApiInformationHelper.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using Windows.ApplicationModel; -using Windows.ApplicationModel.Resources.Core; -using Windows.Foundation.Collections; -using Windows.Foundation.Metadata; -using Windows.System.Profile; -using Windows.UI.Composition; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Hosting; -using Windows.UI.Xaml.Input; - -namespace UICompositionAnimations.Helpers -{ - /// - /// A static class that provides useful information on the available APIs - /// - public static class ApiInformationHelper - { - #region OS build - - static Version _OSVersion; - - /// - /// Gets the current OS version for the device in use - /// - /// It is better to check the available APIs directly with the other properties exposed by the class - /// instead of just checking the OS version and use it to make further decisions. - /// In particular, users on a Windows Insider ring could already have a given set of APIs even though their OS - /// build doesn't exactly match the official Windows 10 version that supports a requested API contract. - public static Version OSVersion - { - get - { - if (_OSVersion == null) - { - string deviceFamilyVersion = AnalyticsInfo.VersionInfo.DeviceFamilyVersion; - ulong version = ulong.Parse(deviceFamilyVersion); - ulong major = (version & 0xFFFF000000000000L) >> 48; - ulong minor = (version & 0x0000FFFF00000000L) >> 32; - ulong build = (version & 0x00000000FFFF0000L) >> 16; - ulong revision = version & 0x000000000000FFFFL; - _OSVersion = new Version((int)major, (int)minor, (int)build, (int)revision); - } - return _OSVersion; - } - } - - /// - /// Gets whether or not the current OS version is at least the Anniversary Update (14393) - /// - /// - public static bool IsAnniversaryUpdateOrLater => OSVersion.Build > 14393; - - /// - /// Gets whether or not the current OS version is at least the Creator's Update (15063) - /// - /// - public static bool IsCreatorsUpdateOrLater => OSVersion.Build > 15063; - - /// - /// Gets whether or not the current OS version is at least the Fall Creator's Update (16xxx) - /// - /// - public static bool IsFallCreatorsUpdateOrLater => OSVersion.Build > 16299; - - #endregion - - #region Available APIs - - /// - /// Gets whether or not the connected animations APIs are available - /// - public static bool AreConnectedAnimationsAvailable => ApiInformation.IsTypePresent("Windows.UI.Xaml.Media.Animation.ConnectedAnimationService"); - - /// - /// Gets whether or not the APIs to find the focusable elements are available - /// - public static bool IsFindFirstFocusableElementAvailable => ApiInformation.IsMethodPresent("Windows.UI.Xaml.Input.FocusManager", nameof(FocusManager.FindFirstFocusableElement)); - - /// - /// Gets or sets whether the Target property of the composition animation APIs is available - /// - public static bool SupportsCompositionAnimationTarget => ApiInformation.IsPropertyPresent("Windows.UI.Composition.CompositionAnimation", nameof(CompositionAnimation.Target)); - - /// - /// Gets whether or not the composition animation group type is present - /// - public static bool IsCompositionAnimationGroupAvailable => ApiInformation.IsTypePresent("Windows.UI.Composition.CompositionAnimationGroup"); - - /// - /// Gets whether or not the implicit animations APIs are available - /// - /// - public static bool AreImplicitAnimationsAvailable => ApiInformation.IsMethodPresent("Windows.UI.Composition.Compositor", nameof(Compositor.CreateImplicitAnimationCollection)); - - /// - /// Gets whether or not the implicit show/hide animation APIs are available - /// - public static bool AreImplicitShowHideAnimationsAvailable => ApiInformation.IsMethodPresent("Windows.UI.Xaml.Hosting.ElementCompositionPreview", nameof(ElementCompositionPreview.SetImplicitShowAnimation)); - - /// - /// Gets whether or not the XAML lights APIs are available - /// - public static bool AreXamlLightsSupported => ApiInformation.IsTypePresent("Windows.UI.Xaml.Media.XamlLight"); - - /// - /// Gets whether or not the custom XAML brush APIs are available - /// - public static bool AreCustomBrushesSupported => ApiInformation.IsTypePresent("Windows.UI.Xaml.Media.XamlCompositionBrushBase"); - - /// - /// Gets whether or not the XYFocusUp property is available/> - /// - /// - public static bool IsXYFocusAvailable => ApiInformation.IsPropertyPresent("Windows.UI.Xaml.Controls.Control", nameof(Control.XYFocusUp)); - - /// - /// Gets whether or not the host backdrop effects are available - /// - /// - public static bool AreHostBackdropEffectsAvailable => ApiInformation.IsTypePresent("Microsoft.Graphics.Canvas.Effects.GaussianBlurEffect") && - ApiInformation.IsMethodPresent("Windows.UI.Composition.Compositor", nameof(Compositor.CreateHostBackdropBrush)); - - /// - /// Gets whether or not the backdrop effects APIs are available - /// - /// - public static bool AreBackdropEffectsAvailable => ApiInformation.IsTypePresent("Microsoft.Graphics.Canvas.Effects.GaussianBlurEffect") && - ApiInformation.IsMethodPresent("Windows.UI.Composition.Compositor", nameof(Compositor.CreateBackdropBrush)); - - /// - /// Gets whether or not the drop shadows APIs are available - /// - public static bool AreDropShadowsAvailable => ApiInformation.IsMethodPresent("Windows.UI.Composition.Compositor", nameof(Compositor.CreateDropShadow)); - - /// - /// Gets whether or not the staggering method is available - /// - /// - public static bool IsRepositionStaggerringAvailable => ApiInformation.IsMethodPresent("Windows.UI.Xaml.Media.Animation.RepositionThemeTransition", "IsStaggeringEnabled"); - - /// - /// Gets whether or not the RequiresPointer property is available - /// - public static bool IsRequiresPointerAvailable => ApiInformation.IsPropertyPresent("Windows.UI.Xaml.Controls.Control", nameof(Control.RequiresPointer)); - - #endregion - - #region Device family - - private static bool? _IsMobileDevice; - - /// - /// Gets whether or not the device is a mobile phone - /// - public static bool IsMobileDevice - { - get - { - if (_IsMobileDevice == null) - { - try - { - IObservableMap qualifiers = ResourceContext.GetForCurrentView().QualifierValues; - _IsMobileDevice = qualifiers.ContainsKey("DeviceFamily") && qualifiers["DeviceFamily"] == "Mobile"; - } - catch (UnauthorizedAccessException) - { - // No idea why this should happen - return ApiInformation.IsTypePresent("Windows.Phone.UI.Input.HardwareButtons"); - } - } - return _IsMobileDevice.Value; - } - } - - private static bool? _IsDesktop; - - /// - /// Gets whether or not the device is running Windows 10 Desktop - /// - public static bool IsDesktop - { - get - { - if (_IsDesktop == null) - { - try - { - IObservableMap qualifiers = ResourceContext.GetForCurrentView().QualifierValues; - _IsDesktop = qualifiers.ContainsKey("DeviceFamily") && qualifiers["DeviceFamily"] == "Desktop"; - } - catch (UnauthorizedAccessException) - { - // Weird crash, but still... - return false; - } - } - return _IsDesktop.Value; - } - } - - #endregion - } -} diff --git a/UICompositionAnimations/Helpers/AsyncMutex.cs b/UICompositionAnimations/Helpers/AsyncMutex.cs new file mode 100644 index 0000000..663a6c5 --- /dev/null +++ b/UICompositionAnimations/Helpers/AsyncMutex.cs @@ -0,0 +1,42 @@ +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +namespace System.Threading.Tasks +{ + /// + /// An implementation that can be easily used inside a block + /// + internal sealed class AsyncMutex + { + // The underlying semaphore used by this instance + [NotNull] + private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1); + + /// + /// Acquires a lock for the current instance, that is automatically released outside the block + /// + [NotNull, ItemNotNull] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public async Task LockAsync() + { + await Semaphore.WaitAsync().ConfigureAwait(false); + return new _Lock(Semaphore); + } + + /// + /// Private class that implements the automatic release of the semaphore + /// + private sealed class _Lock : IDisposable + { + // Reference to the semaphore instance of the parent class + [NotNull] + private readonly SemaphoreSlim Semaphore; + + public _Lock([NotNull] SemaphoreSlim semaphore) => Semaphore = semaphore; + + // Releases the lock when the instance is disposed + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void IDisposable.Dispose() => Semaphore.Release(); + } + } +} \ No newline at end of file diff --git a/UICompositionAnimations/Helpers/ColorConverter.cs b/UICompositionAnimations/Helpers/ColorConverter.cs index 221c8b8..e72ede3 100644 --- a/UICompositionAnimations/Helpers/ColorConverter.cs +++ b/UICompositionAnimations/Helpers/ColorConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using Windows.UI; namespace UICompositionAnimations.Helpers @@ -10,15 +9,15 @@ namespace UICompositionAnimations.Helpers internal static class ColorConverter { /// - /// Returns the Color represented by the hex String + /// Returns the Color represented by the hex string /// /// If it contains just the RGB values {RRBBGG} the Alpha channel is automatically set to FF - public static Color String2Color(String color) + public static Color String2Color(string color) { - //Cancels the # symbol from the String, if present + //Cancels the # symbol from the string, if present if (color.Contains('#')) color = color.Substring(1); byte alpha; - String RGB; + string RGB; if (color.Length == 6) { alpha = 255; diff --git a/UICompositionAnimations/Helpers/DispatcherHelper.cs b/UICompositionAnimations/Helpers/DispatcherHelper.cs index d3fbe94..02dc43e 100644 --- a/UICompositionAnimations/Helpers/DispatcherHelper.cs +++ b/UICompositionAnimations/Helpers/DispatcherHelper.cs @@ -9,6 +9,7 @@ namespace UICompositionAnimations.Helpers /// /// A static class that manages the UI dispatching from background threads /// + [PublicAPI] public static class DispatcherHelper { #region Target dispatcher helpers @@ -18,17 +19,10 @@ public static class DispatcherHelper /// /// The target dispatcher to use to schedule the callback execution /// The action to execute on the UI thread - [PublicAPI] public static void Run([NotNull] this CoreDispatcher dispatcher, [NotNull] Action callback) { if (dispatcher.HasThreadAccess) callback(); - else - { - dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => - { - callback(); - }).Forget(); - } + else dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => callback()).Forget(); } /// @@ -36,17 +30,10 @@ public static void Run([NotNull] this CoreDispatcher dispatcher, [NotNull] Actio /// /// The target dispatcher to use to schedule the callback execution /// The action to execute on the UI thread - [PublicAPI] public static void Run([NotNull] this CoreDispatcher dispatcher, [NotNull] Func asyncCallback) { - // Check the current thread if (dispatcher.HasThreadAccess) asyncCallback(); - - // Schedule on the UI thread if necessary - dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => - { - asyncCallback(); - }).Forget(); + else dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => asyncCallback()).Forget(); } /// @@ -54,7 +41,6 @@ public static void Run([NotNull] this CoreDispatcher dispatcher, [NotNull] Func< /// /// The target dispatcher to use to schedule the callback execution /// The action to execute on the UI thread - [PublicAPI] public static async Task RunAsync([NotNull] this CoreDispatcher dispatcher, [NotNull] Action callback) { if (dispatcher.HasThreadAccess) callback(); @@ -75,13 +61,9 @@ public static async Task RunAsync([NotNull] this CoreDispatcher dispatcher, [Not /// /// The target dispatcher to use to schedule the callback execution /// The action to execute on the UI thread - [PublicAPI] public static Task RunAsync([NotNull] this CoreDispatcher dispatcher, [NotNull] Func asyncCallback) { - // Check the current thread if (dispatcher.HasThreadAccess) return asyncCallback(); - - // Schedule on the UI thread if necessary TaskCompletionSource tcs = new TaskCompletionSource(); dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { @@ -96,17 +78,16 @@ public static Task RunAsync([NotNull] this CoreDispatcher dispatcher, [NotNull] /// The return type /// The target dispatcher to use to schedule the callback execution /// The function to execute on the UI thread - [PublicAPI] - public static async ValueTask GetAsync([NotNull] this CoreDispatcher dispatcher, [NotNull] Func function) + public static Task GetAsync([NotNull] this CoreDispatcher dispatcher, [NotNull] Func function) { - if (dispatcher.HasThreadAccess) return function(); + if (dispatcher.HasThreadAccess) return Task.FromResult(function()); TaskCompletionSource tcs = new TaskCompletionSource(); dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { T result = function(); tcs.SetResult(result); }).Forget(); - return await tcs.Task; + return tcs.Task; } /// @@ -115,13 +96,9 @@ public static async ValueTask GetAsync([NotNull] this CoreDispatcher dispa /// The return type /// The target dispatcher to use to schedule the callback execution /// The async function to execute on the UI thread - [PublicAPI] public static Task GetAsync([NotNull] this CoreDispatcher dispatcher, [NotNull] Func> function) { - // Check the current thread if (dispatcher.HasThreadAccess) return function(); - - // Schedule on the UI thread if necessary TaskCompletionSource> tcs = new TaskCompletionSource>(); dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { @@ -151,28 +128,24 @@ public static Task GetAsync([NotNull] this CoreDispatcher dispatcher, [Not /// Executes a given action on the UI thread without awaiting the operation /// /// The action to execute on the UI thread - [PublicAPI] public static void RunOnUIThread([NotNull] Action callback) => CoreDispatcher.Run(callback); /// /// Executes a given async action on the UI thread without awaiting the operation /// /// The action to execute on the UI thread - [PublicAPI] public static void RunOnUIThread([NotNull] Func asyncCallback) => CoreDispatcher.Run(asyncCallback); /// /// Executes a given action on the UI thread and waits for it to be completed /// /// The action to execute on the UI thread - [PublicAPI] public static Task RunOnUIThreadAsync([NotNull] Action callback) => CoreDispatcher.RunAsync(callback); /// /// Executes a given action on the UI thread and waits for it to be completed /// /// The action to execute on the UI thread - [PublicAPI] public static Task RunOnUIThreadAsync([NotNull] Func asyncCallback) => CoreDispatcher.RunAsync(asyncCallback); /// @@ -180,15 +153,13 @@ public static Task GetAsync([NotNull] this CoreDispatcher dispatcher, [Not /// /// The return type /// The function to execute on the UI thread - [PublicAPI] - public static ValueTask GetFromUIThreadAsync([NotNull] Func function) => CoreDispatcher.GetAsync(function); + public static Task GetFromUIThreadAsync([NotNull] Func function) => CoreDispatcher.GetAsync(function); /// /// Executes a given async function on the UI thread and returns its result /// /// The return type /// The async function to execute on the UI thread - [PublicAPI] public static Task GetFromUIThreadAsync([NotNull] Func> function) => CoreDispatcher.GetAsync(function); #endregion diff --git a/UICompositionAnimations/Helpers/Extensions.cs b/UICompositionAnimations/Helpers/Extensions.cs index e98734c..f20972f 100644 --- a/UICompositionAnimations/Helpers/Extensions.cs +++ b/UICompositionAnimations/Helpers/Extensions.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; -using System.Threading.Tasks; using Windows.Foundation; +using JetBrains.Annotations; namespace UICompositionAnimations.Helpers { @@ -44,10 +46,31 @@ internal static class Extensions public static void Forget(this IAsyncAction action) { } /// - /// Suppresses the warnings when calling an async method without awaiting it + /// Merges the two input instances and makes sure no duplicate keys are present /// - /// The task returned by the async call - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Forget(this Task task) { } + /// The first to merge + /// The second to merge + [Pure, NotNull] + public static IReadOnlyDictionary Merge( + [NotNull] this IReadOnlyDictionary a, + [NotNull] IReadOnlyDictionary b) + { + if (a.Keys.FirstOrDefault(b.ContainsKey) is TKey key) + throw new InvalidOperationException($"The key {key} already exists in the current pipeline"); + return new Dictionary(a.Concat(b)); + } + + /// + /// Merges the two input instances and makes sure no duplicate items are present + /// + /// The first to merge + /// The second to merge + [Pure, NotNull, ItemNotNull] + public static IReadOnlyCollection Merge([NotNull, ItemNotNull] this IReadOnlyCollection a, [NotNull, ItemNotNull] IReadOnlyCollection b) + { + if (a.FirstOrDefault(b.Contains) is T animation) + throw new InvalidOperationException($"The animation {animation} already exists in the current pipeline"); + return a.Concat(b).ToArray(); + } } } diff --git a/UICompositionAnimations/Helpers/PointerEvents/PointerHelper.cs b/UICompositionAnimations/Helpers/PointerEvents/PointerHelper.cs index a8caeab..57fbea2 100644 --- a/UICompositionAnimations/Helpers/PointerEvents/PointerHelper.cs +++ b/UICompositionAnimations/Helpers/PointerEvents/PointerHelper.cs @@ -9,89 +9,74 @@ namespace UICompositionAnimations.Helpers.PointerEvents /// /// A static class with some extension methods to manage different pointer states /// + [PublicAPI] public static class PointerHelper { /// - /// Adds an event handler to all the pointer events of the target control + /// Adds an event handler to the following targets of the input : + /// UIElement.PointerExitedEvent, UIElement.PointerCaptureLostEvent, UIElement.PointerCanceledEvent, UIElement.PointerEnteredEvent, UIElement.PointerReleasedEvent. + /// Some additional handling of the input device is also performed automatically to filter out some useful input combinations (eg. disable touch input for the case). /// - /// The control to monitor - /// An action to call every time a pointer event is raised. The bool parameter - /// indicates whether the pointer is moving to or from the control - [PublicAPI] + /// The to monitor + /// An to call every time a pointer event is raised. The parameter indicates whether the pointer is moving to or from the control [NotNull] - public static ControlAttachedHandlersInfo ManageControlPointerStates(this UIElement control, Action action) + public static ControlAttachedHandlersInfo ManageControlPointerStates([NotNull] this UIElement control, [NotNull] Action action) { - // Nested functions that adds the actual handlers - PointerHandlerInfo AddHandler(RoutedEvent @event, bool state, Func predicate) - { - PointerEventHandler handler = (_, e) => - { - if (predicate == null || predicate(e.Pointer.PointerDeviceType)) - { - action(e.Pointer.PointerDeviceType, state); - } - }; - control.AddHandler(@event, handler, true); - return new PointerHandlerInfo(@event, handler); - } - - // Add handlers return new ControlAttachedHandlersInfo(control, - AddHandler(UIElement.PointerExitedEvent, false, null), - AddHandler(UIElement.PointerCaptureLostEvent, false, null), - AddHandler(UIElement.PointerCanceledEvent, false, null), - AddHandler(UIElement.PointerEnteredEvent, true, p => p != PointerDeviceType.Touch), - AddHandler(UIElement.PointerReleasedEvent, false, p => p == PointerDeviceType.Touch)); + AddHandler(control, UIElement.PointerExitedEvent, false, null, action), + AddHandler(control, UIElement.PointerCaptureLostEvent, false, null, action), + AddHandler(control, UIElement.PointerCanceledEvent, false, null, action), + AddHandler(control, UIElement.PointerEnteredEvent, true, p => p != PointerDeviceType.Touch, action), + AddHandler(control, UIElement.PointerReleasedEvent, false, p => p == PointerDeviceType.Touch, action)); } /// - /// Adds an event handler to all the pointer events of the target element + /// Adds an event handler to the following targets of the input : UIElement.PointerExitedEvent, UIElement.PointerMovedEvent. + /// This is especially useful to check whether or not the user is currently moving the input cursor above a host control, as this method will correctly handle scenarios + /// where the cursor would be considered as exited from the target control, while still being inside its area. + /// For example, this could happen if the user clicked on a nested control inside the target host, without actually moving the cursor away. /// - /// The element to monitor - /// An action to call every time a pointer event is raised. The bool parameter - /// indicates whether the pointer is moving to or from the control - [PublicAPI] + /// The to monitor + /// An to call every time a pointer event is raised. The parameter indicates whether the pointer is moving to or from the control [NotNull] - public static ControlAttachedHandlersInfo ManageHostPointerStates(this UIElement host, Action action) + public static ControlAttachedHandlersInfo ManageHostPointerStates([NotNull] this UIElement host, [NotNull] Action action) { - // Nested functions that adds the actual handlers - PointerHandlerInfo AddHandler(RoutedEvent @event, bool state, Func predicate) - { - PointerEventHandler handler = (_, e) => - { - if (predicate == null || predicate(e.Pointer.PointerDeviceType)) - { - action(e.Pointer.PointerDeviceType, state); - } - }; - host.AddHandler(@event, handler, true); - return new PointerHandlerInfo(@event, handler); - } - - // Add handlers return new ControlAttachedHandlersInfo(host, - AddHandler(UIElement.PointerExitedEvent, false, null), - AddHandler(UIElement.PointerMovedEvent, true, p => p != PointerDeviceType.Touch)); + AddHandler(host, UIElement.PointerExitedEvent, false, null, action), + AddHandler(host, UIElement.PointerMovedEvent, true, p => p != PointerDeviceType.Touch, action)); } /// - /// Adds the appropriate handlers to a control to help setup the light effects (skipped when on a mobile phone) + /// Adds the appropriate handlers to a control to help setup the light effects (skipped when using a touch screen) /// - /// The element to monitor - /// An action to call every time the light effects state should be changed - [PublicAPI] - [CanBeNull] - public static ControlAttachedHandlersInfo ManageLightsPointerStates(this UIElement element, Action action) + /// The to monitor + /// An to call every time the light effects state should be changed + [NotNull] + public static ControlAttachedHandlersInfo ManageLightsPointerStates([NotNull] this UIElement element, [NotNull] Action action) { - // Platform check - if (ApiInformationHelper.IsMobileDevice) return null; - - // Nested functions that adds the actual handlers + bool state = false; return element.ManageHostPointerStates((pointer, value) => { - if (pointer != PointerDeviceType.Mouse) return; + if (pointer != PointerDeviceType.Mouse || state == value) return; + state = value; action(value); }); } + + // Private method that adds a custom handler to the target event and control + [NotNull] + private static PointerHandlerInfo AddHandler([NotNull] UIElement element, [NotNull] RoutedEvent @event, bool state, [CanBeNull] Func predicate, [NotNull] Action action) + { + // Callback with additional pointer type check + void Handler(object _, PointerRoutedEventArgs e) + { + if (predicate == null || predicate(e.Pointer.PointerDeviceType)) + action(e.Pointer.PointerDeviceType, state); + } + + // Handler setup + element.AddHandler(@event, (PointerEventHandler)Handler, true); + return new PointerHandlerInfo(@event, Handler); + } } } diff --git a/UICompositionAnimations/Helpers/Win2DImageHelper.cs b/UICompositionAnimations/Helpers/Win2DImageHelper.cs index 7ba2568..1686632 100644 --- a/UICompositionAnimations/Helpers/Win2DImageHelper.cs +++ b/UICompositionAnimations/Helpers/Win2DImageHelper.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Numerics; using System.Runtime.InteropServices; -using System.Threading; using System.Threading.Tasks; using Windows.Foundation; using Windows.Graphics.DirectX; @@ -16,6 +15,7 @@ using Microsoft.Graphics.Canvas.UI; using Microsoft.Graphics.Canvas.UI.Composition; using Microsoft.Graphics.Canvas.UI.Xaml; +using UICompositionAnimations.Brushes.Cache; using UICompositionAnimations.Enums; namespace UICompositionAnimations.Helpers @@ -30,15 +30,15 @@ public static class Win2DImageHelper /// private const int DeviceLostRecoveryThreshold = 1000; - /// - /// Gets a shared semaphore to avoid loading multiple Win2D resources at the same time - /// - private static readonly SemaphoreSlim Win2DSemaphore = new SemaphoreSlim(1); + // Synchronization mutex to access the cache and load Win2D images concurrently + [NotNull] + private static readonly AsyncMutex Win2DMutex = new AsyncMutex(); /// /// Gets the local cache mapping for previously loaded Win2D images /// - private static readonly IDictionary SurfacesCache = new Dictionary(); + [NotNull] + private static readonly ThreadSafeCompositionMapCache Cache = new ThreadSafeCompositionMapCache(); #region Public APIs @@ -47,11 +47,12 @@ public static class Win2DImageHelper /// /// The path to the image to load /// Indicates the desired DPI mode to use when loading the image + /// Indicates the cache option to use to load the image [PublicAPI] [Pure, ItemCanBeNull] - public static Task LoadImageAsync([NotNull] Uri uri, BitmapDPIMode dpiMode) + public static Task LoadImageAsync([NotNull] Uri uri, BitmapDPIMode dpiMode, BitmapCacheMode cache = BitmapCacheMode.Default) { - return LoadImageAsync(Window.Current.Compositor, uri, BitmapCacheMode.DisableCaching, dpiMode); + return LoadImageAsync(Window.Current.Compositor, uri, dpiMode, cache); } /// @@ -60,11 +61,12 @@ public static Task LoadImageAsync([NotNull] Uri uri, Bi /// The to use to load the target image /// The path to the image to load /// Indicates the desired DPI mode to use when loading the image + /// Indicates the cache option to use to load the image [PublicAPI] [Pure, ItemCanBeNull] - public static Task LoadImageAsync([NotNull] this CanvasControl canvas, [NotNull] Uri uri, BitmapDPIMode dpiMode) + public static Task LoadImageAsync([NotNull] this CanvasControl canvas, [NotNull] Uri uri, BitmapDPIMode dpiMode, BitmapCacheMode cache = BitmapCacheMode.Default) { - return LoadImageAsync(Window.Current.Compositor, canvas, uri, BitmapCacheMode.DisableCaching, dpiMode); + return LoadImageAsync(Window.Current.Compositor, canvas, uri, dpiMode, cache); } /// @@ -74,13 +76,10 @@ public static Task LoadImageAsync([NotNull] this Canvas /// The returned items should be manually disposed once checked that they are no longer being used in other effect pipelines [PublicAPI] [MustUseReturnValue, ItemNotNull] - public static async Task> ClearCacheAsync() + public static async Task> ClearCacheAsync() { - await Win2DSemaphore.WaitAsync(); - IEnumerable surfaces = SurfacesCache.Values; - SurfacesCache.Clear(); - Win2DSemaphore.Release(); - return surfaces; + using (await Win2DMutex.LockAsync()) + return Cache.Clear(); } #endregion @@ -120,13 +119,12 @@ public static async Task> ClearCacheAsync() bitmap = await CanvasBitmap.LoadAsync(creator, uri, dpi >= 96 ? dpi : 96); break; default: - throw new ArgumentOutOfRangeException("Unsupported DPI mode"); + throw new ArgumentOutOfRangeException(nameof(dpiMode), "Unsupported DPI mode"); } // Get the device and the target surface CompositionGraphicsDevice device = CanvasComposition.CreateCompositionGraphicsDevice(compositor, canvasDevice); - CompositionDrawingSurface surface = device.CreateDrawingSurface(default(Size), - DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); + CompositionDrawingSurface surface = device.CreateDrawingSurface(default, DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied); // Calculate the surface size Size @@ -158,6 +156,7 @@ public static async Task> ClearCacheAsync() { // Manual using block to allow the initial switch statement bitmap?.Dispose(); + Cache.Cleanup(); } } @@ -167,13 +166,14 @@ public static async Task> ClearCacheAsync() /// The compositor to use to render the Win2D image /// The to use to load the target image /// The path to the image to load - /// Indicates whether or not to force the reload of the Win2D image /// Indicates the desired DPI mode to use when loading the image + /// Indicates the cache option to use to load the image [ItemCanBeNull] - internal static async Task LoadImageAsync( - [NotNull] Compositor compositor, [NotNull] CanvasControl canvas, [NotNull] Uri uri, BitmapCacheMode options, BitmapDPIMode dpiMode) + internal static async Task LoadImageAsync([NotNull] Compositor compositor, [NotNull] CanvasControl canvas, [NotNull] Uri uri, BitmapDPIMode dpiMode, BitmapCacheMode cache) { TaskCompletionSource tcs = new TaskCompletionSource(); + + // Loads an image using the input CanvasDevice instance async Task LoadImageAsync(bool shouldThrow) { // Load the image - this will only succeed when there's an available Win2D device @@ -187,6 +187,8 @@ async Task LoadImageAsync(bool shouldThrow) return null; } } + + // Handler to create the Win2D image from the input CanvasControl async void Canvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args) { // Cancel previous actions @@ -210,39 +212,38 @@ async void Canvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEve } // Lock the semaphore and check the cache first - await Win2DSemaphore.WaitAsync(); - if (options == BitmapCacheMode.EnableCaching && SurfacesCache.TryGetValue(uri, out CompositionSurfaceBrush cached)) + using (await Win2DMutex.LockAsync()) { - Win2DSemaphore.Release(); - return cached; - } + if (cache == BitmapCacheMode.Default && Cache.TryGetInstance(uri, out CompositionSurfaceBrush cached)) return cached; - // Load the image - canvas.CreateResources += Canvas_CreateResources; - try - { - // This will throw and the canvas will re-initialize the Win2D device if needed - await LoadImageAsync(true); - } - catch (ArgumentException) - { - // Just ignore here - } - catch - { - // Win2D messed up big time - tcs.TrySetResult(null); - } - await Task.WhenAny(tcs.Task, Task.Delay(DeviceLostRecoveryThreshold).ContinueWith(t => tcs.TrySetResult(null))); - canvas.CreateResources -= Canvas_CreateResources; - CompositionSurfaceBrush instance = tcs.Task.Result; + // Load the image + canvas.CreateResources += Canvas_CreateResources; + try + { + // This will throw and the canvas will re-initialize the Win2D device if needed + await LoadImageAsync(true); + } + catch (ArgumentException) + { + // Just ignore here + } + catch + { + // Win2D messed up big time + tcs.TrySetResult(null); + } + await Task.WhenAny(tcs.Task, Task.Delay(DeviceLostRecoveryThreshold).ContinueWith(t => tcs.TrySetResult(null))); + canvas.CreateResources -= Canvas_CreateResources; + CompositionSurfaceBrush brush = tcs.Task.Result; - // Cache when needed and return the result - if (instance != null && - options != BitmapCacheMode.DisableCaching && - !SurfacesCache.ContainsKey(uri)) SurfacesCache.Add(uri, instance); - Win2DSemaphore.Release(); - return instance; + // Cache when needed and return the result + if (brush != null) + { + if (cache == BitmapCacheMode.Default) Cache.Add(uri, brush); + else if (cache == BitmapCacheMode.Overwrite) Cache.Overwrite(uri, brush); + } + return brush; + } } /// @@ -250,51 +251,49 @@ async void Canvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEve /// /// The compositor to use to render the Win2D image /// The path to the image to load - /// Indicates whether or not to force the reload of the Win2D image /// Indicates the desired DPI mode to use when loading the image + /// Indicates the cache option to use to load the image [ItemCanBeNull] - internal static async Task LoadImageAsync( - [NotNull] Compositor compositor, [NotNull] Uri uri, BitmapCacheMode options, BitmapDPIMode dpiMode) + internal static async Task LoadImageAsync([NotNull] Compositor compositor, [NotNull] Uri uri, BitmapDPIMode dpiMode, BitmapCacheMode cache) { // Fix the Uri if it has been generated by the XAML designer if (uri.Scheme.Equals("ms-resource")) { - String path = uri.AbsolutePath.StartsWith("/Files") - ? uri.AbsolutePath.Replace("/Files", String.Empty) + string path = uri.AbsolutePath.StartsWith("/Files") + ? uri.AbsolutePath.Replace("/Files", string.Empty) : uri.AbsolutePath; uri = new Uri($"ms-appx://{path}"); } - // Lock the semaphore and check the cache first - await Win2DSemaphore.WaitAsync(); - if (options == BitmapCacheMode.EnableCaching && SurfacesCache.TryGetValue(uri, out CompositionSurfaceBrush cached)) + // Lock and check the cache first + using (await Win2DMutex.LockAsync()) { - Win2DSemaphore.Release(); - return cached; - } + if (cache == BitmapCacheMode.Default && Cache.TryGetInstance(uri, out CompositionSurfaceBrush cached)) return cached; - // Load the image - CompositionSurfaceBrush brush; - try - { - // This will throw and the canvas will re-initialize the Win2D device if needed - CanvasDevice sharedDevice = CanvasDevice.GetSharedDevice(); - brush = await LoadSurfaceBrushAsync(sharedDevice, compositor, sharedDevice, uri, dpiMode); - } - catch - { - // Device error - brush = null; - } + // Load the image + CompositionSurfaceBrush brush; + try + { + // This will throw and the canvas will re-initialize the Win2D device if needed + CanvasDevice sharedDevice = CanvasDevice.GetSharedDevice(); + brush = await LoadSurfaceBrushAsync(sharedDevice, compositor, sharedDevice, uri, dpiMode); + } + catch + { + // Device error + brush = null; + } - // Cache when needed and return the result - if (brush != null && - options != BitmapCacheMode.DisableCaching && - !SurfacesCache.ContainsKey(uri)) SurfacesCache.Add(uri, brush); - Win2DSemaphore.Release(); - return brush; + // Cache when needed and return the result + if (brush != null) + { + if (cache == BitmapCacheMode.Default) Cache.Add(uri, brush); + else if (cache == BitmapCacheMode.Overwrite) Cache.Overwrite(uri, brush); + } + return brush; + } } #endregion } -} +} \ No newline at end of file diff --git a/UICompositionAnimations/Lights/LightsSourceHelper.cs b/UICompositionAnimations/Lights/LightsSourceHelper.cs index 212abe1..35af443 100644 --- a/UICompositionAnimations/Lights/LightsSourceHelper.cs +++ b/UICompositionAnimations/Lights/LightsSourceHelper.cs @@ -13,6 +13,7 @@ namespace UICompositionAnimations.Lights /// /// A class that contains an attached property to register a target as a lights container /// + [PublicAPI] public static class LightsSourceHelper { // The list of light generators @@ -62,10 +63,7 @@ public static void SetIsLightsContainer(UIElement element, bool value) private static void OnIsLightsContainerPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { // Designer test - if (DesignMode.DesignModeEnabled) return; - - // Platform test - if (ApiInformationHelper.IsMobileDevice) return; + if (DesignMode.DesignMode2Enabled) return; // Unpack if (LightGenerators == null) throw new NullReferenceException($"The {nameof(LightsSourceHelper)} class hasn't been initialized"); diff --git a/UICompositionAnimations/Lights/PointerPositionSpotLight.cs b/UICompositionAnimations/Lights/PointerPositionSpotLight.cs index 426d9fe..82f3363 100644 --- a/UICompositionAnimations/Lights/PointerPositionSpotLight.cs +++ b/UICompositionAnimations/Lights/PointerPositionSpotLight.cs @@ -12,6 +12,7 @@ namespace UICompositionAnimations.Lights /// /// An attached XAML property to enable the XAML brush /// + [PublicAPI] public class PointerPositionSpotLight : XamlLight { #region Properties @@ -21,8 +22,8 @@ public class PointerPositionSpotLight : XamlLight /// public byte Shade { - get { return GetValue(ShadeProperty).To(); } - set { SetValue(ShadeProperty, value); } + get => GetValue(ShadeProperty).To(); + set => SetValue(ShadeProperty, value); } /// @@ -33,8 +34,7 @@ public byte Shade private static void OnShadePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - PointerPositionSpotLight l = d as PointerPositionSpotLight; - if (l?._Light != null) + if (d is PointerPositionSpotLight @this && @this._Light != null) { byte shade = e.NewValue.To(); Color color = new Color @@ -44,7 +44,7 @@ private static void OnShadePropertyChanged(DependencyObject d, DependencyPropert G = shade, B = shade }; - l._Light.InnerConeColor = l._Light.OuterConeColor = color; + @this._Light.InnerConeColor = @this._Light.OuterConeColor = color; } } @@ -73,8 +73,8 @@ public float Z /// public float OuterConeAngle { - get { return (float)GetValue(OuterConeAngleProperty); } - set { SetValue(OuterConeAngleProperty, value); } + get => (float)GetValue(OuterConeAngleProperty); + set => SetValue(OuterConeAngleProperty, value); } /// @@ -85,10 +85,9 @@ public float OuterConeAngle private static void OuterConeAngleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - PointerPositionSpotLight l = d as PointerPositionSpotLight; - if (l?._Light != null) + if (d is PointerPositionSpotLight @this && @this._Light != null) { - l._Light.OuterConeAngleInDegrees = (float)e.NewValue; + @this._Light.OuterConeAngleInDegrees = (float)e.NewValue; } } @@ -100,8 +99,8 @@ private static void OuterConeAngleChanged(DependencyObject d, DependencyProperty /// public bool Active { - get { return GetValue(ActiveProperty).To(); } - set { SetValue(ActiveProperty, value); } + get => GetValue(ActiveProperty).To(); + set => SetValue(ActiveProperty, value); } /// @@ -112,8 +111,8 @@ public bool Active private static void OnActivePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - PointerPositionSpotLight l = d as PointerPositionSpotLight; - l?._Light?.StartAnimationAsync("ConstantAttenuation", e.NewValue.To() ? 0 : InactiveAttenuationValue, TimeSpan.FromMilliseconds(250)); + if (d is PointerPositionSpotLight @this) + @this._Light?.StartAnimationAsync("ConstantAttenuation", e.NewValue.To() ? 0 : InactiveAttenuationValue, TimeSpan.FromMilliseconds(250)); } #endregion @@ -139,9 +138,6 @@ private static void OnActivePropertyChanged(DependencyObject d, DependencyProper private static void OnIsTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - // API test - if (!ApiInformationHelper.AreXamlLightsSupported) return; - // Apply the changes if ((bool)e.NewValue) { @@ -170,16 +166,16 @@ private static void OnIsTargetChanged(DependencyObject d, DependencyPropertyChan } // The light to use - SpotLight _Light; + private SpotLight _Light; // The source compositor - Compositor _Compositor; + private Compositor _Compositor; // The expression animation for the light position - ExpressionAnimation _Animation; + private ExpressionAnimation _Animation; // The properties for the animation - CompositionPropertySet _Properties; + private CompositionPropertySet _Properties; /// protected override void OnConnected(UIElement newElement) @@ -211,14 +207,14 @@ protected override void OnConnected(UIElement newElement) /// Gets or sets a custom appendage for the method /// [NotNull] - public String IdAppendage { get; set; } = String.Empty; + public string IdAppendage { get; set; } = string.Empty; /// - protected override String GetId() => GetIdStatic() + IdAppendage; + protected override string GetId() => GetIdStatic() + IdAppendage; /// /// Gets a static Id for the class /// - public static String GetIdStatic() => typeof(PointerPositionSpotLight).FullName; + public static string GetIdStatic() => typeof(PointerPositionSpotLight).FullName; } } diff --git a/UICompositionAnimations/Properties/AssemblyInfo.cs b/UICompositionAnimations/Properties/AssemblyInfo.cs index 2bf9f88..3eabee3 100644 --- a/UICompositionAnimations/Properties/AssemblyInfo.cs +++ b/UICompositionAnimations/Properties/AssemblyInfo.cs @@ -9,7 +9,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Sergio Pedri")] [assembly: AssemblyProduct("UICompositionAnimations")] -[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyCopyright("Copyright © 2018")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -23,7 +23,8 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.15.0.0")] -[assembly: AssemblyInformationalVersion("2.15.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: ComVisible(false)] \ No newline at end of file +[assembly: AssemblyVersion("3.0.0.0")] +[assembly: AssemblyFileVersion("3.0.0.0")] +[assembly: AssemblyInformationalVersion("3.0.0")] +[assembly: ComVisible(false)] + diff --git a/UICompositionAnimations/UICompositionAnimations.csproj b/UICompositionAnimations/UICompositionAnimations.csproj index 671a449..ae2433a 100644 --- a/UICompositionAnimations/UICompositionAnimations.csproj +++ b/UICompositionAnimations/UICompositionAnimations.csproj @@ -11,11 +11,12 @@ UICompositionAnimations it-IT UAP - 10.0.16299.0 - 10.0.10586.0 + 10.0.17134.0 + 10.0.17134.0 14 512 {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + win10-arm;win10-arm-aot;win10-x86;win10-x86-aot;win10-x64;win10-x64-aot AnyCPU @@ -27,6 +28,7 @@ prompt 4 bin\Debug\UICompositionAnimations.XML + latest AnyCPU @@ -37,6 +39,7 @@ prompt 4 bin\Release\UICompositionAnimations.XML + latest x86 @@ -48,6 +51,8 @@ x86 false prompt + latest + bin\x86\Debug\UICompositionAnimations.XML x86 @@ -60,6 +65,7 @@ false prompt bin\x86\Release\UICompositionAnimations.XML + latest ARM @@ -71,6 +77,8 @@ ARM false prompt + latest + bin\ARM\Debug\UICompositionAnimations.XML ARM @@ -83,6 +91,7 @@ false prompt bin\ARM\Release\UICompositionAnimations.XML + latest x64 @@ -94,6 +103,8 @@ x64 false prompt + latest + bin\x64\Debug\UICompositionAnimations.XML x64 @@ -106,26 +117,21 @@ false prompt bin\x64\Release\UICompositionAnimations.XML + latest - - - - - - - - - - - - - + + + + + + + @@ -135,9 +141,10 @@ + - + @@ -155,6 +162,18 @@ + + + 2018.2.1 + + + 6.1.9 + + + 1.23.0 + + + 14.0 diff --git a/UICompositionAnimations/UICompositionAnimations.nuspec b/UICompositionAnimations/UICompositionAnimations.nuspec index 1c9004c..d9f3389 100644 --- a/UICompositionAnimations/UICompositionAnimations.nuspec +++ b/UICompositionAnimations/UICompositionAnimations.nuspec @@ -2,15 +2,21 @@ UICompositionAnimations - $version$ + 3.0.0 UICompositionAnimations A wrapper UWP PCL to work with Windows.UI.Composition and XAML animations, and Win2D effects Sergio Pedri Sergio Pedri https://github.com/Sergio0694/UICompositionAnimations + https://github.com/Sergio0694/UICompositionAnimations/blob/master/LICENSE.md false - Added new Composition Translation animations APIs, bug fixes - Copyright 2017 + Retargeted to support SDK 17134, new APIs and code refactoring + Copyright © 2018 uwp composition animations xaml csharp windows winrt universal app ui win2d graphics + + + + + \ No newline at end of file diff --git a/UICompositionAnimations/XAMLTransform/XAMLTransformToolkit.cs b/UICompositionAnimations/XAMLTransform/XAMLTransformToolkit.cs index e90241f..3ef0868 100644 --- a/UICompositionAnimations/XAMLTransform/XAMLTransformToolkit.cs +++ b/UICompositionAnimations/XAMLTransform/XAMLTransformToolkit.cs @@ -13,6 +13,7 @@ namespace UICompositionAnimations.XAMLTransform /// /// A toolkit with some extensions for XAML animations and some useful methods /// + [PublicAPI] public static class XAMLTransformToolkit { #region Extensions @@ -109,7 +110,7 @@ public static bool CompareTargetValue(this Storyboard storyboard, Storyboard tes /// The duration of the animation /// The easing function to use inside the animation /// Indicates whether or not to apply this animation to elements that need the visual tree to be rearranged - public static DoubleAnimation CreateDoubleAnimation(DependencyObject target, String property, + public static DoubleAnimation CreateDoubleAnimation(DependencyObject target, string property, double? from, double? to, int ms, EasingFunctionNames easing = EasingFunctionNames.Linear, bool enableDependecyAnimations = false) { DoubleAnimation animation = new DoubleAnimation @@ -140,10 +141,10 @@ public static Storyboard PrepareStoryboard(params Timeline[] animations) } /// - /// Converts the given TranslationAxis enum into its String representation + /// Converts the given TranslationAxis enum into its string representation /// /// The enum to convert - public static String ToPropertyString(this TranslationAxis axis) => axis == TranslationAxis.X ? "X" : "Y"; + public static string ToPropertyString(this TranslationAxis axis) => axis == TranslationAxis.X ? "X" : "Y"; #endregion } diff --git a/UICompositionAnimations/XAMLTransformExtensions.cs b/UICompositionAnimations/XAMLTransformExtensions.cs index c34d4f2..b2e95ca 100644 --- a/UICompositionAnimations/XAMLTransformExtensions.cs +++ b/UICompositionAnimations/XAMLTransformExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Windows.UI; using Windows.UI.Xaml; @@ -14,6 +15,7 @@ namespace UICompositionAnimations /// /// A static class that wraps the animation methods in the Windows.UI.Xaml.Media.Animation namespace /// + [PublicAPI] public static class XAMLTransformExtensions { #region Fade @@ -150,7 +152,7 @@ public static class XAMLTransformExtensions /// The element to animate /// The initial opacity /// The end opacity - /// A String that indicates which axis to use with the TranslateTransform animation + /// A string that indicates which axis to use with the TranslateTransform animation /// The initial axis value /// The final axis value /// The duration of the animation in milliseconds @@ -385,7 +387,7 @@ public static class XAMLTransformExtensions /// Slides a target element over a given axis /// /// The element to animate - /// A String that indicates which axis to use with the TranslateTransform animation + /// A string that indicates which axis to use with the TranslateTransform animation /// The initial axis value /// The final axis value /// The duration of the animation in milliseconds @@ -423,6 +425,7 @@ public static class XAMLTransformExtensions /// The margin side to animate /// The duration of the animation to create /// The optional initial delay for the animation + [SuppressMessage("ReSharper", "AccessToModifiedClosure")] // Margin updates at each animation timestep public static async Task StartXAMLMarginAnimation([NotNull] this FrameworkElement element, double? start, double end, MarginSide side, int duration, int? delay = null) { // Delay if needed, and calculate the start offset @@ -460,6 +463,7 @@ public static async Task StartXAMLMarginAnimation([NotNull] this FrameworkElemen DispatcherTimer timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(25) }; // 40fps timer.Tick += TickHandler; Thickness margin = element.Margin; + void TickHandler(object sender, object e) { elapsed++; @@ -521,7 +525,7 @@ void TickHandler(object sender, object e) /// The target color to set /// The duration of the animation /// The easing function to use - public static void AnimateColor(this SolidColorBrush solidColorBrush, String toColor, int ms, EasingFunctionNames easing) + public static void AnimateColor(this SolidColorBrush solidColorBrush, string toColor, int ms, EasingFunctionNames easing) { // Get the target color Color targetColor = ColorConverter.String2Color(toColor); diff --git a/UICompositionAnimations/project.json b/UICompositionAnimations/project.json deleted file mode 100644 index 991a94b..0000000 --- a/UICompositionAnimations/project.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "dependencies": { - "JetBrains.Annotations": "11.1.0", - "Microsoft.NETCore.UniversalWindowsPlatform": "6.0.7", - "System.Threading.Tasks.Extensions": "4.4.0", - "Win2D.uwp": "1.21.0" - }, - "frameworks": { - "uap10.0.10586": {} - }, - "runtimes": { - "win10-arm": {}, - "win10-arm-aot": {}, - "win10-x86": {}, - "win10-x86-aot": {}, - "win10-x64": {}, - "win10-x64-aot": {} - } -} \ No newline at end of file