diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsCode.bind index deba75d104e..2ba6632624a 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsCode.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsCode.bind @@ -5,15 +5,49 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="using:Microsoft.Toolkit.Uwp.UI" + xmlns:interactivity="using:Microsoft.Xaml.Interactivity" + xmlns:interactions="using:Microsoft.Xaml.Interactions.Core" + xmlns:ani="using:Microsoft.Toolkit.Uwp.UI.Animations" + xmlns:behaviors="using:Microsoft.Toolkit.Uwp.UI.Behaviors" mc:Ignorable="d"> - + + + - + ui:VisualExtensions.NormalizedCenterPoint="0.5" + ui:VisualExtensions.Translation="20,12,0"/> + + + + diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsPage.xaml index 50f0f87c579..a8bf12a5a4f 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsPage.xaml +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsPage.xaml @@ -3,11 +3,15 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ui="using:Microsoft.Toolkit.Uwp.UI" + xmlns:interactivity="using:Microsoft.Xaml.Interactivity" + xmlns:interactions="using:Microsoft.Xaml.Interactions.Core" + xmlns:ani="using:Microsoft.Toolkit.Uwp.UI.Animations" + xmlns:behaviors="using:Microsoft.Toolkit.Uwp.UI.Behaviors" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - + - + ui:VisualExtensions.NormalizedCenterPoint="0.5" + ui:VisualExtensions.Translation="20,12,0"/> + + diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/VisualExtensions.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/VisualExtensions.cs index c4f2c57b366..cf1ddb360e6 100644 --- a/Microsoft.Toolkit.Uwp.UI/Extensions/VisualExtensions.cs +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/VisualExtensions.cs @@ -2,6 +2,7 @@ // 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.Globalization; using System.Numerics; using Windows.UI.Composition; using Windows.UI.Xaml; @@ -114,6 +115,36 @@ public static void SetOffset(DependencyObject obj, string value) obj.SetValue(OffsetProperty, value); } + /// + /// Gets the "Translation" property of the underlying object for a , in form. + /// + /// The instance. + /// The representation of the "Translation" property property. + public static string GetTranslation(DependencyObject obj) + { + if (!DesignTimeHelpers.IsRunningInLegacyDesignerMode && obj is UIElement element) + { + return GetTranslationForElement(element); + } + + return (string)obj.GetValue(TranslationProperty); + } + + /// + /// Sets the "Translation" property of the underlying object for a , in form. + /// + /// The instance. + /// The representation of the "Translation" property property to be set. + public static void SetTranslation(DependencyObject obj, string value) + { + if (!DesignTimeHelpers.IsRunningInLegacyDesignerMode && obj is UIElement element) + { + SetTranslationForElement(value, element); + } + + obj.SetValue(TranslationProperty, value); + } + /// /// Gets the of a UIElement /// @@ -334,6 +365,12 @@ public static void SetNormalizedCenterPoint(DependencyObject obj, string value) public static readonly DependencyProperty OffsetProperty = DependencyProperty.RegisterAttached("Offset", typeof(string), typeof(VisualExtensions), new PropertyMetadata(null, OnOffsetChanged)); + /// + /// Identifies the Translation attached property. + /// + public static readonly DependencyProperty TranslationProperty = + DependencyProperty.RegisterAttached("Translation", typeof(string), typeof(VisualExtensions), new PropertyMetadata(null, OnTranslationChanged)); + /// /// Identifies the Opacity attached property. /// @@ -400,6 +437,14 @@ private static void OnOffsetChanged(DependencyObject d, DependencyPropertyChange } } + private static void OnTranslationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (e.NewValue is string str) + { + SetTranslation(d, str); + } + } + private static void OnOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (e.NewValue is double dbl) @@ -503,6 +548,37 @@ private static void SetOffsetForElement(string value, UIElement element) visual.Offset = value.ToVector3(); } + private static string GetTranslationForElement(UIElement element) + { + CompositionGetValueStatus result = GetVisual(element).Properties.TryGetVector3("Translation", out Vector3 translation); + + return result switch + { + // The ("G", CultureInfo.InvariantCulture) combination produces a string with the default numeric + // formatting style, and using ',' as component separator, so that the resulting text can safely + // be parsed back if needed with the StringExtensions.ToVector3(string) extension, which uses + // the invariant culture mode by default so that the syntax will always match that from XAML. + CompositionGetValueStatus.Succeeded => translation.ToString("G", CultureInfo.InvariantCulture), + _ => "<0, 0, 0>" + }; + } + + private static void SetTranslationForElement(string value, UIElement element) + { + ElementCompositionPreview.SetIsTranslationEnabled(element, true); + + // The "Translation" attached property refers to the "hidden" property that is enabled + // through "ElementCompositionPreview.SetIsTranslationEnabled". The value for this property + // is not available directly on the Visual class and can only be accessed through its property + // set. Note that this "Translation" value is not the same as Visual.TransformMatrix.Translation. + // In fact, the latter doesn't require to be explicitly enabled and is actually combined with + // this at runtime (ie. the whole transform matrix is combined with the additional translation + // from the "Translation" property, if any), and the two can be set and animated independently. + // In this case we're just interested in the "Translation" property, which is more commonly used + // as it can also be animated directly with a Vector3 animation instead of a Matrix4x4 one. + GetVisual(element).Properties.InsertVector3("Translation", value.ToVector3()); + } + private static double GetOpacityForElement(UIElement element) { var visual = GetVisual(element); diff --git a/UnitTests/UnitTests.UWP/UI/Extensions/Test_VisualExtensions.cs b/UnitTests/UnitTests.UWP/UI/Extensions/Test_VisualExtensions.cs new file mode 100644 index 00000000000..244cfaba0b3 --- /dev/null +++ b/UnitTests/UnitTests.UWP/UI/Extensions/Test_VisualExtensions.cs @@ -0,0 +1,83 @@ +// 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.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Toolkit.Uwp; +using Windows.UI.Xaml.Controls; +using Microsoft.Toolkit.Uwp.UI; +using System.Numerics; +using Windows.UI.Composition; +using Microsoft.Toolkit.Uwp.UI.Animations; + +namespace UnitTests.UWP.UI +{ + [TestClass] + [TestCategory("Test_VisualExtensions")] + public class Test_VisualExtensions : VisualUITestBase + { + [TestMethod] + public async Task GetDefaultTranslation() + { + await App.DispatcherQueue.EnqueueAsync(() => + { + var button = new Button(); + + string text = VisualExtensions.GetTranslation(button); + + Assert.AreEqual(text, "<0, 0, 0>"); + }); + } + + [TestMethod] + public async Task SetAndGetTranslation() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var button = new Button(); + var grid = new Grid() { Children = { button } }; + + VisualExtensions.SetTranslation(button, "80, 20, 0"); + + await SetTestContentAsync(grid); + + var success = button.GetVisual().Properties.TryGetVector3("Translation", out Vector3 translation); + + Assert.AreEqual(success, CompositionGetValueStatus.Succeeded); + Assert.AreEqual(translation, new Vector3(80, 20, 0)); + + string text = VisualExtensions.GetTranslation(button); + + Assert.AreEqual(text, new Vector3(80, 20, 0).ToString()); + }); + } + + [TestMethod] + public async Task SetAndAnimateTranslation() + { + await App.DispatcherQueue.EnqueueAsync(async () => + { + var button = new Button(); + var grid = new Grid() { Children = { button } }; + + VisualExtensions.SetTranslation(button, "80, 20, 0"); + + await SetTestContentAsync(grid); + + await AnimationBuilder.Create() + .Translation(to: new Vector3(11, 22, 0)) + .StartAsync(button); + + var success = button.GetVisual().Properties.TryGetVector3("Translation", out Vector3 translation); + + Assert.AreEqual(success, CompositionGetValueStatus.Succeeded); + Assert.AreEqual(translation, new Vector3(11, 22, 0)); + + string text = VisualExtensions.GetTranslation(button); + + Assert.AreEqual(text, new Vector3(11, 22, 0).ToString()); + }); + } + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj index ff55da9ad64..d03e8e97983 100644 --- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj +++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj @@ -211,6 +211,7 @@ +