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; }