diff --git a/src/Compatibility/Core/src/Android/VisualElementTracker.cs b/src/Compatibility/Core/src/Android/VisualElementTracker.cs index 4ba17c7a9761..7bed614b9619 100644 --- a/src/Compatibility/Core/src/Android/VisualElementTracker.cs +++ b/src/Compatibility/Core/src/Android/VisualElementTracker.cs @@ -267,6 +267,7 @@ void SetElement(VisualElement oldElement, VisualElement newElement) } } + [PortHandler] void UpdateAnchorX() { VisualElement view = _renderer.Element; @@ -278,6 +279,7 @@ void UpdateAnchorX() aview.PivotX = target; } + [PortHandler] void UpdateAnchorY() { VisualElement view = _renderer.Element; @@ -400,6 +402,7 @@ void UpdateOpacity() Performance.Stop(reference); } + [PortHandler] void UpdateRotation() { VisualElement view = _renderer.Element; @@ -408,6 +411,7 @@ void UpdateRotation() aview.Rotation = (float)view.Rotation; } + [PortHandler] void UpdateRotationX() { VisualElement view = _renderer.Element; @@ -416,6 +420,7 @@ void UpdateRotationX() aview.RotationX = (float)view.RotationX; } + [PortHandler] void UpdateRotationY() { VisualElement view = _renderer.Element; @@ -424,6 +429,7 @@ void UpdateRotationY() aview.RotationY = (float)view.RotationY; } + [PortHandler] void UpdateScale() { VisualElement view = _renderer.Element; @@ -433,6 +439,7 @@ void UpdateScale() aview.ScaleY = (float)view.Scale * (float)view.ScaleY; } + [PortHandler] void UpdateTranslationX() { VisualElement view = _renderer.Element; @@ -441,6 +448,7 @@ void UpdateTranslationX() aview.TranslationX = _context.ToPixels(view.TranslationX); } + [PortHandler] void UpdateTranslationY() { VisualElement view = _renderer.Element; diff --git a/src/Compatibility/Core/src/iOS/VisualElementTracker.cs b/src/Compatibility/Core/src/iOS/VisualElementTracker.cs index 39cb647e6882..120d83d39b02 100644 --- a/src/Compatibility/Core/src/iOS/VisualElementTracker.cs +++ b/src/Compatibility/Core/src/iOS/VisualElementTracker.cs @@ -374,6 +374,7 @@ void SetElement(VisualElement oldElement, VisualElement newElement) } } + [PortHandler("Partially ported")] void UpdateNativeControl() { Performance.Start(out string reference); diff --git a/src/Controls/samples/Controls.Sample/Pages/MainPage.cs b/src/Controls/samples/Controls.Sample/Pages/MainPage.cs index e5dbc6ab7311..e6d2cb9de306 100644 --- a/src/Controls/samples/Controls.Sample/Pages/MainPage.cs +++ b/src/Controls/samples/Controls.Sample/Pages/MainPage.cs @@ -55,6 +55,7 @@ void SetupMauiLayout() verticalStack.Add(CreateResizingButton()); AddTextResizeDemo(verticalStack); + verticalStack.Add(CreateTransformations()); verticalStack.Add(new Label { Text = " ", Padding = new Thickness(10) }); var label = new Label { Text = "End-aligned text", BackgroundColor = Colors.Fuchsia, HorizontalTextAlignment = TextAlignment.End }; @@ -74,6 +75,8 @@ void SetupMauiLayout() new Button { Text = "Push a Page", + Rotation = 15, + Scale = 1.5, Command = new Command(async () => { await Navigation.PushAsync(new SemanticsPage()); @@ -357,6 +360,18 @@ IView CreateSampleGrid() return layout; } + IView CreateTransformations() + { + var verticalStack = new VerticalStackLayout(); + var label = new Button { BackgroundColor = Colors.Red, TextColor = Colors.White, Text = "Transformations" }; + var rotationSlider = new Slider { Minimum = -360, Maximum = 360 }; + rotationSlider.ValueChanged += (sender, e) => label.Rotation = e.NewValue; + verticalStack.Add(rotationSlider); + verticalStack.Add(label); + + return verticalStack; + } + void AddTextResizeDemo(Microsoft.Maui.ILayout layout) { var resizeTestButton = new Button { Text = "Resize Test" }; diff --git a/src/Core/src/Core/IFrameworkElement.cs b/src/Core/src/Core/IFrameworkElement.cs index 70ac15f45908..118a8b879ff5 100644 --- a/src/Core/src/Core/IFrameworkElement.cs +++ b/src/Core/src/Core/IFrameworkElement.cs @@ -2,13 +2,12 @@ using Microsoft.Maui.Graphics; using Microsoft.Maui.Primitives; - namespace Microsoft.Maui { /// /// Represents a framework-level set of properties, events, and methods for .NET MAUI elements. /// - public interface IFrameworkElement + public interface IFrameworkElement : ITransform { /// /// Gets a value indicating whether this FrameworkElement is enabled in the user interface. diff --git a/src/Core/src/Core/ITransform.cs b/src/Core/src/Core/ITransform.cs new file mode 100644 index 000000000000..010426fef2f8 --- /dev/null +++ b/src/Core/src/Core/ITransform.cs @@ -0,0 +1,63 @@ +namespace Microsoft.Maui +{ + /// + /// Provides functionality to be able to apply transformations to a View. + /// + public interface ITransform + { + /// + /// Gets the X translation delta of the element. + /// + double TranslationX { get; } + + /// + /// Gets the Y translation delta of the element. + /// + double TranslationY { get; } + + /// + /// Gets the scale factor applied to the element. + /// + double Scale { get; } + + /// + /// Gets the scale about the X-axis factor applied to the element. + /// + double ScaleX { get; } + + /// + /// Gets the scale about the Y-axis factor applied to the element. + /// + double ScaleY { get; } + + /// + /// Gets the rotation (in degrees) about the Z-axis (affine rotation) + /// when the element is rendered. + /// + double Rotation { get; } + + /// + /// Gets the rotation (in degrees) about the X-axis (perspective rotation) + /// when the element is rendered. + /// + double RotationX { get; } + + /// + /// Gets the rotation (in degrees) about the Y-axis (perspective rotation) + /// when the element is rendered. + /// + double RotationY { get; } + + /// + /// Gets the X component of the center point for any transform, relative + /// to the bounds of the element. + /// + double AnchorX { get; } + + /// + /// Gets the Y component of the center point for any transform, relative + /// to the bounds of the element. + /// + double AnchorY { get; } + } +} \ No newline at end of file diff --git a/src/Core/src/Handlers/View/ViewHandler.Android.cs b/src/Core/src/Handlers/View/ViewHandler.Android.cs index ce817d98485b..866db7488e1f 100644 --- a/src/Core/src/Handlers/View/ViewHandler.Android.cs +++ b/src/Core/src/Handlers/View/ViewHandler.Android.cs @@ -14,11 +14,61 @@ partial void DisconnectingHandler(NativeView? nativeView) if (nativeView.IsAlive() && AccessibilityDelegate != null) { AccessibilityDelegate.Handler = null; - AndroidX.Core.View.ViewCompat.SetAccessibilityDelegate(nativeView, null); + ViewCompat.SetAccessibilityDelegate(nativeView, null); AccessibilityDelegate = null; } } + public static void MapTranslationX(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTranslationX(view); + } + + public static void MapTranslationY(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTranslationY(view); + } + + public static void MapScale(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateScale(view); + } + + public static void MapScaleX(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateScaleX(view); + } + + public static void MapScaleY(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateScaleY(view); + } + + public static void MapRotation(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateRotation(view); + } + + public static void MapRotationX(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateRotationX(view); + } + + public static void MapRotationY(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateRotationY(view); + } + + public static void MapAnchorX(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateAnchorX(view); + } + + public static void MapAnchorY(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateAnchorY(view); + } + static partial void MappingSemantics(IViewHandler handler, IView view) { if (view.Semantics != null && diff --git a/src/Core/src/Handlers/View/ViewHandler.Standard.cs b/src/Core/src/Handlers/View/ViewHandler.Standard.cs new file mode 100644 index 000000000000..754bab57655e --- /dev/null +++ b/src/Core/src/Handlers/View/ViewHandler.Standard.cs @@ -0,0 +1,25 @@ +namespace Microsoft.Maui.Handlers +{ + public partial class ViewHandler + { + public static void MapTranslationX(IViewHandler handler, IView view) { } + + public static void MapTranslationY(IViewHandler handler, IView view) { } + + public static void MapScale(IViewHandler handler, IView view) { } + + public static void MapScaleX(IViewHandler handler, IView view) { } + + public static void MapScaleY(IViewHandler handler, IView view) { } + + public static void MapRotation(IViewHandler handler, IView view) { } + + public static void MapRotationX(IViewHandler handler, IView view) { } + + public static void MapRotationY(IViewHandler handler, IView view) { } + + public static void MapAnchorX(IViewHandler handler, IView view) { } + + public static void MapAnchorY(IViewHandler handler, IView view) { } + } +} \ No newline at end of file diff --git a/src/Core/src/Handlers/View/ViewHandler.Windows.cs b/src/Core/src/Handlers/View/ViewHandler.Windows.cs new file mode 100644 index 000000000000..de6d2f054b76 --- /dev/null +++ b/src/Core/src/Handlers/View/ViewHandler.Windows.cs @@ -0,0 +1,58 @@ +#nullable enable +using NativeView = Microsoft.UI.Xaml.FrameworkElement; + +namespace Microsoft.Maui.Handlers +{ + public partial class ViewHandler + { + public static void MapTranslationX(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTransformation(view); + } + + public static void MapTranslationY(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTransformation(view); + } + + public static void MapScale(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTransformation(view); + } + + public static void MapScaleX(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTransformation(view); + } + + public static void MapScaleY(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTransformation(view); + } + + public static void MapRotation(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTransformation(view); + } + + public static void MapRotationX(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTransformation(view); + } + + public static void MapRotationY(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTransformation(view); + } + + public static void MapAnchorX(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTransformation(view); + } + + public static void MapAnchorY(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTransformation(view); + } + } +} \ No newline at end of file diff --git a/src/Core/src/Handlers/View/ViewHandler.cs b/src/Core/src/Handlers/View/ViewHandler.cs index 81bfa7291500..7fea24afec19 100644 --- a/src/Core/src/Handlers/View/ViewHandler.cs +++ b/src/Core/src/Handlers/View/ViewHandler.cs @@ -23,9 +23,20 @@ public abstract partial class ViewHandler : IViewHandler [nameof(IView.Height)] = MapHeight, [nameof(IView.IsEnabled)] = MapIsEnabled, [nameof(IView.Semantics)] = MapSemantics, - Actions = { - [nameof(IFrameworkElement.InvalidateMeasure)] = MapInvalidateMeasure - } + [nameof(IView.TranslationX)] = MapTranslationX, + [nameof(IView.TranslationY)] = MapTranslationY, + [nameof(IView.Scale)] = MapScale, + [nameof(IView.ScaleX)] = MapScale, + [nameof(IView.ScaleY)] = MapScale, + [nameof(IView.Rotation)] = MapRotation, + [nameof(IView.RotationX)] = MapRotationX, + [nameof(IView.RotationY)] = MapRotationY, + [nameof(IView.AnchorX)] = MapAnchorX, + [nameof(IView.AnchorY)] = MapAnchorY, + Actions = + { + [nameof(IFrameworkElement.InvalidateMeasure)] = MapInvalidateMeasure + } }; internal ViewHandler() @@ -77,12 +88,20 @@ public bool HasContainer private protected void ConnectHandler(NativeView? nativeView) { +#if __IOS__ + _layer = nativeView?.Layer; + _originalAnchor = _layer?.AnchorPoint; +#endif } partial void DisconnectingHandler(NativeView? nativeView); private protected void DisconnectHandler(NativeView? nativeView) { +#if __IOS__ + _layer = null; + _originalAnchor = null; +#endif DisconnectingHandler(nativeView); if (VirtualView != null) diff --git a/src/Core/src/Handlers/View/ViewHandler.iOS.cs b/src/Core/src/Handlers/View/ViewHandler.iOS.cs new file mode 100644 index 000000000000..f4cc1b141d95 --- /dev/null +++ b/src/Core/src/Handlers/View/ViewHandler.iOS.cs @@ -0,0 +1,67 @@ +using CoreAnimation; +using CoreGraphics; +using NativeView = UIKit.UIView; + +namespace Microsoft.Maui.Handlers +{ + public partial class ViewHandler + { + CALayer? _layer; + CGPoint? _originalAnchor; + + public static void MapTranslationX(IViewHandler handler, IView view) + { + UpdateTransformation(handler, view); + } + + public static void MapTranslationY(IViewHandler handler, IView view) + { + UpdateTransformation(handler, view); + } + + public static void MapScale(IViewHandler handler, IView view) + { + UpdateTransformation(handler, view); + } + + public static void MapScaleX(IViewHandler handler, IView view) + { + UpdateTransformation(handler, view); + } + + public static void MapScaleY(IViewHandler handler, IView view) + { + UpdateTransformation(handler, view); + } + + public static void MapRotation(IViewHandler handler, IView view) + { + UpdateTransformation(handler, view); + } + + public static void MapRotationX(IViewHandler handler, IView view) + { + UpdateTransformation(handler, view); + } + + public static void MapRotationY(IViewHandler handler, IView view) + { + UpdateTransformation(handler, view); + } + + public static void MapAnchorX(IViewHandler handler, IView view) + { + UpdateTransformation(handler, view); + } + + public static void MapAnchorY(IViewHandler handler, IView view) + { + UpdateTransformation(handler, view); + } + + internal static void UpdateTransformation(IViewHandler handler, IView view) + { + ((NativeView?)handler.NativeView)?.UpdateTransformation(view, ((ViewHandler)handler)._layer, ((ViewHandler)handler)._originalAnchor); + } + } +} \ No newline at end of file diff --git a/src/Core/src/Platform/Android/TransformationExtensions.cs b/src/Core/src/Platform/Android/TransformationExtensions.cs new file mode 100644 index 000000000000..ebb7c1f68196 --- /dev/null +++ b/src/Core/src/Platform/Android/TransformationExtensions.cs @@ -0,0 +1,74 @@ +using AView = Android.Views.View; + +namespace Microsoft.Maui +{ + public static class TransformationExtensions + { + public static void UpdateTranslationX(this AView nativeView, IView view) + { + if (nativeView.Context != null) + nativeView.TranslationX = nativeView.Context.ToPixels(view.TranslationX); + } + + public static void UpdateTranslationY(this AView nativeView, IView view) + { + if (nativeView.Context != null) + nativeView.TranslationY = nativeView.Context.ToPixels(view.TranslationY); + } + + public static void UpdateScale(this AView nativeView, IView view) + { + nativeView.UpdateScaleX(view); + nativeView.UpdateScaleY(view); + } + + public static void UpdateScaleX(this AView nativeView, IView view) + { + nativeView.ScaleX = (float)view.Scale * (float)view.ScaleX; + } + + public static void UpdateScaleY(this AView nativeView, IView view) + { + nativeView.ScaleY = (float)view.Scale * (float)view.ScaleY; + } + + public static void UpdateRotation(this AView nativeView, IView view) + { + nativeView.Rotation = (float)view.Rotation; + } + + public static void UpdateRotationX(this AView nativeView, IView view) + { + nativeView.RotationX = (float)view.RotationX; + } + + public static void UpdateRotationY(this AView nativeView, IView view) + { + nativeView.RotationY = (float)view.RotationY; + } + + public static void UpdateAnchorX(this AView nativeView, IView view) + { + if (nativeView.Context == null) + return; + + float currentPivot = nativeView.PivotX; + var target = (float)(view.AnchorX * nativeView.Context.ToPixels(view.Width)); + + if (currentPivot != target) + nativeView.PivotX = target; + } + + public static void UpdateAnchorY(this AView nativeView, IView view) + { + if (nativeView.Context == null) + return; + + float currentPivot = nativeView.PivotY; + var target = (float)(view.AnchorY * nativeView.Context.ToPixels(view.Height)); + + if (currentPivot != target) + nativeView.PivotY = target; + } + } +} \ No newline at end of file diff --git a/src/Core/src/Platform/Standard/ViewExtensions.cs b/src/Core/src/Platform/Standard/ViewExtensions.cs index af3bfed968d5..f95c55a4df96 100644 --- a/src/Core/src/Platform/Standard/ViewExtensions.cs +++ b/src/Core/src/Platform/Standard/ViewExtensions.cs @@ -10,6 +10,22 @@ public static void UpdateAutomationId(this object nativeView, IView view) { } public static void UpdateSemantics(this object nativeView, IView view) { } + public static void UpdateTranslationX(this object nativeView, IView view) { } + + public static void UpdateTranslationY(this object nativeView, IView view) { } + + public static void UpdateScale(this object nativeView, IView view) { } + + public static void UpdateRotation(this object nativeView, IView view) { } + + public static void UpdateRotationX(this object nativeView, IView view) { } + + public static void UpdateRotationY(this object nativeView, IView view) { } + + public static void UpdateAnchorX(this object nativeView, IView view) { } + + public static void UpdateAnchorY(this object nativeView, IView view) { } + public static void InvalidateMeasure(this object nativeView, IView view) { } public static void UpdateWidth(this object nativeView, IView view) { } diff --git a/src/Core/src/Platform/Windows/TransformationExtensions.cs b/src/Core/src/Platform/Windows/TransformationExtensions.cs new file mode 100644 index 000000000000..29f7d461108b --- /dev/null +++ b/src/Core/src/Platform/Windows/TransformationExtensions.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; + +namespace Microsoft.Maui +{ + public static class TransformationExtensions + { + public static void UpdateTransformation(this FrameworkElement frameworkElement, IView view) + { + double anchorX = view.AnchorX; + double anchorY = view.AnchorY; + double rotationX = view.RotationX; + double rotationY = view.RotationY; + double rotation = view.Rotation; + double translationX = view.TranslationX; + double translationY = view.TranslationY; + double scaleX = view.Scale * view.ScaleX; + double scaleY = view.Scale * view.ScaleY; + + frameworkElement.RenderTransformOrigin = new Windows.Foundation.Point(anchorX, anchorY); + frameworkElement.RenderTransform = new ScaleTransform { ScaleX = scaleX, ScaleY = scaleY }; + + if (rotationX % 360 == 0 && rotationY % 360 == 0 && rotation % 360 == 0 && + translationX == 0 && translationY == 0 && scaleX == 1 && scaleY == 1) + { + frameworkElement.Projection = null; + frameworkElement.RenderTransform = null; + } + else + { + // PlaneProjection removes touch and scrollwheel functionality on scrollable views such + // as ScrollView, ListView, and TableView. If neither RotationX or RotationY are set + // (i.e. their absolute value is 0), a CompositeTransform is instead used to allow for + // rotation of the control on a 2D plane, and the other values are set. Otherwise, the + // rotation values are set, but the aforementioned functionality will be lost. + if (Math.Abs(view.RotationX) != 0 || Math.Abs(view.RotationY) != 0) + { + frameworkElement.Projection = new PlaneProjection + { + CenterOfRotationX = anchorX, + CenterOfRotationY = anchorY, + GlobalOffsetX = translationX, + GlobalOffsetY = translationY, + RotationX = -rotationX, + RotationY = -rotationY, + RotationZ = -rotation + }; + } + else + { + frameworkElement.RenderTransform = new CompositeTransform + { + CenterX = anchorX, + CenterY = anchorY, + Rotation = rotation, + ScaleX = scaleX, + ScaleY = scaleY, + TranslateX = translationX, + TranslateY = translationY + }; + } + } + } + } +} \ No newline at end of file diff --git a/src/Core/src/Platform/iOS/TransformationExtensions.cs b/src/Core/src/Platform/iOS/TransformationExtensions.cs new file mode 100644 index 000000000000..c3c63f6b5fed --- /dev/null +++ b/src/Core/src/Platform/iOS/TransformationExtensions.cs @@ -0,0 +1,138 @@ +using System; +using CoreAnimation; +using CoreGraphics; +using Microsoft.Maui.Graphics; +using UIKit; + +namespace Microsoft.Maui +{ + public static class TransformationExtensions + { + public static void UpdateTransformation(this UIView nativeView, IView? view) + { + CALayer? layer = nativeView.Layer; + CGPoint? originalAnchor = layer?.AnchorPoint; + + nativeView.UpdateTransformation(view, layer, originalAnchor); + } + + public static void UpdateTransformation(this UIView nativeView, IView? view, CALayer? layer, CGPoint? originalAnchor) + { + if (view == null) + return; + + var anchorX = (float)view.AnchorX; + var anchorY = (float)view.AnchorY; + var translationX = (float)view.TranslationX; + var translationY = (float)view.TranslationY; + var rotationX = (float)view.RotationX; + var rotationY = (float)view.RotationY; + var rotation = (float)view.Rotation; + var scale = (float)view.Scale; + var scaleX = (float)view.ScaleX * scale; + var scaleY = (float)view.ScaleY * scale; + var width = (float)view.Frame.Width; + var height = (float)view.Frame.Height; + var x = (float)view.Frame.X; + var y = (float)view.Frame.Y; + + // TODO: Port Opacity and IsVisible properties. + var opacity = 1.0d; + var isVisible = true; + + void Update() + { + var parent = view.Parent; + + var shouldRelayoutSublayers = false; + + if (isVisible && layer != null && layer.Hidden) + { + layer.Hidden = false; + if (!layer.Frame.IsEmpty) + shouldRelayoutSublayers = true; + } + + if (!isVisible && layer != null && !layer.Hidden) + { + layer.Hidden = true; + shouldRelayoutSublayers = true; + } + + // Ripe for optimization + var transform = CATransform3D.Identity; + + bool shouldUpdate = view is not IPage && width > 0 && height > 0 && parent != null; + + if (shouldUpdate) + { + var target = new RectangleF(x, y, width, height); + + // Must reset transform prior to setting frame... + if (layer != null && originalAnchor != null && layer.AnchorPoint != originalAnchor) + layer.AnchorPoint = originalAnchor.Value; + + if (layer != null) + layer.Transform = transform; + + nativeView.Frame = target; + + if (layer != null && shouldRelayoutSublayers) + layer.LayoutSublayers(); + } + else if (width <= 0 || height <= 0) + return; + + if (layer != null) + { + layer.AnchorPoint = new PointF(anchorX, anchorY); + layer.Opacity = (float)opacity; + } + + const double epsilon = 0.001; + + // Position is relative to anchor point + if (Math.Abs(anchorX - .5) > epsilon) + transform = transform.Translate((anchorX - .5f) * width, 0, 0); + + if (Math.Abs(anchorY - .5) > epsilon) + transform = transform.Translate(0, (anchorY - .5f) * height, 0); + + if (Math.Abs(translationX) > epsilon || Math.Abs(translationY) > epsilon) + transform = transform.Translate(translationX, translationY, 0); + + // Not just an optimization, iOS will not "pixel align" a view which has m34 set + if (Math.Abs(rotationY % 180) > epsilon || Math.Abs(rotationX % 180) > epsilon) + transform.m34 = 1.0f / -400f; + + if (Math.Abs(rotationX % 360) > epsilon) + transform = transform.Rotate(rotationX * (float)Math.PI / 180.0f, 1.0f, 0.0f, 0.0f); + + if (Math.Abs(rotationY % 360) > epsilon) + transform = transform.Rotate(rotationY * (float)Math.PI / 180.0f, 0.0f, 1.0f, 0.0f); + + transform = transform.Rotate(rotation * (float)Math.PI / 180.0f, 0.0f, 0.0f, 1.0f); + + if (Math.Abs(scaleX - 1) > epsilon || Math.Abs(scaleY - 1) > epsilon) + transform = transform.Scale(scaleX, scaleY, scale); + + if (Foundation.NSThread.IsMain) + { + if (layer != null) + layer.Transform = transform; + return; + } + + CoreFoundation.DispatchQueue.MainQueue.DispatchAsync(() => + { + if (layer != null) + layer.Transform = transform; + }); + } + + // TODO: Use the thread var when porting the Device class. + + Update(); + } + } +} \ No newline at end of file diff --git a/src/Core/tests/Benchmarks/Stubs/StubBase.cs b/src/Core/tests/Benchmarks/Stubs/StubBase.cs index 24998a10c8b1..8562d409b038 100644 --- a/src/Core/tests/Benchmarks/Stubs/StubBase.cs +++ b/src/Core/tests/Benchmarks/Stubs/StubBase.cs @@ -14,6 +14,26 @@ public class StubBase : IFrameworkElement public Rectangle Frame { get; set; } = new Rectangle(0, 0, 20, 20); + public double TranslationX { get; set; } + + public double TranslationY { get; set; } + + public double Scale { get; set; } + + public double ScaleX { get; set; } + + public double ScaleY { get; set; } + + public double Rotation { get; set; } + + public double RotationX { get; set; } + + public double RotationY { get; set; } + + public double AnchorX { get; set; } + + public double AnchorY { get; set; } + public IViewHandler Handler { get; set; } public IFrameworkElement Parent { get; set; } diff --git a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs index 9865396c1ffd..891c9639a88c 100644 --- a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs +++ b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.Android.cs @@ -1,13 +1,123 @@ using System; +using System.Threading.Tasks; using Android.Views; -using Android.Widget; -using AndroidX.Core.View; -using AndroidX.Core.View.Accessibility; +using Xunit; namespace Microsoft.Maui.DeviceTests { public partial class HandlerTestBase { + [Theory(DisplayName = "TranslationX Initialize Correctly")] + [InlineData(10)] + [InlineData(50)] + [InlineData(100)] + public async Task TranslationXInitializeCorrectly(double translationX) + { + var view = new TStub() + { + TranslationX = translationX + }; + + var tX = await GetValueAsync(view, handler => GetTranslationX(handler)); + Assert.Equal(view.TranslationX, tX); + } + + [Theory(DisplayName = "TranslationY Initialize Correctly")] + [InlineData(10)] + [InlineData(50)] + [InlineData(100)] + public async Task TranslationYInitializeCorrectly(double translationY) + { + var view = new TStub() + { + TranslationY = translationY + }; + + var tY = await GetValueAsync(view, handler => GetTranslationY(handler)); + Assert.Equal(view.TranslationY, tY); + } + + [Theory(DisplayName = "ScaleX Initialize Correctly")] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public async Task ScaleXInitializeCorrectly(double scaleX) + { + var view = new TStub() + { + ScaleX = scaleX + }; + + var sX = await GetValueAsync(view, handler => GetScaleX(handler)); + Assert.Equal(view.ScaleX, sX); + } + + [Theory(DisplayName = "ScaleY Initialize Correctly")] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public async Task ScaleYInitializeCorrectly(double scaleY) + { + var view = new TStub() + { + ScaleY = scaleY + }; + + var sY = await GetValueAsync(view, handler => GetScaleY(handler)); + Assert.Equal(view.ScaleY, sY); + } + + [Theory(DisplayName = "Rotation Initialize Correctly")] + [InlineData(0)] + [InlineData(90)] + [InlineData(180)] + [InlineData(270)] + [InlineData(360)] + public async Task RotationInitializeCorrectly(double rotation) + { + var view = new TStub() + { + Rotation = rotation + }; + + var r = await GetValueAsync(view, handler => GetRotation(handler)); + Assert.Equal(view.Rotation, r); + } + + [Theory(DisplayName = "RotationX Initialize Correctly")] + [InlineData(0)] + [InlineData(90)] + [InlineData(180)] + [InlineData(270)] + [InlineData(360)] + public async Task RotationXInitializeCorrectly(double rotationX) + { + var view = new TStub() + { + RotationX = rotationX + }; + + var rX = await GetValueAsync(view, handler => GetRotationX(handler)); + Assert.Equal(view.RotationX, rX); + } + + [Theory(DisplayName = "RotationY Initialize Correctly")] + [InlineData(0)] + [InlineData(90)] + [InlineData(180)] + [InlineData(270)] + [InlineData(360)] + public async Task RotationYInitializeCorrectly(double rotationY) + { + var view = new TStub() + { + RotationY = rotationY + }; + + var rY = await GetValueAsync(view, handler => GetRotationY(handler)); + Assert.Equal(view.RotationY, rY); + } + protected THandler CreateHandler(IView view) { var handler = Activator.CreateInstance(); @@ -36,5 +146,54 @@ protected SemanticHeadingLevel GetSemanticHeading(IViewHandler viewHandler) return viewHandler.VirtualView.Semantics.HeadingLevel; } + + double GetTranslationX(IViewHandler viewHandler) + { + var nativeView = (View)viewHandler.NativeView; + + return Math.Floor(nativeView.Context.FromPixels(nativeView.TranslationX)); + } + + double GetTranslationY(IViewHandler viewHandler) + { + var nativeView = (View)viewHandler.NativeView; + + return Math.Floor(nativeView.Context.FromPixels(nativeView.TranslationY)); + } + + double GetScaleX(IViewHandler viewHandler) + { + var nativeView = (View)viewHandler.NativeView; + + return Math.Floor(nativeView.ScaleX); + } + + double GetScaleY(IViewHandler viewHandler) + { + var nativeView = (View)viewHandler.NativeView; + + return Math.Floor(nativeView.ScaleY); + } + + double GetRotation(IViewHandler viewHandler) + { + var nativeView = (View)viewHandler.NativeView; + + return Math.Floor(nativeView.Rotation); + } + + double GetRotationX(IViewHandler viewHandler) + { + var nativeView = (View)viewHandler.NativeView; + + return Math.Floor(nativeView.RotationX); + } + + double GetRotationY(IViewHandler viewHandler) + { + var nativeView = (View)viewHandler.NativeView; + + return Math.Floor(nativeView.RotationY); + } } } \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.iOS.cs b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.iOS.cs index 66d00f21db43..bb42bce73ae5 100644 --- a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.iOS.cs +++ b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.iOS.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using Microsoft.Maui.DeviceTests.Stubs; +using CoreAnimation; using UIKit; using Xunit; @@ -8,6 +8,25 @@ namespace Microsoft.Maui.DeviceTests { public partial class HandlerTestBase { + [Fact(DisplayName = "Transformation Initialize Correctly")] + public async Task TransformationInitializeCorrectly() + { + var view = new TStub() + { + TranslationX = 10, + TranslationY = 10, + Scale = 1.2, + Rotation = 90 + }; + + var handler = await CreateHandlerAsync(view); + var nativeView = (UIView)handler.NativeView; + + var transform = nativeView.Layer.Transform; + + Assert.NotEqual(CATransform3D.Identity, transform); + } + protected THandler CreateHandler(IView view) { var handler = Activator.CreateInstance(); diff --git a/src/Core/tests/DeviceTests/Stubs/StubBase.cs b/src/Core/tests/DeviceTests/Stubs/StubBase.cs index 70653be96caa..8520c0abbf32 100644 --- a/src/Core/tests/DeviceTests/Stubs/StubBase.cs +++ b/src/Core/tests/DeviceTests/Stubs/StubBase.cs @@ -24,6 +24,26 @@ public class StubBase : IFrameworkElement public double Height { get; set; } = 20; + public double TranslationX { get; set; } + + public double TranslationY { get; set; } + + public double Scale { get; set; } = 1d; + + public double ScaleX { get; set; } = 1d; + + public double ScaleY { get; set; } = 1d; + + public double Rotation { get; set; } + + public double RotationX { get; set; } + + public double RotationY { get; set; } + + public double AnchorX { get; set; } = .5d; + + public double AnchorY { get; set; } = .5d; + public Thickness Margin { get; set; } public string AutomationId { get; set; }