diff --git a/src/Controls/src/Core/Compatibility/Handlers/Android/FrameRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Android/FrameRenderer.cs index 3d141d78ba99..7d5191eeb766 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Android/FrameRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Android/FrameRenderer.cs @@ -4,6 +4,7 @@ using Android.Graphics; using Android.Graphics.Drawables; using Android.Views; +using Android.Views.Animations; using AndroidX.CardView.Widget; using AndroidX.Core.View; using Microsoft.Maui.Controls.Platform; @@ -52,7 +53,6 @@ public static IPropertyMapper Mapper public static CommandMapper CommandMapper = new CommandMapper(ViewRenderer.VisualElementRendererCommandMapper); - float _defaultElevation = -1f; float _defaultCornerRadius = -1f; @@ -68,6 +68,8 @@ public static CommandMapper CommandMapper public event EventHandler? ElementChanged; public event EventHandler? ElementPropertyChanged; + const double LegacyMinimumFrameSize = 20; + public FrameRenderer(Context context) : this(context, Mapper) { } @@ -96,17 +98,28 @@ protected Frame? Element } } - Size IViewHandler.GetDesiredSize(double widthMeasureSpec, double heightMeasureSpec) + Size IViewHandler.GetDesiredSize(double widthConstraint, double heightConstraint) { - double minWidth = 20; - if (Primitives.Dimension.IsExplicitSet(widthMeasureSpec) && !double.IsInfinity(widthMeasureSpec)) - minWidth = widthMeasureSpec; + var virtualView = (this as IViewHandler)?.VirtualView; + if (virtualView is null) + { + return Size.Zero; + } + + var minWidth = virtualView.MinimumWidth; + var minHeight = virtualView.MinimumHeight; - double minHeight = 20; - if (Primitives.Dimension.IsExplicitSet(widthMeasureSpec) && !double.IsInfinity(heightMeasureSpec)) - minHeight = heightMeasureSpec; + if (!Primitives.Dimension.IsExplicitSet(minWidth)) + { + minWidth = LegacyMinimumFrameSize; + } + + if (!Primitives.Dimension.IsExplicitSet(minHeight)) + { + minHeight = LegacyMinimumFrameSize; + } - return VisualElementRenderer.GetDesiredSize(this, widthMeasureSpec, heightMeasureSpec, + return VisualElementRenderer.GetDesiredSize(this, widthConstraint, heightConstraint, new Size(minWidth, minHeight)); } diff --git a/src/Controls/tests/DeviceTests/Elements/Frame/FrameHandlerTest.Android.cs b/src/Controls/tests/DeviceTests/Elements/Frame/FrameHandlerTest.Android.cs index 587891f74cf4..f05babfdc875 100644 --- a/src/Controls/tests/DeviceTests/Elements/Frame/FrameHandlerTest.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/Frame/FrameHandlerTest.Android.cs @@ -1,4 +1,8 @@ using System.Threading.Tasks; +using Java.Lang; +using Microsoft.Maui.Controls; +using Xunit; +using Xunit.Sdk; namespace Microsoft.Maui.DeviceTests { @@ -21,5 +25,23 @@ public override Task ContainerViewRemainsIfShadowMapperRunsAgain() // https://github.com/dotnet/maui/pull/12218 return Task.CompletedTask; } + + public override async Task ReturnsNonEmptyNativeBoundingBox(int size) + { + // Frames have a legacy hard-coded minimum size of 20x20 + var expectedSize = Math.Max(20, size); + var expectedBounds = new Graphics.Rect(0, 0, expectedSize, expectedSize); + + var view = new Frame() + { + HeightRequest = size, + WidthRequest = size + }; + + var nativeBoundingBox = await GetValueAsync(view, handler => GetBoundingBox(handler)); + Assert.NotEqual(nativeBoundingBox, Graphics.Rect.Zero); + + AssertWithinTolerance(expectedBounds.Size, nativeBoundingBox.Size); + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/Frame/FrameTests.cs b/src/Controls/tests/DeviceTests/Elements/Frame/FrameTests.cs index b6d53fd92f4d..23bfd0e4366a 100644 --- a/src/Controls/tests/DeviceTests/Elements/Frame/FrameTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Frame/FrameTests.cs @@ -91,30 +91,7 @@ public async Task FrameWithEntryMeasuresCorrectly() } }; - var layoutFrame = - await InvokeOnMainThreadAsync(() => - layout.ToPlatform(MauiContext).AttachAndRun(async () => - { - var size = (layout as IView).Measure(double.PositiveInfinity, double.PositiveInfinity); - (layout as IView).Arrange(new Graphics.Rect(0, 0, size.Width, size.Height)); - - await OnFrameSetToNotEmpty(layout); - await OnFrameSetToNotEmpty(frame); - - // verify that the PlatformView was measured - var frameControlSize = (frame.Handler as IPlatformViewHandler).PlatformView.GetBoundingBox(); - Assert.True(frameControlSize.Width > 0); - Assert.True(frameControlSize.Width > 0); - - // if the control sits inside a container make sure that also measured - var containerControlSize = frame.ToPlatform().GetBoundingBox(); - Assert.True(frameControlSize.Width > 0); - Assert.True(frameControlSize.Width > 0); - - return layout.Frame; - - }) - ); + var layoutFrame = await LayoutFrame(layout, frame, double.PositiveInfinity, double.PositiveInfinity); Assert.True(entry.Width > 0); Assert.True(entry.Height > 0); @@ -187,5 +164,92 @@ await InvokeOnMainThreadAsync(() => platformView.AssertContainsColor(expectedColor); }); } + + [Fact(DisplayName = "Frame Respects minimum height/width")] + public async Task FrameRespectsMinimums() + { + SetupBuilder(); + + var content = new Button { Text = "Hey", WidthRequest = 50, HeightRequest = 50 }; + + var frame = new Frame() + { + Content = content, + MinimumHeightRequest = 100, + MinimumWidthRequest = 100, + VerticalOptions = LayoutOptions.Start, + HorizontalOptions = LayoutOptions.Start + }; + + var layout = new StackLayout() + { + Children = + { + frame + } + }; + + var layoutFrame = await LayoutFrame(layout, frame, 500, 500); + + Assert.True(100 <= layoutFrame.Height); + Assert.True(100 <= layoutFrame.Width); + } + + [Fact] + public async Task FrameDoesNotInterpretConstraintsAsMinimums() + { + SetupBuilder(); + + var content = new Button { Text = "Hey", WidthRequest = 50, HeightRequest = 50 }; + + var frame = new Frame() + { + Content = content, + MinimumHeightRequest = 100, + MinimumWidthRequest = 100, + VerticalOptions = LayoutOptions.Start, + HorizontalOptions = LayoutOptions.Start + }; + + var layout = new StackLayout() + { + Children = + { + frame + } + }; + + var layoutFrame = await LayoutFrame(layout, frame, 500, 500); + + Assert.True(500 > layoutFrame.Width); + Assert.True(500 > layoutFrame.Height); + } + + async Task LayoutFrame(Layout layout, Frame frame, double measureWidth, double measureHeight) + { + return await InvokeOnMainThreadAsync(() => + layout.ToPlatform(MauiContext).AttachAndRun(async () => + { + var size = (layout as IView).Measure(measureWidth, measureHeight); + (layout as IView).Arrange(new Graphics.Rect(0, 0, size.Width, size.Height)); + + await OnFrameSetToNotEmpty(layout); + await OnFrameSetToNotEmpty(frame); + + // verify that the PlatformView was measured + var frameControlSize = (frame.Handler as IPlatformViewHandler).PlatformView.GetBoundingBox(); + Assert.True(frameControlSize.Width > 0); + Assert.True(frameControlSize.Width > 0); + + // if the control sits inside a container make sure that also measured + var containerControlSize = frame.ToPlatform().GetBoundingBox(); + Assert.True(frameControlSize.Width > 0); + Assert.True(frameControlSize.Width > 0); + + return layout.Frame; + + }) + ); + } } } diff --git a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBaseOfT.Tests.cs b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBaseOfT.Tests.cs index 8e21d4bc0fbc..2bd0060b9b3f 100644 --- a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBaseOfT.Tests.cs +++ b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBaseOfT.Tests.cs @@ -8,6 +8,7 @@ using Microsoft.Maui.Hosting; using Microsoft.Maui.Media; using Xunit; +using Xunit.Sdk; namespace Microsoft.Maui.DeviceTests { @@ -244,7 +245,7 @@ public async Task ReturnsNonEmptyPlatformViewBounds(int size) Assert.NotEqual(platformViewBounds, new Graphics.Rect()); } - [Theory(DisplayName = "Native View Bounding Box are not empty" + [Theory(DisplayName = "Native View Bounding Box is not empty" #if WINDOWS , Skip = "https://github.com/dotnet/maui/issues/9054" #endif @@ -252,7 +253,7 @@ public async Task ReturnsNonEmptyPlatformViewBounds(int size) [InlineData(1)] [InlineData(100)] [InlineData(1000)] - public async Task ReturnsNonEmptyNativeBoundingBounds(int size) + public virtual async Task ReturnsNonEmptyNativeBoundingBox(int size) { var view = new TStub() { @@ -261,8 +262,7 @@ public async Task ReturnsNonEmptyNativeBoundingBounds(int size) }; var nativeBoundingBox = await GetValueAsync(view, handler => GetBoundingBox(handler)); - Assert.NotEqual(nativeBoundingBox, new Graphics.Rect()); - + Assert.NotEqual(nativeBoundingBox, Graphics.Rect.Zero); // Currently there's an issue with label/progress where they don't set the frame size to // the explicit Width and Height values set @@ -290,21 +290,29 @@ public async Task ReturnsNonEmptyNativeBoundingBounds(int size) #endif else if (view is IProgress) { - if (!CloseEnough(size, nativeBoundingBox.Size.Width)) - Assert.Equal(new Size(size, size), nativeBoundingBox.Size); + AssertWithinTolerance(size, nativeBoundingBox.Size.Width); } else { - if (!CloseEnough(size, nativeBoundingBox.Size.Height) || !CloseEnough(size, nativeBoundingBox.Size.Width)) - Assert.Equal(new Size(size, size), nativeBoundingBox.Size); + var expectedSize = new Size(size, size); + AssertWithinTolerance(expectedSize, nativeBoundingBox.Size); } + } - bool CloseEnough(double value1, double value2) + protected void AssertWithinTolerance(double expected, double actual, double tolerance = 0.2, string message = "Value was not within tolerance.") + { + var diff = System.Math.Abs(expected - actual); + if (diff > tolerance) { - return System.Math.Abs(value2 - value1) < 0.2; + throw new XunitException($"{message} Expected: {expected}; Actual: {actual}; Tolerance {tolerance}"); } } + protected void AssertWithinTolerance(Graphics.Size expected, Graphics.Size actual, double tolerance = 0.2) + { + AssertWithinTolerance(expected.Height, actual.Height, tolerance, "Height was not within tolerance."); + AssertWithinTolerance(expected.Width, actual.Width, tolerance, "Width was not within tolerance."); + } [Theory(DisplayName = "Native View Transforms are not empty" #if IOS