diff --git a/Windows.Toolkit.Common.props b/Windows.Toolkit.Common.props index 51218d281..e97e92774 100644 --- a/Windows.Toolkit.Common.props +++ b/Windows.Toolkit.Common.props @@ -9,7 +9,7 @@ (c) .NET Foundation and Contributors. All rights reserved. https://github.com/CommunityToolkit/Labs-Windows https://github.com/CommunityToolkit/Labs-Windows/releases - Icon.png + https://raw.githubusercontent.com/CommunityToolkit/Labs-Windows/main/nuget.png $(NoWarn);NU1505;NU1504 diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/CustomTextScalingCalculator.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/CustomTextScalingCalculator.cs new file mode 100644 index 000000000..a671becb7 --- /dev/null +++ b/labs/TransitionHelper/samples/TransitionHelper.Samples/CustomTextScalingCalculator.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; +using Microsoft.Toolkit.Uwp.UI; +using Microsoft.Toolkit.Uwp.UI.Animations.Helpers; + +namespace TransitionHelperExperiment.Samples; + +public sealed class CustomTextScalingCalculator : IScalingCalculator +{ + /// + public Vector2 GetScaling(UIElement source, UIElement target) + { + var sourceTextElement = source?.FindDescendantOrSelf(); + var targetTextElement = target?.FindDescendantOrSelf(); + if (sourceTextElement is not null && targetTextElement is not null) + { + var scale = targetTextElement.FontSize / sourceTextElement.FontSize; + return new Vector2((float)scale); + } + + return new Vector2(1); + } +} diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.Samples.csproj b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.Samples.csproj index 10b2ca7b5..bae97a860 100644 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.Samples.csproj +++ b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.Samples.csproj @@ -14,10 +14,34 @@ TransitionHelperExperiment.Samples TransitionHelperExperiment.Samples + + + + + + + + + + + + + + + + + + + + + Designer + MSBuild:Compile + + diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.md b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.md index 485b361c9..d4d32a83e 100644 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.md +++ b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelper.md @@ -1,7 +1,7 @@ --- title: TransitionHelper author: githubaccount -description: TODO: Your experiment's description here +description: An animation helper that morphs between two controls. keywords: TransitionHelper, Control, Layout dev_langs: - csharp @@ -11,54 +11,11 @@ discussion-id: 0 issue-id: 0 --- - - - - - - - - # TransitionHelper -TODO: Fill in information about this experiment and how to get started here... - -## Custom Control - -You can inherit from an existing component as well, like `Panel`, this example shows a control without a -XAML Style that will be more light-weight to consume by an app developer: - -> [!Sample TransitionHelperCustomSample] - -## Templated Controls - -The Toolkit is built with templated controls. This provides developers a flexible way to restyle components -easily while still inheriting the general functionality a control provides. The examples below show -how a component can use a default style and then get overridden by the end developer. - -TODO: Two types of templated control building methods are shown. Delete these if you're building a custom component. -Otherwise, pick one method for your component and delete the files related to the unchosen `_ClassicBinding` or `_xBind` -classes (and the custom non-suffixed one as well). Then, rename your component to just be your component name. - -The `_ClassicBinding` class shows the traditional method used to develop components with best practices. - -### Implict style - -> [!SAMPLE TransitionHelperTemplatedSample] - -### Custom style - -> [!SAMPLE TransitionHelperTemplatedStyleCustomSample] - -## Templated Controls with x:Bind - -This is an _experimental_ new way to define components which allows for the use of x:Bind within the style. - -### Implict style - -> [!SAMPLE TransitionHelperXbindBackedSample] +An animation helper that morphs between two controls. -### Custom style +### Example -> [!SAMPLE TransitionHelperXbindBackedStyleCustomSample] +> [!SAMPLE TransitionHelperFullExample] diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml deleted file mode 100644 index a237bd8fe..000000000 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml.cs deleted file mode 100644 index 01740f754..000000000 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperCustomSample.xaml.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace TransitionHelperExperiment.Samples; - -/// -/// An example sample page of a custom control inheriting from Panel. -/// -[ToolkitSampleMultiChoiceOption("LayoutOrientation", title: "Orientation", "Horizontal", "Vertical")] - -[ToolkitSample(id: nameof(TransitionHelperCustomSample), "Custom control", description: $"A sample for showing how to create and use a {nameof(TransitionHelper)} custom control.")] -public sealed partial class TransitionHelperCustomSample : Page -{ - public TransitionHelperCustomSample() - { - this.InitializeComponent(); - } - - // TODO: See https://github.com/CommunityToolkit/Labs-Windows/issues/149 - public static Orientation ConvertStringToOrientation(string orientation) => orientation switch - { - "Vertical" => Orientation.Vertical, - "Horizontal" => Orientation.Horizontal, - _ => throw new System.NotImplementedException(), - }; -} diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml new file mode 100644 index 000000000..be0b3bd74 --- /dev/null +++ b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Magic + Magic is a cute 😺. + + + + + + + + + + + + + + + + + + + Magic is my cat's name + + + Magic is a cute 😺, but sometimes very naughty. + + + + + + + + + + + + + + + + + diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml.cs new file mode 100644 index 000000000..ae580a901 --- /dev/null +++ b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperFullExample.xaml.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace TransitionHelperExperiment.Samples; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class TransitionHelperFullExample : Page +{ + public TransitionHelperFullExample() + { + this.InitializeComponent(); + } +} diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml deleted file mode 100644 index 00cf16c13..000000000 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml.cs deleted file mode 100644 index a917a6239..000000000 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedSample.xaml.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace TransitionHelperExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", "IsVisible", true)] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleMultiChoiceOption("TextSize", title: "Text size", "Small : 12", "Normal : 16", "Big : 32")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", title: "Font family", "Segoe UI", "Arial", "Consolas")] -[ToolkitSampleMultiChoiceOption("TextForeground", title: "Text foreground", - "Teal : #0ddc8c", - "Sand : #e7a676", - "Dull green : #5d7577")] - -[ToolkitSample(id: nameof(TransitionHelperTemplatedSample), "Templated control", description: "A sample for showing how to create and use a templated control.")] -public sealed partial class TransitionHelperTemplatedSample : Page -{ - public TransitionHelperTemplatedSample() - { - this.InitializeComponent(); - } -} diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml deleted file mode 100644 index 477899de8..000000000 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml.cs deleted file mode 100644 index fae83df31..000000000 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperTemplatedStyleCustomSample.xaml.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace TransitionHelperExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", "IsVisible", true)] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleMultiChoiceOption("TextSize", title: "Text size", "Small : 12", "Normal : 16", "Big : 32")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", title: "Font family", "Segoe UI", "Arial", "Consolas")] -[ToolkitSampleMultiChoiceOption("TextForeground", title: "Text foreground", - "Teal : #0ddc8c", - "Sand : #e7a676", - "Dull green : #5d7577")] - -[ToolkitSample(id: nameof(TransitionHelperTemplatedStyleCustomSample), "Templated control (restyled)", description: "A sample for showing how to create a use and templated control with a custom style.")] -public sealed partial class TransitionHelperTemplatedStyleCustomSample : Page -{ - public TransitionHelperTemplatedStyleCustomSample() - { - this.InitializeComponent(); - } -} diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml deleted file mode 100644 index f5476a392..000000000 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml.cs deleted file mode 100644 index e22a4a199..000000000 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedSample.xaml.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace TransitionHelperExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", "IsVisible", true)] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleMultiChoiceOption("TextSize", title: "Text size", "Small : 12", "Normal : 16", "Big : 32")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", title: "Font family", "Segoe UI", "Arial", "Consolas")] -[ToolkitSampleMultiChoiceOption("TextForeground", title: "Text foreground", - "Teal : #0ddc8c", - "Sand : #e7a676", - "Dull green : #5d7577")] - -[ToolkitSample(id: nameof(TransitionHelperXbindBackedSample), "Backed templated control", description: "A sample for showing how to create and use a templated control with a backed resource dictionary.")] -public sealed partial class TransitionHelperXbindBackedSample : Page -{ - public TransitionHelperXbindBackedSample() - { - this.InitializeComponent(); - } -} diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml deleted file mode 100644 index 1cc8e6630..000000000 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml.cs b/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml.cs deleted file mode 100644 index a184806db..000000000 --- a/labs/TransitionHelper/samples/TransitionHelper.Samples/TransitionHelperXbindBackedStyleCustomSample.xaml.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace TransitionHelperExperiment.Samples; - -[ToolkitSampleBoolOption("IsTextVisible", "IsVisible", true)] -// Single values without a colon are used for both label and value. -// To provide a different label for the value, separate with a colon surrounded by a single space on both sides ("label : value"). -[ToolkitSampleMultiChoiceOption("TextSize", title: "Text size", "Small : 12", "Normal : 16", "Big : 32")] -[ToolkitSampleMultiChoiceOption("TextFontFamily", title: "Font family", "Segoe UI", "Arial", "Consolas")] -[ToolkitSampleMultiChoiceOption("TextForeground", title: "Text foreground", - "Teal : #0ddc8c", - "Sand : #e7a676", - "Dull green : #5d7577")] - -[ToolkitSample(id: nameof(TransitionHelperXbindBackedStyleCustomSample), "Backed templated control (restyled)", description: "A sample for showing how to create and use a templated control with a backed resource dictionary and a custom style.")] -public sealed partial class TransitionHelperXbindBackedStyleCustomSample : Page -{ - public TransitionHelperXbindBackedStyleCustomSample() - { - this.InitializeComponent(); - } -} diff --git a/labs/TransitionHelper/src/Behaviors/ReverseTransitionAction.cs b/labs/TransitionHelper/src/Behaviors/ReverseTransitionAction.cs new file mode 100644 index 000000000..2a42b6f79 --- /dev/null +++ b/labs/TransitionHelper/src/Behaviors/ReverseTransitionAction.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Xaml.Interactivity; + +#if WINAPPSDK +using CommunityToolkit.WinUI.UI.Animations; +#else +using Microsoft.Toolkit.Uwp.UI.Animations; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// An implementation that can trigger a target instance. +/// +public sealed partial class ReverseTransitionAction : DependencyObject, IAction +{ + /// + /// Gets or sets the linked instance to reverse. + /// + public TransitionHelper Transition + { + get + { + return (TransitionHelper)this.GetValue(TransitionProperty); + } + + set + { + this.SetValue(TransitionProperty, value); + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TransitionProperty = DependencyProperty.Register( + nameof(Transition), + typeof(TransitionHelper), + typeof(StartTransitionAction), + new PropertyMetadata(null)); + + /// + public object Execute(object sender, object parameter) + { + if (this.Transition is null) + { + throw new ArgumentNullException(nameof(this.Transition)); + } + + _ = this.Transition.ReverseAsync(); + + return null!; + } +} diff --git a/labs/TransitionHelper/src/Behaviors/StartTransitionAction.cs b/labs/TransitionHelper/src/Behaviors/StartTransitionAction.cs new file mode 100644 index 000000000..fd4be2cea --- /dev/null +++ b/labs/TransitionHelper/src/Behaviors/StartTransitionAction.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Xaml.Interactivity; + +#if WINAPPSDK +using CommunityToolkit.WinUI.UI.Animations; +#else +using Microsoft.Toolkit.Uwp.UI.Animations; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// An implementation that can trigger a target instance. +/// +public sealed partial class StartTransitionAction : DependencyObject, IAction +{ + /// + /// Gets or sets the linked instance to invoke. + /// + public TransitionHelper Transition + { + get + { + return (TransitionHelper)this.GetValue(TransitionProperty); + } + + set + { + this.SetValue(TransitionProperty, value); + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TransitionProperty = DependencyProperty.Register( + nameof(Transition), + typeof(TransitionHelper), + typeof(StartTransitionAction), + new PropertyMetadata(null)); + + /// + /// Gets or sets the source control of the . + /// + public FrameworkElement Source + { + get + { + return (FrameworkElement)this.GetValue(SourceProperty); + } + + set + { + this.SetValue(SourceProperty, value); + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty SourceProperty = DependencyProperty.Register( + nameof(Source), + typeof(FrameworkElement), + typeof(StartTransitionAction), + new PropertyMetadata(null)); + + /// + /// Gets or sets the target control of the . + /// + public FrameworkElement Target + { + get + { + return (FrameworkElement)this.GetValue(TargetProperty); + } + + set + { + this.SetValue(TargetProperty, value); + } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TargetProperty = DependencyProperty.Register( + nameof(Target), + typeof(FrameworkElement), + typeof(StartTransitionAction), + new PropertyMetadata(null)); + + /// + public object Execute(object sender, object parameter) + { + if (this.Transition is null) + { + throw new ArgumentNullException(nameof(this.Transition)); + } + + if (this.Source is null) + { + throw new ArgumentNullException(nameof(this.Source)); + } + + if (this.Target is null) + { + throw new ArgumentNullException(nameof(this.Target)); + } + + this.Transition.Source = this.Source; + this.Transition.Target = this.Target; + _ = this.Transition.StartAsync(); + + return null!; + } +} diff --git a/labs/TransitionHelper/src/Dependencies.props b/labs/TransitionHelper/src/Dependencies.props index e622e1df4..9c7434348 100644 --- a/labs/TransitionHelper/src/Dependencies.props +++ b/labs/TransitionHelper/src/Dependencies.props @@ -11,21 +11,21 @@ - + - + - + - + - + diff --git a/labs/TransitionHelper/src/Enums/ScaleMode.cs b/labs/TransitionHelper/src/Enums/ScaleMode.cs new file mode 100644 index 000000000..b75729fa2 --- /dev/null +++ b/labs/TransitionHelper/src/Enums/ScaleMode.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// Indicates the strategy when the scale property of a UI element is animated. +/// +public enum ScaleMode +{ + /// + /// Do not make any changes to the scale attribute of the UI element. + /// + None, + + /// + /// Apply the scaling changes to the horizontal and vertical directions of the UI element. + /// + Scale, + + /// + /// Apply the scaling changes to the horizontal and vertical directions of the UI element, + /// but the value is calculated based on the change in the horizontal direction. + /// + ScaleX, + + /// + /// Apply scaling changes to the horizontal and vertical directions of the UI element, + /// but the value is calculated based on the change in the vertical direction. + /// + ScaleY, + + /// + /// Apply the scaling changes calculated by using custom scaling calculator. + /// + Custom, +} diff --git a/labs/TransitionHelper/src/Enums/VisualStateToggleMethod.cs b/labs/TransitionHelper/src/Enums/VisualStateToggleMethod.cs new file mode 100644 index 000000000..ed37092c4 --- /dev/null +++ b/labs/TransitionHelper/src/Enums/VisualStateToggleMethod.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// Indicates the method of changing the visibility of UI elements. +/// +public enum VisualStateToggleMethod +{ + /// + /// Change the visibility of UI elements by modifying the Visibility property. + /// + ByVisibility, + + /// + /// Change the visibility of UI elements by modifying the IsVisible property of it's Visual. + /// + ByIsVisible +} diff --git a/labs/TransitionHelper/src/IScalingCalculator.cs b/labs/TransitionHelper/src/IScalingCalculator.cs new file mode 100644 index 000000000..a11b2cd95 --- /dev/null +++ b/labs/TransitionHelper/src/IScalingCalculator.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// Defines methods to support calculating scaling changes. +/// +public interface IScalingCalculator +{ + /// + /// Gets the scaling changes when the source element transitions to the target element. + /// + /// The source element. + /// The target element. + /// A whose X value represents the horizontal scaling change and whose Y represents the vertical scaling change. + Vector2 GetScaling(UIElement source, UIElement target); +} diff --git a/labs/TransitionHelper/src/Properties/IsExternalInit.cs b/labs/TransitionHelper/src/Properties/IsExternalInit.cs new file mode 100644 index 000000000..0160c8ef0 --- /dev/null +++ b/labs/TransitionHelper/src/Properties/IsExternalInit.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices; + +/// +/// Reserved to be used by the compiler for tracking metadata. +/// This class should not be used by developers in source code. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class IsExternalInit +{ +} diff --git a/labs/TransitionHelper/src/Themes/Generic.xaml b/labs/TransitionHelper/src/Themes/Generic.xaml deleted file mode 100644 index b728504c7..000000000 --- a/labs/TransitionHelper/src/Themes/Generic.xaml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/labs/TransitionHelper/src/Toolkit/AnimationExtensions.cs b/labs/TransitionHelper/src/Toolkit/AnimationExtensions.cs new file mode 100644 index 000000000..5088d40b5 --- /dev/null +++ b/labs/TransitionHelper/src/Toolkit/AnimationExtensions.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; + +#if WINAPPSDK +using CommunityToolkit.WinUI.UI.Animations; +using Microsoft.UI.Xaml.Media.Animation; +#else +using Microsoft.Toolkit.Uwp.UI.Animations; +using Windows.UI.Xaml.Media.Animation; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// Common properties related to extensions. (Copied from internal of Toolkit) +/// +internal static class AnimationExtensions +{ + /// + /// The reusable mapping of control points for easing curves for combinations of and values. + /// + internal static readonly Dictionary<(EasingType Type, EasingMode Mode), (Vector2 A, Vector2 B)> EasingMaps = new() + { + // The default/inout combination is missing, as in this case we just skip creating + // an easing function entirely, and rely on the composition APIs using the implicit + // easing automatically. This is a bit more efficient, and results in the same + // visual behavior anyway, as that's the standard combination for animations. + [(EasingType.Default, EasingMode.EaseOut)] = (new(0.1f, 0.9f), new(0.2f, 1.0f)), + [(EasingType.Default, EasingMode.EaseIn)] = (new(0.7f, 0.0f), new(1.0f, 0.5f)), + + [(EasingType.Cubic, EasingMode.EaseOut)] = (new(0.215f, 0.61f), new(0.355f, 1f)), + [(EasingType.Cubic, EasingMode.EaseIn)] = (new(0.55f, 0.055f), new(0.675f, 0.19f)), + [(EasingType.Cubic, EasingMode.EaseInOut)] = (new(0.645f, 0.045f), new(0.355f, 1f)), + + [(EasingType.Back, EasingMode.EaseOut)] = (new(0.175f, 0.885f), new(0.32f, 1.275f)), + [(EasingType.Back, EasingMode.EaseIn)] = (new(0.6f, -0.28f), new(0.735f, 0.045f)), + [(EasingType.Back, EasingMode.EaseInOut)] = (new(0.68f, -0.55f), new(0.265f, 1.55f)), + + [(EasingType.Bounce, EasingMode.EaseOut)] = (new(0.58f, 1.93f), new(.08f, .36f)), + [(EasingType.Bounce, EasingMode.EaseIn)] = (new(0.93f, 0.7f), new(0.4f, -0.93f)), + [(EasingType.Bounce, EasingMode.EaseInOut)] = (new(0.65f, -0.85f), new(0.35f, 1.85f)), + + [(EasingType.Elastic, EasingMode.EaseOut)] = (new(0.37f, 2.68f), new(0f, 0.22f)), + [(EasingType.Elastic, EasingMode.EaseIn)] = (new(1, .78f), new(.63f, -1.68f)), + [(EasingType.Elastic, EasingMode.EaseInOut)] = (new(0.9f, -1.2f), new(0.1f, 2.2f)), + + [(EasingType.Circle, EasingMode.EaseOut)] = (new(0.075f, 0.82f), new(0.165f, 1f)), + [(EasingType.Circle, EasingMode.EaseIn)] = (new(0.6f, 0.04f), new(0.98f, 0.335f)), + [(EasingType.Circle, EasingMode.EaseInOut)] = (new(0.785f, 0.135f), new(0.15f, 0.86f)), + + [(EasingType.Quadratic, EasingMode.EaseOut)] = (new(0.25f, 0.46f), new(0.45f, 0.94f)), + [(EasingType.Quadratic, EasingMode.EaseIn)] = (new(0.55f, 0.085f), new(0.68f, 0.53f)), + [(EasingType.Quadratic, EasingMode.EaseInOut)] = (new(0.445f, 0.03f), new(0.515f, 0.955f)), + + [(EasingType.Quartic, EasingMode.EaseOut)] = (new(0.165f, 0.84f), new(0.44f, 1f)), + [(EasingType.Quartic, EasingMode.EaseIn)] = (new(0.895f, 0.03f), new(0.685f, 0.22f)), + [(EasingType.Quartic, EasingMode.EaseInOut)] = (new(0.77f, 0.0f), new(0.175f, 1.0f)), + + [(EasingType.Quintic, EasingMode.EaseOut)] = (new(0.23f, 1f), new(0.32f, 1f)), + [(EasingType.Quintic, EasingMode.EaseIn)] = (new(0.755f, 0.05f), new(0.855f, 0.06f)), + [(EasingType.Quintic, EasingMode.EaseInOut)] = (new(0.86f, 0.0f), new(0.07f, 1.0f)), + + [(EasingType.Sine, EasingMode.EaseOut)] = (new(0.39f, 0.575f), new(0.565f, 1f)), + [(EasingType.Sine, EasingMode.EaseIn)] = (new(0.47f, 0.0f), new(0.745f, 0.715f)), + [(EasingType.Sine, EasingMode.EaseInOut)] = (new(0.445f, 0.05f), new(0.55f, 0.95f)) + }; +} diff --git a/labs/TransitionHelper/src/TransitionConfig.cs b/labs/TransitionHelper/src/TransitionConfig.cs new file mode 100644 index 000000000..4b2166841 --- /dev/null +++ b/labs/TransitionHelper/src/TransitionConfig.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINAPPSDK +using CommunityToolkit.WinUI.UI.Animations; +using Microsoft.UI.Xaml.Media.Animation; +#else +using Microsoft.Toolkit.Uwp.UI.Animations; +using Windows.UI.Xaml.Media.Animation; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// Configuration used for the transition between UI elements. +/// +public class TransitionConfig +{ + /// + /// Gets or sets an id to indicate the target UI elements. + /// + public string? Id { get; set; } + + /// + /// Gets or sets the scale strategy of the transition. + /// The default value is . + /// + public ScaleMode ScaleMode { get; set; } = ScaleMode.None; + + /// + /// Gets or sets the custom scale calculator. + /// Only works when is . + /// If this value is not set, the scale strategy will fall back to . + /// + public IScalingCalculator? CustomScalingCalculator { get; set; } = null; + + /// + /// Gets or sets a value indicating whether clip animations are enabled for the target UI elements. + /// + public bool EnableClipAnimation { get; set; } + + /// + /// Gets or sets the center point used to calculate the element's translation or scale when animating. + /// Value is normalized with respect to the size of the animated element. + /// For example, a value of (0.0, 0.5) means that this point is at the leftmost point of the element horizontally and the center of the element vertically. + /// The default value is (0, 0). + /// + public Point NormalizedCenterPoint { get; set; } = default; + + /// + /// Gets or sets the easing function type for the transition. + /// If this value is not set, it will fall back to the value in . + /// + public EasingType? EasingType { get; set; } = null; + + /// + /// Gets or sets the easing function mode for the transition. + /// If this value is not set, it will fall back to the value in . + /// + public EasingMode? EasingMode { get; set; } = null; + + /// + /// Gets or sets the key point of opacity transition. + /// The time the keyframe of opacity from 0 to 1 or from 1 to 0 should occur at, expressed as a percentage of the animation duration. The allowed values are from (0, 0) to (1, 1). + /// .X will be used in the animation of the normal direction. + /// .Y will be used in the animation of the reverse direction. + /// If this value is not set, it will fall back to the value in . + /// + public Point? OpacityTransitionProgressKey { get; set; } = null; +} diff --git a/labs/TransitionHelper/src/TransitionHelper.Animation.cs b/labs/TransitionHelper/src/TransitionHelper.Animation.cs new file mode 100644 index 000000000..56fcd27b2 --- /dev/null +++ b/labs/TransitionHelper/src/TransitionHelper.Animation.cs @@ -0,0 +1,446 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.Contracts; +using System.Numerics; +using System.Runtime.CompilerServices; + +#if WINAPPSDK +using CommunityToolkit.WinUI.UI; +using CommunityToolkit.WinUI.UI.Animations; +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Hosting; +using Microsoft.UI.Xaml.Media.Animation; +using AnimationDirection = Microsoft.UI.Composition.AnimationDirection; +#else +using Microsoft.Toolkit.Uwp.UI; +using Microsoft.Toolkit.Uwp.UI.Animations; +using Windows.UI.Composition; +using Windows.UI.Xaml.Hosting; +using Windows.UI.Xaml.Media.Animation; +using AnimationDirection = Windows.UI.Composition.AnimationDirection; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// A animation helper that morphs between two controls. +/// +public sealed partial class TransitionHelper +{ + private const string TranslationPropertyName = "Translation"; + private const string TranslationXYPropertyName = "Translation.XY"; + private const string ScaleXYPropertyName = "Scale.XY"; + + private interface IEasingFunctionFactory + { + CompositionEasingFunction? GetEasingFunction(Compositor compositor, bool inverse); + } + + private interface IKeyFrameCompositionAnimationFactory + { + KeyFrameAnimation GetAnimation(CompositionObject targetHint, bool reversed, bool useReversedKeyframes, bool inverseEasingFunction, out CompositionObject? target); + } + + private interface IKeyFrameAnimationGroupController + { + float? LastStopProgress { get; } + + AnimationDirection? CurrentDirection { get; } + + Task StartAsync(CancellationToken token, TimeSpan? duration); + + Task ReverseAsync(CancellationToken token, bool inverseEasingFunction, TimeSpan? duration); + + void AddAnimationFor(UIElement target, IKeyFrameCompositionAnimationFactory? factory); + + void AddAnimationGroupFor(UIElement target, IKeyFrameCompositionAnimationFactory?[] factories); + } + + private sealed record EasingFunctionFactory( + EasingType Type = EasingType.Default, + EasingMode Mode = EasingMode.EaseInOut, + bool Inverse = false) + : IEasingFunctionFactory + { + public CompositionEasingFunction? GetEasingFunction(Compositor compositor, bool inverse) + { + if (Type == EasingType.Linear) + { + return compositor.CreateLinearEasingFunction(); + } + + var inversed = Inverse ^ inverse; + if (Type == EasingType.Default && Mode == EasingMode.EaseInOut) + { + return inversed ? compositor.CreateCubicBezierEasingFunction(new(1f, 0.06f), new(0.59f, 0.48f)) : null; + } + + var (a, b) = AnimationExtensions.EasingMaps[(Type, Mode)]; + return inversed ? compositor.CreateCubicBezierEasingFunction(new(1 - b.X, 1 - b.Y), new(1 - a.X, 1 - a.Y)) : compositor.CreateCubicBezierEasingFunction(a, b); + } + } + + private sealed record KeyFrameAnimationFactory( + string Property, + T To, + T? From, + TimeSpan? Delay, + TimeSpan? Duration, + IEasingFunctionFactory? EasingFunctionFactory, + Dictionary? NormalizedKeyFrames, + Dictionary? ReversedNormalizedKeyFrames) + : IKeyFrameCompositionAnimationFactory + where T : unmanaged + { + public KeyFrameAnimation GetAnimation(CompositionObject targetHint, bool reversed, bool useReversedKeyframes, bool inverseEasingFunction, out CompositionObject? target) + { + target = null; + + var direction = reversed ? AnimationDirection.Reverse : AnimationDirection.Normal; + var keyFrames = (useReversedKeyframes && ReversedNormalizedKeyFrames is not null) ? ReversedNormalizedKeyFrames : NormalizedKeyFrames; + + if (typeof(T) == typeof(float)) + { + var scalarAnimation = targetHint.Compositor.CreateScalarKeyFrameAnimation( + Property, + CastTo(To), + CastToNullable(From), + Delay, + Duration, + EasingFunctionFactory?.GetEasingFunction(targetHint.Compositor, inverseEasingFunction), + direction: direction); + if (keyFrames?.Count > 0) + { + foreach (var item in keyFrames) + { + var (value, easingFunctionFactory) = item.Value; + scalarAnimation.InsertKeyFrame(item.Key, CastTo(value), easingFunctionFactory?.GetEasingFunction(targetHint.Compositor, inverseEasingFunction)); + } + } + + return scalarAnimation; + } + + if (typeof(T) == typeof(Vector2)) + { + var vector2Animation = targetHint.Compositor.CreateVector2KeyFrameAnimation( + Property, + CastTo(To), + CastToNullable(From), + Delay, + Duration, + EasingFunctionFactory?.GetEasingFunction(targetHint.Compositor, inverseEasingFunction), + direction: direction); + if (keyFrames?.Count > 0) + { + foreach (var item in keyFrames) + { + var (value, easingFunctionFactory) = item.Value; + vector2Animation.InsertKeyFrame(item.Key, CastTo(value), easingFunctionFactory?.GetEasingFunction(targetHint.Compositor, inverseEasingFunction)); + } + } + + return vector2Animation; + } + + throw new InvalidOperationException("Invalid animation type"); + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private TValue CastTo(T value) + where TValue : unmanaged + { + return (TValue)(object)value; + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private TValue? CastToNullable(T? value) + where TValue : unmanaged + { + if (value is null) + { + return null; + } + + T validValue = value.GetValueOrDefault(); + + return (TValue)(object)validValue; + } + } + + private sealed record ClipScalarAnimationFactory( + string Property, + float To, + float? From, + TimeSpan? Delay, + TimeSpan? Duration, + IEasingFunctionFactory? EasingFunctionFactory) + : IKeyFrameCompositionAnimationFactory + { + public KeyFrameAnimation GetAnimation(CompositionObject targetHint, bool reversed, bool useReversedKeyframes, bool inverseEasingFunction, out CompositionObject target) + { + var direction = reversed ? AnimationDirection.Reverse : AnimationDirection.Normal; + var visual = (Visual)targetHint; + var clip = visual.Clip as InsetClip ?? (InsetClip)(visual.Clip = visual.Compositor.CreateInsetClip()); + var easingFunction = EasingFunctionFactory?.GetEasingFunction(clip.Compositor, inverseEasingFunction); + var animation = clip.Compositor.CreateScalarKeyFrameAnimation( + Property, + To, + From, + Delay, + Duration, + easingFunction, + direction: direction); + + target = clip; + return animation; + } + } + + private IKeyFrameCompositionAnimationFactory[] Clip( + Thickness to, + IEasingFunctionFactory? easingFunctionFactory, + Thickness? from = null, + TimeSpan? delay = null, + TimeSpan? duration = null) + { + return new[] + { + new ClipScalarAnimationFactory( + nameof(InsetClip.LeftInset), + (float)to.Left, + (float?)from?.Left, + delay, + duration, + easingFunctionFactory), + new ClipScalarAnimationFactory( + nameof(InsetClip.TopInset), + (float)to.Top, + (float?)from?.Top, + delay, + duration, + easingFunctionFactory), + new ClipScalarAnimationFactory( + nameof(InsetClip.RightInset), + (float)to.Right, + (float?)from?.Right, + delay, + duration, + easingFunctionFactory), + new ClipScalarAnimationFactory( + nameof(InsetClip.BottomInset), + (float)to.Bottom, + (float?)from?.Bottom, + delay, + duration, + easingFunctionFactory) + }; + } + + private IKeyFrameCompositionAnimationFactory Translation( + Vector2 to, + IEasingFunctionFactory? easingFunctionFactory, + Vector2? from = null, + TimeSpan? delay = null, + TimeSpan? duration = null, + Dictionary? normalizedKeyFrames = null, + Dictionary? reversedNormalizedKeyFrames = null) + { + return new KeyFrameAnimationFactory(TranslationXYPropertyName, to, from, delay, duration, easingFunctionFactory, normalizedKeyFrames, reversedNormalizedKeyFrames); + } + + private IKeyFrameCompositionAnimationFactory Opacity( + double to, + IEasingFunctionFactory? easingFunctionFactory, + double? from = null, + TimeSpan? delay = null, + TimeSpan? duration = null, + Dictionary? normalizedKeyFrames = null, + Dictionary? reversedNormalizedKeyFrames = null) + { + return new KeyFrameAnimationFactory(nameof(Visual.Opacity), (float)to, (float?)from, delay, duration, easingFunctionFactory, normalizedKeyFrames, reversedNormalizedKeyFrames); + } + + private IKeyFrameCompositionAnimationFactory Scale( + Vector2 to, + IEasingFunctionFactory? easingFunctionFactory, + Vector2? from = null, + TimeSpan? delay = null, + TimeSpan? duration = null, + Dictionary? normalizedKeyFrames = null, + Dictionary? reversedNormalizedKeyFrames = null) + { + return new KeyFrameAnimationFactory(ScaleXYPropertyName, to, from, delay, duration, easingFunctionFactory, normalizedKeyFrames, reversedNormalizedKeyFrames); + } + + private sealed class KeyFrameAnimationGroupController : IKeyFrameAnimationGroupController + { + private readonly Dictionary> animationFactories = new(); + + public float? LastStopProgress { get; private set; } = null; + + public AnimationDirection? CurrentDirection { get; private set; } = null; + + private bool _lastInverseEasingFunction = false; + + private bool _lastStartInNormalDirection = true; + + public void AddAnimationFor(UIElement target, IKeyFrameCompositionAnimationFactory? factory) + { + if (factory is null) + { + return; + } + + if (animationFactories.ContainsKey(target)) + { + animationFactories[target].Add(factory); + } + else + { + animationFactories.Add(target, new List() { factory }); + } + } + + public void AddAnimationGroupFor(UIElement target, IKeyFrameCompositionAnimationFactory?[] factories) + { + var validFactories = factories.Where(factory => factory is not null); + if (validFactories.Any() is false) + { + return; + } + + if (animationFactories.ContainsKey(target)) + { + animationFactories[target].AddRange(validFactories!); + } + else + { + animationFactories.Add(target, new List(validFactories!)); + } + } + + public Task StartAsync(CancellationToken token, TimeSpan? duration) + { + var start = this.LastStopProgress; + var isInterruptedAnimation = start.HasValue; + if (isInterruptedAnimation is false) + { + this._lastStartInNormalDirection = true; + } + + var inverseEasing = isInterruptedAnimation && this._lastInverseEasingFunction; + var useReversedKeyframes = isInterruptedAnimation && !this._lastStartInNormalDirection; + return AnimateAsync(false, useReversedKeyframes, token, inverseEasing, duration, start); + } + + public Task ReverseAsync(CancellationToken token, bool inverseEasingFunction, TimeSpan? duration) + { + float? start = null; + if (this.LastStopProgress.HasValue) + { + start = 1 - this.LastStopProgress.Value; + } + + var isInterruptedAnimation = start.HasValue; + if (isInterruptedAnimation is false) + { + this._lastStartInNormalDirection = false; + } + + var inverseEasing = (isInterruptedAnimation && this._lastInverseEasingFunction) || (!isInterruptedAnimation && inverseEasingFunction); + var useReversedKeyframes = !isInterruptedAnimation || !this._lastStartInNormalDirection; + return AnimateAsync(true, useReversedKeyframes, token, inverseEasing, duration, start); + } + + private Task AnimateAsync(bool reversed, bool useReversedKeyframes, CancellationToken token, bool inverseEasingFunction, TimeSpan? duration, float? startProgress) + { + List? tasks = null; + List<(CompositionObject Target, string Path)>? compositionAnimations = null; + DateTime? animationStartTime = null; + this.LastStopProgress = null; + this.CurrentDirection = reversed ? AnimationDirection.Reverse : AnimationDirection.Normal; + this._lastInverseEasingFunction = inverseEasingFunction; + if (this.animationFactories.Count > 0) + { + if (duration.HasValue) + { + var elapsedDuration = duration.Value * (startProgress ?? 0d); + animationStartTime = DateTime.Now - elapsedDuration; + } + + tasks = new List(this.animationFactories.Count); + compositionAnimations = new List<(CompositionObject Target, string Path)>(); + foreach (var item in this.animationFactories) + { + tasks.Add(StartForAsync(item.Key, reversed, useReversedKeyframes, inverseEasingFunction, duration, startProgress, compositionAnimations)); + } + } + + static void Stop(object state) + { + var (controller, reversed, duration, animationStartTime, animations) = ((KeyFrameAnimationGroupController, bool, TimeSpan?, DateTime?, List<(CompositionObject Target, string Path)>))state; + foreach (var (target, path) in animations) + { + target.StopAnimation(path); + } + + if (duration.HasValue is false || animationStartTime.HasValue is false) + { + return; + } + + var stopProgress = Math.Max(0, Math.Min((DateTime.Now - animationStartTime.Value) / duration.Value, 1)); + controller.LastStopProgress = (float)(reversed ? 1 - stopProgress : stopProgress); + } + + if (compositionAnimations is not null) + { + token.Register(static obj => Stop(obj!), (this, reversed, duration, animationStartTime, compositionAnimations)); + } + + return tasks is null ? Task.CompletedTask : Task.WhenAll(tasks); + } + + private Task StartForAsync(UIElement element, bool reversed, bool useReversedKeyframes, bool inverseEasingFunction, TimeSpan? duration, float? startProgress, List<(CompositionObject Target, string Path)> animations) + { + if (!this.animationFactories.TryGetValue(element, out var factories) || factories.Count > 0 is false) + { + return Task.CompletedTask; + } + + ElementCompositionPreview.SetIsTranslationEnabled(element, true); + var visual = element.GetVisual(); + var batch = visual.Compositor.CreateScopedBatch(CompositionBatchTypes.Animation); + var taskCompletionSource = new TaskCompletionSource(); + batch.Completed += (_, _) => taskCompletionSource.SetResult(null); + foreach (var factory in factories) + { + var animation = factory.GetAnimation(visual, reversed, useReversedKeyframes, inverseEasingFunction, out var target); + if (duration.HasValue) + { + animation.Duration = duration.Value; + } + + (target ?? visual).StartAnimation(animation.Target, animation); + if (startProgress.HasValue) + { + var controller = (target ?? visual).TryGetAnimationController(animation.Target); + if (controller is not null) + { + controller.Progress = startProgress.Value; + } + } + + animations.Add((target ?? visual, animation.Target)); + } + + batch.End(); + return taskCompletionSource.Task; + } + } +} diff --git a/labs/TransitionHelper/src/TransitionHelper.AttachedProperty.cs b/labs/TransitionHelper/src/TransitionHelper.AttachedProperty.cs new file mode 100644 index 000000000..374aadc18 --- /dev/null +++ b/labs/TransitionHelper/src/TransitionHelper.AttachedProperty.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// A animation helper that morphs between two controls. +/// +public sealed partial class TransitionHelper +{ + /// + /// Get the animation id of the UI element. + /// + /// The animation id of the UI element + public static string GetId(DependencyObject obj) + { + return (string)obj.GetValue(IdProperty); + } + + /// + /// Set the animation id of the UI element. + /// + public static void SetId(DependencyObject obj, string value) + { + obj.SetValue(IdProperty, value); + } + + /// + /// Id is used to mark the animation id of UI elements. + /// Two elements of the same id on different controls will be connected by animation. + /// + public static readonly DependencyProperty IdProperty = + DependencyProperty.RegisterAttached("Id", typeof(string), typeof(TransitionHelper), null); + + /// + /// Get the value indicating whether the UI element is animated independently. + /// + /// A bool value indicating whether the UI element needs to be connected by animation. + public static bool GetIsIndependent(DependencyObject obj) + { + return (bool)obj.GetValue(IsIndependentProperty); + } + + /// + /// Set the value indicating whether the UI element is animated independently. + /// + public static void SetIsIndependent(DependencyObject obj, bool value) + { + obj.SetValue(IsIndependentProperty, value); + } + + /// + /// IsIndependent is used to mark controls that do not need to be connected by animation, it will disappear/show independently. + /// + public static readonly DependencyProperty IsIndependentProperty = + DependencyProperty.RegisterAttached("IsIndependent", typeof(bool), typeof(TransitionHelper), new PropertyMetadata(false)); + + /// + /// Get the translation used by the show or hide animation for independent or unpaired UI elements. + /// + /// A bool value indicating whether the UI element needs to be connected by animation. + public static Point? GetIndependentTranslation(DependencyObject obj) + { + return (Point?)obj.GetValue(IndependentTranslationProperty); + } + + /// + /// Set the translation used by the show or hide animation for independent or unpaired UI elements. + /// + public static void SetIndependentTranslation(DependencyObject obj, Point? value) + { + obj.SetValue(IndependentTranslationProperty, value); + } + + /// + /// IsIndependent is used by the show or hide animation for independent or unpaired UI elements. + /// + public static readonly DependencyProperty IndependentTranslationProperty = + DependencyProperty.RegisterAttached("IndependentTranslation", typeof(Point?), typeof(TransitionHelper), null); + + /// + /// Get the target animation id for coordinated animation of the UI element. + /// + /// The target animation id for coordinated animation of the UI element. + public static string GetCoordinatedTarget(DependencyObject obj) + { + return (string)obj.GetValue(CoordinatedTargetProperty); + } + + /// + /// Set the target animation id for coordinated animation of the UI element. + /// + public static void SetCoordinatedTarget(DependencyObject obj, string value) + { + obj.SetValue(CoordinatedTargetProperty, value); + } + + /// + /// CoordinatedTarget is used to mark the target animation id of coordinated animation. + /// These elements that use coordinated animation will travel alongside the target UI element. + /// + public static readonly DependencyProperty CoordinatedTargetProperty = + DependencyProperty.RegisterAttached("CoordinatedTarget", typeof(string), typeof(TransitionHelper), null); +} diff --git a/labs/TransitionHelper/src/TransitionHelper.Helpers.cs b/labs/TransitionHelper/src/TransitionHelper.Helpers.cs new file mode 100644 index 000000000..cc79fc2f0 --- /dev/null +++ b/labs/TransitionHelper/src/TransitionHelper.Helpers.cs @@ -0,0 +1,276 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; + +#if WINAPPSDK +using CommunityToolkit.WinUI.UI; +using CommunityToolkit.WinUI.UI.Animations; +using Microsoft.UI.Composition; +using Microsoft.UI.Xaml.Hosting; +using Microsoft.UI.Xaml.Media.Animation; +#else +using Microsoft.Toolkit.Uwp.UI; +using Microsoft.Toolkit.Uwp.UI.Animations; +using Windows.UI.Composition; +using Windows.UI.Xaml.Hosting; +using Windows.UI.Xaml.Media.Animation; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// A animation helper that morphs between two controls. +/// +public sealed partial class TransitionHelper +{ + private sealed class AnimatedElementComparer : IEqualityComparer + { + public bool Equals(DependencyObject? x, DependencyObject? y) + { + if (x == null || y == null) + { + return false; + } + + if (GetIsIndependent(x) || GetIsIndependent(y)) + { + return false; + } + + return GetId(x) is { } xId && GetId(y) is { } yId && xId.Equals(yId); + } + + public int GetHashCode(DependencyObject obj) + { + return 0; + } + } + + private static AnimatedElements GetAnimatedElements(DependencyObject? parent) + { + var animatedElements = new AnimatedElements( + new Dictionary(), + new Dictionary>(), + new List()); + if (parent is null) + { + return animatedElements; + } + + var allAnimatedElements = FindDescendantsWithBFSAndPruneAndPredicate(parent, IsNotVisible, IsAnimatedElement) + .Distinct(new AnimatedElementComparer()) + .OfType(); + foreach (var item in allAnimatedElements) + { + if (GetId(item) is { } id) + { + animatedElements.ConnectedElements[id] = item; + } + else if (GetCoordinatedTarget(item) is { } targetId) + { + if (animatedElements.CoordinatedElements.ContainsKey(targetId) is false) + { + animatedElements.CoordinatedElements[targetId] = new List { item }; + } + else + { + animatedElements.CoordinatedElements[targetId].Add(item); + } + } + else + { + animatedElements.IndependentElements.Add(item); + } + } + + return animatedElements; + } + + private static bool IsNotVisible(DependencyObject element) + { + if (element is not UIElement target + || target.Visibility == Visibility.Collapsed + || target.Opacity < AlmostZero) + { + return true; + } + + return false; + } + + private static bool IsAnimatedElement(DependencyObject element) + { + return GetId(element) is not null || GetCoordinatedTarget(element) is not null || GetIsIndependent(element); + } + + private static IEnumerable FindDescendantsWithBFSAndPruneAndPredicate(DependencyObject element, Func prune, Func predicate) + { + if (predicate(element)) + { + yield return element; + yield break; + } + + var searchQueue = new Queue(); + var childrenCount = VisualTreeHelper.GetChildrenCount(element); + for (var i = 0; i < childrenCount; i++) + { + var child = VisualTreeHelper.GetChild(element, i); + if (predicate(child)) + { + yield return child; + } + else if (prune(child) is false) + { + searchQueue.Enqueue(child); + } + } + + while (searchQueue.Count > 0) + { + var parent = searchQueue.Dequeue(); + childrenCount = VisualTreeHelper.GetChildrenCount(parent); + for (var j = 0; j < childrenCount; j++) + { + var child = VisualTreeHelper.GetChild(parent, j); + if (predicate(child)) + { + yield return child; + } + else if (prune(child) is false) + { + searchQueue.Enqueue(child); + } + } + } + } + + private static void ToggleVisualState(UIElement target, VisualStateToggleMethod method, bool isVisible) + { + if (target is null) + { + return; + } + + switch (method) + { + case VisualStateToggleMethod.ByVisibility: + target.Visibility = isVisible ? Visibility.Visible : Visibility.Collapsed; + break; + case VisualStateToggleMethod.ByIsVisible: + target.GetVisual().IsVisible = isVisible; + break; + } + + target.IsHitTestVisible = isVisible; + } + + private static void RestoreElements(IEnumerable animatedElements) + { + foreach (var animatedElement in animatedElements) + { + RestoreElement(animatedElement); + } + } + + private static void RestoreElement(UIElement animatedElement) + { + ElementCompositionPreview.SetIsTranslationEnabled(animatedElement, true); + var visual = animatedElement.GetVisual(); + visual.StopAnimation(nameof(Visual.Opacity)); + visual.StopAnimation(TranslationXYPropertyName); + visual.StopAnimation(ScaleXYPropertyName); + if (visual.Clip is InsetClip clip) + { + clip.StopAnimation(nameof(InsetClip.LeftInset)); + clip.StopAnimation(nameof(InsetClip.TopInset)); + clip.StopAnimation(nameof(InsetClip.RightInset)); + clip.StopAnimation(nameof(InsetClip.BottomInset)); + } + + visual.Opacity = 1; + visual.Scale = Vector3.One; + visual.Clip = null; + visual.Properties.InsertVector3(TranslationPropertyName, Vector3.Zero); + } + + private static void IsNotNullAndIsInVisualTree(FrameworkElement? target, string name) + { + if (target is null) + { + throw new ArgumentNullException(name); + } + + if (VisualTreeHelper.GetParent(target) is null) + { + throw new ArgumentException($"The {name} element is not in the visual tree.", name); + } + } + + private static Task UpdateControlLayout(FrameworkElement target) + { + var updateTargetLayoutTaskSource = new TaskCompletionSource(); + void OnTargetLayoutUpdated(object? sender, object e) + { + target.LayoutUpdated -= OnTargetLayoutUpdated; + _ = updateTargetLayoutTaskSource.TrySetResult(null); + } + + target.LayoutUpdated += OnTargetLayoutUpdated; + target.UpdateLayout(); + return updateTargetLayoutTaskSource.Task; + } + + private static Vector2 GetInverseScale(Vector2 scale) => new(1 / scale.X, 1 / scale.Y); + + private static Thickness GetFixedThickness(double left, double top, double right, double bottom, double defaultValue = 0) + { + var fixedLeft = left < AlmostZero ? defaultValue : left; + var fixedTop = top < AlmostZero ? defaultValue : top; + var fixedRight = right < AlmostZero ? defaultValue : right; + var fixedBottom = bottom < AlmostZero ? defaultValue : bottom; + return new Thickness(fixedLeft, fixedTop, fixedRight, fixedBottom); + } + + private static Rect GetTransformedBounds(Vector2 initialLocation, Vector2 initialSize, Vector2 centerPoint, Vector2 targetScale) + { + var targetMatrix3x2 = Matrix3x2.CreateScale(targetScale, centerPoint); + return new Rect((initialLocation + Vector2.Transform(default, targetMatrix3x2)).ToPoint(), (initialSize * targetScale).ToSize()); + } + + private static Thickness? GetTargetClip(Vector2 initialLocation, Vector2 initialSize, Vector2 centerPoint, Vector2 targetScale, Vector2 translation, Rect targetParentBounds) + { + var transformedBounds = GetTransformedBounds(initialLocation + translation, initialSize, centerPoint, targetScale); + var inverseScale = GetInverseScale(targetScale); + if (targetParentBounds.Contains(new Point(transformedBounds.Left, transformedBounds.Top)) && targetParentBounds.Contains(new Point(transformedBounds.Right, transformedBounds.Bottom))) + { + return null; + } + + return GetFixedThickness( + (targetParentBounds.X - transformedBounds.X) * inverseScale.X, + (targetParentBounds.Y - transformedBounds.Y) * inverseScale.Y, + (transformedBounds.Right - targetParentBounds.Right) * inverseScale.X, + (transformedBounds.Bottom - targetParentBounds.Bottom) * inverseScale.X); + } + + private static readonly Dictionary<(EasingType, EasingMode, bool), IEasingFunctionFactory> EasingFunctionFactoryCache = new(); + + private static IEasingFunctionFactory GetEasingFunctionFactory(EasingType type = EasingType.Default, EasingMode mode = EasingMode.EaseInOut, bool inverse = false) + { + if (EasingFunctionFactoryCache.TryGetValue((type, mode, inverse), out var easingFunctionFactory)) + { + return easingFunctionFactory; + } + + var factory = new EasingFunctionFactory(type, mode, inverse); + EasingFunctionFactoryCache[(type, mode, inverse)] = factory; + return factory; + } + + private static float GetOpacityTransitionStartKey(float normalizedKey, float halfTransitionNormalizedDuration = 0.1f) => Math.Clamp(normalizedKey - halfTransitionNormalizedDuration, 0, 1); + + private static float GetOpacityTransitionEndKey(float normalizedKey, float halfTransitionNormalizedDuration = 0.1f) => Math.Clamp(normalizedKey + halfTransitionNormalizedDuration, 0, 1); +} diff --git a/labs/TransitionHelper/src/TransitionHelper.Logic.cs b/labs/TransitionHelper/src/TransitionHelper.Logic.cs new file mode 100644 index 000000000..c2b87d728 --- /dev/null +++ b/labs/TransitionHelper/src/TransitionHelper.Logic.cs @@ -0,0 +1,574 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Numerics; + +#if WINAPPSDK +using CommunityToolkit.WinUI.UI; +using CommunityToolkit.WinUI.UI.Animations; +using Microsoft.UI.Xaml.Media.Animation; +using AnimationDirection = Microsoft.UI.Composition.AnimationDirection; +#else +using Microsoft.Toolkit.Uwp.UI; +using Microsoft.Toolkit.Uwp.UI.Animations; +using Windows.UI.Xaml.Media.Animation; +using AnimationDirection = Windows.UI.Composition.AnimationDirection; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// A animation helper that morphs between two controls. +/// +public sealed partial class TransitionHelper +{ + private void RestoreState(bool isTargetState) + { + this.IsTargetState = isTargetState; + if (this.Source is not null) + { + Canvas.SetZIndex(this.Source, _sourceZIndex); + ToggleVisualState(this.Source, this.SourceToggleMethod, !isTargetState); + RestoreElements(this.SourceAnimatedElements.All()); + } + + if (this.Target is not null) + { + Canvas.SetZIndex(this.Target, _targetZIndex); + ToggleVisualState(this.Target, this.TargetToggleMethod, isTargetState); + RestoreElements(this.TargetAnimatedElements.All()); + } + } + + private async Task InitControlsStateAsync(bool forceUpdateAnimatedElements = false) + { + var maxZIndex = Math.Max(_sourceZIndex, _targetZIndex) + 1; + Canvas.SetZIndex(this.IsTargetState ? this.Source : this.Target, maxZIndex); + + await Task.WhenAll( + this.InitControlStateAsync(this.Source), + this.InitControlStateAsync(this.Target)); + + if (forceUpdateAnimatedElements) + { + _sourceAnimatedElements = null; + _targetAnimatedElements = null; + } + } + + private async Task InitControlStateAsync(FrameworkElement? target) + { + if (target is null) + { + return; + } + + target.IsHitTestVisible = IsHitTestVisibleWhenAnimating; + if (target.Visibility == Visibility.Collapsed) + { + target.Visibility = Visibility.Visible; + await UpdateControlLayout(target); + } + else if (target.Opacity < AlmostZero) + { + target.Opacity = 1; + } + else if (target.GetVisual() is { IsVisible: false } visual) + { + visual.IsVisible = true; + } + } + + private async Task AnimateControlsAsync(bool reversed, CancellationToken token, bool forceUpdateAnimatedElements) + { + IsNotNullAndIsInVisualTree(this.Source, nameof(this.Source)); + IsNotNullAndIsInVisualTree(this.Target, nameof(this.Target)); + if (IsAnimating) + { + if ((_currentAnimationGroupController?.CurrentDirection is AnimationDirection.Reverse) == reversed) + { + return; + } + else + { + this.Stop(); + } + } + else if (this.IsTargetState == !reversed) + { + return; + } + else + { + this._currentAnimationGroupController = null; + await this.InitControlsStateAsync(forceUpdateAnimatedElements); + } + + this._currentAnimationCancellationTokenSource = new CancellationTokenSource(); + var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, this._currentAnimationCancellationTokenSource.Token); + await this.AnimateControlsImpAsync(reversed ? this.ReverseDuration : this.Duration, reversed, linkedTokenSource.Token); + if (linkedTokenSource.Token.IsCancellationRequested) + { + return; + } + + this._currentAnimationGroupController = null; + this.RestoreState(!reversed); + } + + private Task AnimateControlsImpAsync(TimeSpan duration, bool reversed, CancellationToken token) + { + var sourceUnpairedElements = this.SourceAnimatedElements.ConnectedElements + .Where(item => !this.TargetAnimatedElements.ConnectedElements.ContainsKey(item.Key)) + .SelectMany(item => + { + var result = new[] { item.Value }; + if (this.SourceAnimatedElements.CoordinatedElements.TryGetValue(item.Key, out var coordinatedElements)) + { + return result.Concat(coordinatedElements); + } + + return result; + }); + var targetUnpairedElements = this.TargetAnimatedElements.ConnectedElements + .Where(item => !this.SourceAnimatedElements.ConnectedElements.ContainsKey(item.Key)) + .SelectMany(item => + { + var result = new[] { item.Value }; + if (this.TargetAnimatedElements.CoordinatedElements.TryGetValue(item.Key, out var coordinatedElements)) + { + return result.Concat(coordinatedElements); + } + + return result; + }); + var pairedElementKeys = this.SourceAnimatedElements.ConnectedElements + .Where(item => this.TargetAnimatedElements.ConnectedElements.ContainsKey(item.Key)) + .Select(item => item.Key); + if (_currentAnimationGroupController is null) + { + _currentAnimationGroupController = new KeyFrameAnimationGroupController(); + foreach (var key in pairedElementKeys) + { + var source = this.SourceAnimatedElements.ConnectedElements[key]; + var target = this.TargetAnimatedElements.ConnectedElements[key]; + var animationConfig = this.Configs.FirstOrDefault(config => config.Id == key) ?? + this.DefaultConfig; + this.SourceAnimatedElements.CoordinatedElements.TryGetValue(key, out var sourceAttachedElements); + this.TargetAnimatedElements.CoordinatedElements.TryGetValue(key, out var targetAttachedElements); + this.BuildConnectedAnimationController( + _currentAnimationGroupController, + source, + target, + sourceAttachedElements, + targetAttachedElements, + duration, + animationConfig); + } + } + + TimeSpan? startTime = null; + if (_currentAnimationGroupController.LastStopProgress.HasValue) + { + var startProgress = reversed ? (1 - _currentAnimationGroupController.LastStopProgress.Value) : _currentAnimationGroupController.LastStopProgress.Value; + startTime = startProgress * duration; + } + + var animationTasks = new[] + { + reversed + ? _currentAnimationGroupController.ReverseAsync(token, this.InverseEasingFunctionWhenReversing, duration) + : _currentAnimationGroupController.StartAsync(token, duration), + + this.AnimateIndependentElements( + this.SourceAnimatedElements.IndependentElements.Concat(sourceUnpairedElements), + reversed, + token, + startTime, + IndependentElementEasingType, + IndependentElementEasingMode), + this.AnimateIndependentElements( + this.TargetAnimatedElements.IndependentElements.Concat(targetUnpairedElements), + !reversed, + token, + startTime, + IndependentElementEasingType, + IndependentElementEasingMode) + }; + + return Task.WhenAll(animationTasks); + } + + private void BuildConnectedAnimationController( + IKeyFrameAnimationGroupController controller, + UIElement source, + UIElement target, + IList? sourceAttachedElements, + IList? targetAttachedElements, + TimeSpan duration, + TransitionConfig config) + { + var sourceActualSize = source is FrameworkElement sourceElement ? new Vector2((float)sourceElement.ActualWidth, (float)sourceElement.ActualHeight) : source.ActualSize; + var targetActualSize = target is FrameworkElement targetElement ? new Vector2((float)targetElement.ActualWidth, (float)targetElement.ActualHeight) : target.ActualSize; + var sourceCenterPoint = sourceActualSize * config.NormalizedCenterPoint.ToVector2(); + var targetCenterPoint = targetActualSize * config.NormalizedCenterPoint.ToVector2(); + + source.GetVisual().CenterPoint = new Vector3(sourceCenterPoint, 0); + target.GetVisual().CenterPoint = new Vector3(targetCenterPoint, 0); + var easingType = config.EasingType ?? this.DefaultEasingType; + var easingMode = config.EasingMode ?? this.DefaultEasingMode; + var (sourceTranslationAnimation, targetTranslationAnimation, sourceTargetTranslation) = this.AnimateTranslation( + source, + target, + sourceCenterPoint, + targetCenterPoint, + duration, + easingType, + easingMode); + var (sourceOpacityAnimation, targetOpacityAnimation) = this.AnimateOpacity(duration, config.OpacityTransitionProgressKey ?? this.DefaultOpacityTransitionProgressKey); + var (sourceScaleAnimation, targetScaleAnimation, sourceTargetScale) = config.ScaleMode switch + { + ScaleMode.None => (null, null, Vector2.One), + ScaleMode.Scale => this.AnimateScale( + sourceActualSize, + targetActualSize, + duration, + easingType, + easingMode), + ScaleMode.ScaleX => this.AnimateScaleX( + sourceActualSize, + targetActualSize, + duration, + easingType, + easingMode), + ScaleMode.ScaleY => this.AnimateScaleY( + sourceActualSize, + targetActualSize, + duration, + easingType, + easingMode), + ScaleMode.Custom => this.AnimateScaleWithScaleCalculator( + source, + target, + config.CustomScalingCalculator, + duration, + easingType, + easingMode), + _ => (null, null, Vector2.One), + }; + + controller.AddAnimationGroupFor( + source, + new[] + { + sourceTranslationAnimation, + sourceOpacityAnimation, + sourceScaleAnimation + }); + controller.AddAnimationGroupFor( + target, + new[] + { + targetTranslationAnimation, + targetOpacityAnimation, + targetScaleAnimation + }); + if (sourceAttachedElements?.Count > 0 && this.Target != null) + { + var targetControlBounds = new Rect(0, 0, this.Target.ActualWidth, this.Target.ActualHeight); + var targetTransformedBounds = this.Target.TransformToVisual(this.Source).TransformBounds(targetControlBounds); + var targetScale = sourceTargetScale; + foreach (var coordinatedElement in sourceAttachedElements) + { + var coordinatedElementActualSize = coordinatedElement is FrameworkElement coordinatedFrameworkElement ? new Vector2((float)coordinatedFrameworkElement.ActualWidth, (float)coordinatedFrameworkElement.ActualHeight) : coordinatedElement.ActualSize; + var coordinatedElementCenterPoint = coordinatedElementActualSize * config.NormalizedCenterPoint.ToVector2(); + coordinatedElement.GetVisual().CenterPoint = new Vector3(coordinatedElementCenterPoint, 0); + controller.AddAnimationGroupFor( + coordinatedElement, + new[] + { + sourceTranslationAnimation, + sourceOpacityAnimation, + sourceScaleAnimation + }); + var initialLocation = coordinatedElement.TransformToVisual(this.Source).TransformPoint(default).ToVector2(); + var targetClip = GetTargetClip(initialLocation, coordinatedElementActualSize, coordinatedElementCenterPoint, targetScale, sourceTargetTranslation, targetTransformedBounds); + if (targetClip.HasValue) + { + controller.AddAnimationGroupFor( + coordinatedElement, + this.Clip( + targetClip.Value, + GetEasingFunctionFactory(easingType, easingMode), + duration: duration)); + } + } + } + + if (targetAttachedElements?.Count > 0 && this.Source != null) + { + var sourceControlBounds = new Rect(0, 0, this.Source.ActualWidth, this.Source.ActualHeight); + var sourceTransformedBounds = this.Source.TransformToVisual(this.Target).TransformBounds(sourceControlBounds); + var targetScale = GetInverseScale(sourceTargetScale); + foreach (var coordinatedElement in targetAttachedElements) + { + var coordinatedElementActualSize = coordinatedElement is FrameworkElement coordinatedFrameworkElement ? new Vector2((float)coordinatedFrameworkElement.ActualWidth, (float)coordinatedFrameworkElement.ActualHeight) : coordinatedElement.ActualSize; + var coordinatedElementCenterPoint = coordinatedElementActualSize * config.NormalizedCenterPoint.ToVector2(); + coordinatedElement.GetVisual().CenterPoint = new Vector3(coordinatedElementCenterPoint, 0); + controller.AddAnimationGroupFor( + coordinatedElement, + new[] + { + targetTranslationAnimation, + targetOpacityAnimation, + targetScaleAnimation + }); + var initialLocation = coordinatedElement.TransformToVisual(this.Target).TransformPoint(default).ToVector2(); + var targetClip = GetTargetClip(initialLocation, coordinatedElementActualSize, coordinatedElementCenterPoint, targetScale, -sourceTargetTranslation, sourceTransformedBounds); + if (targetClip.HasValue) + { + controller.AddAnimationGroupFor( + coordinatedElement, + this.Clip( + default, + GetEasingFunctionFactory(easingType, easingMode), + from: targetClip.Value, + duration: duration)); + } + } + } + + if (config.EnableClipAnimation) + { + var (sourceClipAnimationGroup, targetClipAnimationGroup) = this.AnimateClip( + sourceActualSize, + targetActualSize, + sourceCenterPoint, + targetCenterPoint, + sourceTargetScale, + duration, + easingType, + easingMode); + if (sourceClipAnimationGroup is not null) + { + controller.AddAnimationGroupFor(source, sourceClipAnimationGroup); + } + + if (targetClipAnimationGroup is not null) + { + controller.AddAnimationGroupFor(target, targetClipAnimationGroup); + } + } + } + + private Task AnimateIndependentElements( + IEnumerable independentElements, + bool isShow, + CancellationToken token, + TimeSpan? startTime, + EasingType easingType, + EasingMode easingMode) + { + if (independentElements?.ToArray() is not { Length: > 0 } elements) + { + return Task.CompletedTask; + } + + var controller = new KeyFrameAnimationGroupController(); + var duration = isShow ? this.IndependentElementShowDuration : this.IndependentElementHideDuration; + var delay = isShow ? this.IndependentElementShowDelay : TimeSpan.Zero; + var opacityFrom = isShow ? 0 : 1; + var opacityTo = isShow ? 1 : 0; + foreach (var item in elements) + { + if (startTime.HasValue && delay < startTime) + { + if (isShow) + { + RestoreElement(item); + } + else + { + item.GetVisual().Opacity = 0; + } + } + else + { + var independentTranslation = GetIndependentTranslation(item) ?? this.DefaultIndependentTranslation; + var translationFrom = isShow ? independentTranslation.ToVector2() : Vector2.Zero; + var translationTo = isShow ? Vector2.Zero : independentTranslation.ToVector2(); + var useDelay = delay - (startTime ?? TimeSpan.Zero); + if (Math.Abs(independentTranslation.X) > AlmostZero || + Math.Abs(independentTranslation.Y) > AlmostZero) + { + controller.AddAnimationFor(item, this.Translation( + translationTo, + GetEasingFunctionFactory(easingType, easingMode), + startTime.HasValue ? null : translationFrom, + useDelay, + duration: duration)); + } + + controller.AddAnimationFor(item, this.Opacity( + opacityTo, + GetEasingFunctionFactory(easingType, easingMode), + startTime.HasValue ? null : opacityFrom, + useDelay, + duration)); + } + + if (isShow) + { + delay += this.IndependentElementShowInterval; + } + } + + return controller.StartAsync(token, null); + } + + private (IKeyFrameCompositionAnimationFactory, IKeyFrameCompositionAnimationFactory, Vector2) AnimateTranslation( + UIElement source, + UIElement target, + Vector2 sourceCenterPoint, + Vector2 targetCenterPoint, + TimeSpan duration, + EasingType easingType, + EasingMode easingMode) + { + var translation = target.TransformToVisual(source).TransformPoint(default).ToVector2() - sourceCenterPoint + targetCenterPoint; + return (this.Translation(translation, GetEasingFunctionFactory(easingType, easingMode), Vector2.Zero, duration: duration), + this.Translation(Vector2.Zero, GetEasingFunctionFactory(easingType, easingMode), -translation, duration: duration), + translation); + } + + private (IKeyFrameCompositionAnimationFactory, IKeyFrameCompositionAnimationFactory, Vector2) AnimateScale( + Vector2 sourceActualSize, + Vector2 targetActualSize, + TimeSpan duration, + EasingType easingType, + EasingMode easingMode) + { + var scaleX = targetActualSize.X / sourceActualSize.X; + var scaleY = targetActualSize.Y / sourceActualSize.Y; + var scale = new Vector2(scaleX, scaleY); + var (sourceFactory, targetFactory) = this.AnimateScaleImp(scale, duration, easingType, easingMode); + return (sourceFactory, targetFactory, scale); + } + + private (IKeyFrameCompositionAnimationFactory, IKeyFrameCompositionAnimationFactory, Vector2) AnimateScaleX( + Vector2 sourceActualSize, + Vector2 targetActualSize, + TimeSpan duration, + EasingType easingType, + EasingMode easingMode) + { + var scaleX = targetActualSize.X / sourceActualSize.X; + var scale = new Vector2(scaleX, scaleX); + var (sourceFactory, targetFactory) = this.AnimateScaleImp(scale, duration, easingType, easingMode); + return (sourceFactory, targetFactory, scale); + } + + private (IKeyFrameCompositionAnimationFactory, IKeyFrameCompositionAnimationFactory, Vector2) AnimateScaleY( + Vector2 sourceActualSize, + Vector2 targetActualSize, + TimeSpan duration, + EasingType easingType, + EasingMode easingMode) + { + var scaleY = targetActualSize.Y / sourceActualSize.Y; + var scale = new Vector2(scaleY, scaleY); + var (sourceFactory, targetFactory) = this.AnimateScaleImp(scale, duration, easingType, easingMode); + return (sourceFactory, targetFactory, scale); + } + + private (IKeyFrameCompositionAnimationFactory?, IKeyFrameCompositionAnimationFactory?, Vector2) AnimateScaleWithScaleCalculator( + UIElement source, + UIElement target, + IScalingCalculator? scalingCalculator, + TimeSpan duration, + EasingType easingType, + EasingMode easingMode) + { + if (scalingCalculator is null) + { + return (null, null, Vector2.One); + } + + var scale = scalingCalculator.GetScaling(source, target); + var (sourceFactory, targetFactory) = this.AnimateScaleImp(scale, duration, easingType, easingMode); + return (sourceFactory, targetFactory, scale); + } + + private (IKeyFrameCompositionAnimationFactory, IKeyFrameCompositionAnimationFactory) AnimateScaleImp( + Vector2 targetScale, + TimeSpan duration, + EasingType easingType, + EasingMode easingMode) + { + return (this.Scale(targetScale, GetEasingFunctionFactory(easingType, easingMode), Vector2.One, duration: duration), + this.Scale(Vector2.One, GetEasingFunctionFactory(easingType, easingMode), GetInverseScale(targetScale), duration: duration)); + } + + private (IKeyFrameCompositionAnimationFactory, IKeyFrameCompositionAnimationFactory) AnimateOpacity(TimeSpan duration, Point opacityTransitionProgressKey) + { + var normalKey = (float)opacityTransitionProgressKey.X; + var normalKeyForTarget = Math.Clamp(normalKey - 0.1f, 0, 1); + var sourceNormalizedKeyFrames = new Dictionary + { + [GetOpacityTransitionStartKey(normalKey)] = (1, null), + [GetOpacityTransitionEndKey(normalKey)] = (0, GetEasingFunctionFactory(EasingType.Cubic, EasingMode.EaseIn)) + }; + var targetNormalizedKeyFrames = new Dictionary + { + [GetOpacityTransitionStartKey(normalKeyForTarget)] = (0, null), + [GetOpacityTransitionEndKey(normalKeyForTarget)] = (1, GetEasingFunctionFactory(EasingType.Cubic, EasingMode.EaseOut)) + }; + + var reversedKey = (float)(1 - opacityTransitionProgressKey.Y); + var reversedKeyForSource = Math.Clamp(reversedKey + 0.1f, 0, 1); + var reversedSourceNormalizedKeyFrames = new Dictionary + { + [GetOpacityTransitionStartKey(reversedKeyForSource)] = (1, null), + [GetOpacityTransitionEndKey(reversedKeyForSource)] = (0, GetEasingFunctionFactory(EasingType.Cubic, EasingMode.EaseIn, true)) + }; + var reversedTargetNormalizedKeyFrames = new Dictionary + { + [GetOpacityTransitionStartKey(reversedKey)] = (0, null), + [GetOpacityTransitionEndKey(reversedKey)] = (1, GetEasingFunctionFactory(EasingType.Cubic, EasingMode.EaseOut, true)), + }; + + return (this.Opacity(0, null, 1, duration: duration, normalizedKeyFrames: sourceNormalizedKeyFrames, reversedNormalizedKeyFrames: reversedSourceNormalizedKeyFrames), + this.Opacity(1, null, 0, duration: duration, normalizedKeyFrames: targetNormalizedKeyFrames, reversedNormalizedKeyFrames: reversedTargetNormalizedKeyFrames)); + } + + private (IKeyFrameCompositionAnimationFactory[]?, IKeyFrameCompositionAnimationFactory[]?) AnimateClip( + Vector2 sourceActualSize, + Vector2 targetActualSize, + Vector2 sourceCenterPoint, + Vector2 targetCenterPoint, + Vector2 sourceTargetScale, + TimeSpan duration, + EasingType easingType, + EasingMode easingMode) + { + var sourceToClip = GetTargetClip(-sourceCenterPoint, sourceActualSize, sourceCenterPoint, sourceTargetScale, default, new Rect((-targetCenterPoint).ToPoint(), targetActualSize.ToSize())); + var targetFromClip = GetTargetClip(-targetCenterPoint, targetActualSize, targetCenterPoint, GetInverseScale(sourceTargetScale), default, new Rect((-sourceCenterPoint).ToPoint(), sourceActualSize.ToSize())); + return ( + sourceToClip.HasValue + ? this.Clip( + sourceToClip.Value, + GetEasingFunctionFactory(easingType, easingMode), + default, + duration: duration) + : null, + targetFromClip.HasValue + ? this.Clip( + default, + GetEasingFunctionFactory(easingType, easingMode), + targetFromClip.Value, + duration: duration) + : null + ); + } +} diff --git a/labs/TransitionHelper/src/TransitionHelper.Properties.cs b/labs/TransitionHelper/src/TransitionHelper.Properties.cs new file mode 100644 index 000000000..e12a5ecdb --- /dev/null +++ b/labs/TransitionHelper/src/TransitionHelper.Properties.cs @@ -0,0 +1,204 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if WINAPPSDK +using CommunityToolkit.WinUI.UI.Animations; +using Microsoft.UI.Xaml.Media.Animation; +#else +using Microsoft.Toolkit.Uwp.UI.Animations; +using Windows.UI.Xaml.Media.Animation; +#endif + +namespace CommunityToolkit.Labs.WinUI; + +/// +/// A animation helper that morphs between two controls. +/// +public sealed partial class TransitionHelper +{ + private FrameworkElement? _source; + private int _sourceZIndex = -1; + private FrameworkElement? _target; + private int _targetZIndex = -1; + + /// + /// Gets or sets the source control. + /// + public FrameworkElement? Source + { + get + { + return this._source; + } + + set + { + if (this._source == value) + { + return; + } + + var needReset = IsAnimating || IsTargetState; + if (IsAnimating && this._source is not null) + { + this.Stop(); + RestoreElements(this.SourceAnimatedElements.All()); + } + + this._currentAnimationGroupController = null; + this._source = value; + this._sourceZIndex = value is null ? -1 : Canvas.GetZIndex(value); + this._sourceAnimatedElements = null; + if (needReset) + { + this.Reset(true); + } + } + } + + /// + /// Gets or sets the target control. + /// + public FrameworkElement? Target + { + get + { + return this._target; + } + + set + { + if (this._target == value) + { + return; + } + + var needReset = IsAnimating || IsTargetState; + if (IsAnimating && this._target is not null) + { + this.Stop(); + RestoreElements(this.TargetAnimatedElements.All()); + } + + this._currentAnimationGroupController = null; + this._target = value; + this._targetZIndex = value is null ? -1 : Canvas.GetZIndex(value); + this._targetAnimatedElements = null; + if (needReset) + { + this.Reset(true); + } + } + } + + /// + /// Gets or sets transition configurations of UI elements that need to be connected by animation. + /// + public List Configs { get; set; } = new(); + + /// + /// Gets a value indicating whether the source control has been morphed to the target control. + /// The default value is false. + /// + public bool IsTargetState { get; private set; } = false; + + /// + /// Gets or sets a value indicating whether the contained area of the source or target control can return true values for hit testing when animating. + /// The default value is false. + /// + public bool IsHitTestVisibleWhenAnimating { get; set; } = false; + + /// + /// Gets or sets the method of changing the visibility of the source control. + /// The default value is . + /// + public VisualStateToggleMethod SourceToggleMethod { get; set; } = VisualStateToggleMethod.ByVisibility; + + /// + /// Gets or sets the method of changing the visibility of the target control. + /// The default value is . + /// + public VisualStateToggleMethod TargetToggleMethod { get; set; } = VisualStateToggleMethod.ByVisibility; + + /// + /// Gets or sets the duration of the connected animation between two UI elements. + /// The default value is 600ms. + /// + public TimeSpan Duration { get; set; } = TimeSpan.FromMilliseconds(600); + + /// + /// Gets or sets the reverse duration of the connected animation between two UI elements. + /// The default value is 600ms. + /// + public TimeSpan ReverseDuration { get; set; } = TimeSpan.FromMilliseconds(600); + + /// + /// Gets or sets a value indicating whether to use the inverse easing function when animating in reverse direction. + /// The default value is true. + /// + public bool InverseEasingFunctionWhenReversing { get; set; } = true; + + /// + /// Gets or sets the duration of the show animation for independent or unpaired UI elements. + /// The default value is 200ms. + /// + public TimeSpan IndependentElementShowDuration { get; set; } = TimeSpan.FromMilliseconds(200); + + /// + /// Gets or sets the delay of the show animation for independent or unpaired UI elements. + /// The default value is 300ms. + /// + public TimeSpan IndependentElementShowDelay { get; set; } = TimeSpan.FromMilliseconds(300); + + /// + /// Gets or sets the interval between the show animations for independent or unpaired UI elements. + /// The default value is 50ms. + /// + public TimeSpan IndependentElementShowInterval { get; set; } = TimeSpan.FromMilliseconds(50); + + /// + /// Gets or sets the duration of the hide animation for independent or unpaired UI elements. + /// The default value is 100ms. + /// + public TimeSpan IndependentElementHideDuration { get; set; } = TimeSpan.FromMilliseconds(100); + + /// + /// Gets or sets the default easing function type for the transition. + /// The default value is . + /// + public EasingType DefaultEasingType { get; set; } = EasingType.Default; + + /// + /// Gets or sets the default easing function mode for the transition. + /// The default value is . + /// + public EasingMode DefaultEasingMode { get; set; } = EasingMode.EaseInOut; + + /// + /// Gets or sets the default translation used by the show or hide animation for independent or unpaired UI elements. + /// The default value is (0, 20). + /// + public Point DefaultIndependentTranslation { get; set; } = new(0, 20); + + /// + /// Gets or sets the default key point of opacity transition. + /// The time the keyframe of opacity from 0 to 1 or from 1 to 0 should occur at, expressed as a percentage of the animation duration. The allowed values are from (0, 0) to (1, 1). + /// .X will be used in the animation of the normal direction. + /// .Y will be used in the animation of the reverse direction. + /// The default value is (0.3, 0.3). + /// + public Point DefaultOpacityTransitionProgressKey { get; set; } = new(0.3, 0.3); + + /// + /// Gets or sets the easing function type for animation of independent or unpaired UI elements. + /// The default value is . + /// + public EasingType IndependentElementEasingType { get; set; } = EasingType.Default; + + /// + /// Gets or sets the easing function mode for animation of independent or unpaired UI elements. + /// The default value is . + /// + public EasingMode IndependentElementEasingMode { get; set; } = EasingMode.EaseInOut; +} diff --git a/labs/TransitionHelper/src/TransitionHelper.cs b/labs/TransitionHelper/src/TransitionHelper.cs index 83b65af3b..06fbb6de5 100644 --- a/labs/TransitionHelper/src/TransitionHelper.cs +++ b/labs/TransitionHelper/src/TransitionHelper.cs @@ -2,107 +2,131 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#nullable enable + namespace CommunityToolkit.Labs.WinUI; /// -/// This is an example control based off of the BoxPanel sample here: https://docs.microsoft.com/windows/apps/design/layout/boxpanel-example-custom-panel. If you need this similar sort of layout component for an application, see UniformGrid in the Toolkit. -/// It is provided as an example of how to inherit from another control like . -/// You can choose to start here or from the or example components. Remove unused components and rename as appropriate. +/// A animation helper that morphs between two controls. /// -public partial class TransitionHelper : Panel +[ContentProperty(Name = nameof(Configs))] +public sealed partial class TransitionHelper { + private sealed record AnimatedElements( + IDictionary ConnectedElements, + IDictionary> CoordinatedElements, + IList IndependentElements) + { + public IEnumerable All() + { + return this.ConnectedElements.Values.Concat(this.IndependentElements).Concat(this.CoordinatedElements.SelectMany(item => item.Value)); + } + } + + private const double AlmostZero = 0.01; + private AnimatedElements? _sourceAnimatedElements; + private AnimatedElements? _targetAnimatedElements; + private CancellationTokenSource? _currentAnimationCancellationTokenSource; + private IKeyFrameAnimationGroupController? _currentAnimationGroupController; + + private AnimatedElements SourceAnimatedElements => _sourceAnimatedElements ??= GetAnimatedElements(this.Source); + + private AnimatedElements TargetAnimatedElements => _targetAnimatedElements ??= GetAnimatedElements(this.Target); + + private TransitionConfig DefaultConfig => new() + { + EasingMode = DefaultEasingMode, + EasingType = DefaultEasingType, + OpacityTransitionProgressKey = DefaultIndependentTranslation + }; + /// - /// Identifies the property. + /// Gets a value indicating whether the source and target controls are animating. /// - public static readonly DependencyProperty OrientationProperty = - DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(TransitionHelper), new PropertyMetadata(null, OnOrientationChanged)); + public bool IsAnimating => _currentAnimationCancellationTokenSource is not null && this._currentAnimationGroupController is not null; /// - /// Gets the preference of the rows/columns when there are a non-square number of children. Defaults to Vertical. + /// Morphs from source control to target control. /// - public Orientation Orientation + /// A that completes when all animations have completed. + public Task StartAsync() { - get { return (Orientation)GetValue(OrientationProperty); } - set { SetValue(OrientationProperty, value); } + return StartAsync(CancellationToken.None, false); } - // Invalidate our layout when the property changes. - private static void OnOrientationChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args) + /// + /// Morphs from source control to target control. + /// + /// Indicates whether to force the update of the child element list before the animation starts. + /// A that completes when all animations have completed. + public Task StartAsync(bool forceUpdateAnimatedElements) { - if (dependencyObject is TransitionHelper panel) - { - panel.InvalidateMeasure(); - } + return StartAsync(CancellationToken.None, forceUpdateAnimatedElements); } - // Store calculations we want to use between the Measure and Arrange methods. - int _columnCount; - double _cellWidth, _cellHeight; - - protected override Size MeasureOverride(Size availableSize) + /// + /// Morphs from source control to target control. + /// + /// The cancellation token to stop animations while they're running. + /// Indicates whether to force the update of the child element list before the animation starts. + /// A that completes when all animations have completed. + public Task StartAsync(CancellationToken token, bool forceUpdateAnimatedElements) { - // Determine the square that can contain this number of items. - var maxrc = (int)Math.Ceiling(Math.Sqrt(Children.Count)); - // Get an aspect ratio from availableSize, decides whether to trim row or column. - var aspectratio = availableSize.Width / availableSize.Height; - if (Orientation == Orientation.Vertical) { aspectratio = 1 / aspectratio; } - - int rowcount; - - // Now trim this square down to a rect, many times an entire row or column can be omitted. - if (aspectratio > 1) - { - rowcount = maxrc; - _columnCount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc; - } - else - { - rowcount = (maxrc > 2 && Children.Count <= maxrc * (maxrc - 1)) ? maxrc - 1 : maxrc; - _columnCount = maxrc; - } - - // Now that we have a column count, divide available horizontal, that's our cell width. - _cellWidth = (int)Math.Floor(availableSize.Width / _columnCount); - // Next get a cell height, same logic of dividing available vertical by rowcount. - _cellHeight = Double.IsInfinity(availableSize.Height) ? Double.PositiveInfinity : availableSize.Height / rowcount; + return this.AnimateControlsAsync(false, token, forceUpdateAnimatedElements); + } - double maxcellheight = 0; + /// + /// Reverse animation, morphs from target control to source control. + /// + /// A that completes when all animations have completed. + public Task ReverseAsync() + { + return ReverseAsync(CancellationToken.None, false); + } - foreach (UIElement child in Children) - { - child.Measure(new Size(_cellWidth, _cellHeight)); - maxcellheight = (child.DesiredSize.Height > maxcellheight) ? child.DesiredSize.Height : maxcellheight; - } + /// + /// Reverse animation, morphs from target control to source control. + /// + /// Indicates whether to force the update of child elements before the animation starts. + /// A that completes when all animations have completed. + public Task ReverseAsync(bool forceUpdateAnimatedElements) + { + return ReverseAsync(CancellationToken.None, forceUpdateAnimatedElements); + } - return LimitUnboundedSize(availableSize, maxcellheight); + /// + /// Reverse animation, morphs from target control to source control. + /// + /// The cancellation token to stop animations while they're running. + /// Indicates whether to force the update of child elements before the animation starts. + /// A that completes when all animations have completed. + public Task ReverseAsync(CancellationToken token, bool forceUpdateAnimatedElements) + { + return this.AnimateControlsAsync(true, token, forceUpdateAnimatedElements); } - // This method limits the panel height when no limit is imposed by the panel's parent. - // That can happen to height if the panel is close to the root of main app window. - // In this case, base the height of a cell on the max height from desired size - // and base the height of the panel on that number times the #rows. - Size LimitUnboundedSize(Size input, double maxcellheight) - { - if (Double.IsInfinity(input.Height)) + /// + /// Stop all animations. + /// + public void Stop() + { + if (IsAnimating is false) { - input.Height = maxcellheight * _columnCount; - _cellHeight = maxcellheight; + return; } - return input; + + this._currentAnimationCancellationTokenSource?.Cancel(); + this._currentAnimationCancellationTokenSource = null; } - protected override Size ArrangeOverride(Size finalSize) + /// + /// Reset to initial or target state. + /// + /// Indicates whether to reset to initial state. default value is True, if it is False, it will be reset to target state. + public void Reset(bool toInitialState = true) { - int count = 1; - double x, y; - foreach (UIElement child in Children) - { - x = (count - 1) % _columnCount * _cellWidth; - y = ((int)(count - 1) / _columnCount) * _cellHeight; - Point anchorPoint = new Point(x, y); - child.Arrange(new Rect(anchorPoint, child.DesiredSize)); - count++; - } - return finalSize; + this.Stop(); + this._currentAnimationGroupController = null; + this.RestoreState(!toInitialState); } } diff --git a/labs/TransitionHelper/src/TransitionHelperStyle_ClassicBinding.xaml b/labs/TransitionHelper/src/TransitionHelperStyle_ClassicBinding.xaml deleted file mode 100644 index dfa7429fc..000000000 --- a/labs/TransitionHelper/src/TransitionHelperStyle_ClassicBinding.xaml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - 4,4,4,4 - - - - - - - - diff --git a/labs/TransitionHelper/src/TransitionHelperStyle_xBind.xaml b/labs/TransitionHelper/src/TransitionHelperStyle_xBind.xaml deleted file mode 100644 index 73272c0be..000000000 --- a/labs/TransitionHelper/src/TransitionHelperStyle_xBind.xaml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - 4,4,4,4 - - - - - - - - diff --git a/labs/TransitionHelper/src/TransitionHelperStyle_xBind.xaml.cs b/labs/TransitionHelper/src/TransitionHelperStyle_xBind.xaml.cs deleted file mode 100644 index 07dea2c4a..000000000 --- a/labs/TransitionHelper/src/TransitionHelperStyle_xBind.xaml.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace CommunityToolkit.Labs.WinUI; - -/// -/// Backing code for this resource dictionary. -/// -public sealed partial class TransitionHelperStyle_xBind : ResourceDictionary -{ - // NOTICE - // This file only exists to enable x:Bind in the resource dictionary. - // Do not add code here. - // Instead, add code-behind to your templated control. - public TransitionHelperStyle_xBind() - { - this.InitializeComponent(); - } -} diff --git a/labs/TransitionHelper/src/TransitionHelper_ClassicBinding.cs b/labs/TransitionHelper/src/TransitionHelper_ClassicBinding.cs deleted file mode 100644 index 07b452575..000000000 --- a/labs/TransitionHelper/src/TransitionHelper_ClassicBinding.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace CommunityToolkit.Labs.WinUI; - -/// -/// An example templated control. -/// -[TemplatePart(Name = nameof(PART_HelloWorld), Type = typeof(TextBlock))] -public partial class TransitionHelper_ClassicBinding : Control -{ - /// - /// Creates a new instance of the class. - /// - public TransitionHelper_ClassicBinding() - { - this.DefaultStyleKey = typeof(TransitionHelper_ClassicBinding); - } - - /// - /// The primary text block that displays "Hello world". - /// - protected TextBlock? PART_HelloWorld { get; private set; } - - /// - protected override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - // Detach all attached events when a new template is applied. - if (PART_HelloWorld is not null) - { - PART_HelloWorld.PointerEntered -= Element_PointerEntered; - } - - // Attach events when the template is applied and the control is loaded. - PART_HelloWorld = GetTemplateChild(nameof(PART_HelloWorld)) as TextBlock; - - if (PART_HelloWorld is not null) - { - PART_HelloWorld.PointerEntered += Element_PointerEntered; - } - } - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty ItemPaddingProperty = DependencyProperty.Register( - nameof(ItemPadding), - typeof(Thickness), - typeof(TransitionHelper_ClassicBinding), - new PropertyMetadata(defaultValue: new Thickness(0))); - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( - nameof(MyProperty), - typeof(string), - typeof(TransitionHelper_ClassicBinding), - new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((TransitionHelper_ClassicBinding)d).OnMyPropertyChanged((string)e.OldValue, (string)e.NewValue))); - - /// - /// Gets or sets an example string. A basic DependencyProperty example. - /// - public string MyProperty - { - get => (string)GetValue(MyPropertyProperty); - set => SetValue(MyPropertyProperty, value); - } - - /// - /// Gets or sets a padding for an item. A basic DependencyProperty example. - /// - public Thickness ItemPadding - { - get => (Thickness)GetValue(ItemPaddingProperty); - set => SetValue(ItemPaddingProperty, value); - } - - protected virtual void OnMyPropertyChanged(string oldValue, string newValue) - { - // Do something with the changed value. - } - - public void Element_PointerEntered(object sender, PointerRoutedEventArgs e) - { - if (sender is TextBlock text) - { - text.Opacity = 1; - } - } -} diff --git a/labs/TransitionHelper/src/TransitionHelper_xBind.cs b/labs/TransitionHelper/src/TransitionHelper_xBind.cs deleted file mode 100644 index 86ba41222..000000000 --- a/labs/TransitionHelper/src/TransitionHelper_xBind.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace CommunityToolkit.Labs.WinUI; - -/// -/// An example templated control. -/// -public partial class TransitionHelper_xBind: Control -{ - /// - /// Creates a new instance of the class. - /// - public TransitionHelper_xBind() - { - this.DefaultStyleKey = typeof(TransitionHelper_xBind); - - // Allows directly using this control as the x:DataType in the template. - this.DataContext = this; - } - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty ItemPaddingProperty = DependencyProperty.Register( - nameof(ItemPadding), - typeof(Thickness), - typeof(TransitionHelper_xBind), - new PropertyMetadata(defaultValue: new Thickness(0))); - - /// - /// The backing for the property. - /// - public static readonly DependencyProperty MyPropertyProperty = DependencyProperty.Register( - nameof(MyProperty), - typeof(string), - typeof(TransitionHelper_xBind), - new PropertyMetadata(defaultValue: string.Empty, (d, e) => ((TransitionHelper_xBind)d).OnMyPropertyChanged((string)e.OldValue, (string)e.NewValue))); - - /// - /// Gets or sets an example string. A basic DependencyProperty example. - /// - public string MyProperty - { - get => (string)GetValue(MyPropertyProperty); - set => SetValue(MyPropertyProperty, value); - } - - /// - /// Gets or sets a padding for an item. A basic DependencyProperty example. - /// - public Thickness ItemPadding - { - get => (Thickness)GetValue(ItemPaddingProperty); - set => SetValue(ItemPaddingProperty, value); - } - - protected virtual void OnMyPropertyChanged(string oldValue, string newValue) - { - // Do something with the changed value. - } - - public void Element_PointerEntered(object sender, PointerRoutedEventArgs e) - { - if (sender is TextBlock text) - { - text.Opacity = 1; - } - } -}