diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue22288.xaml b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue22288.xaml
new file mode 100644
index 000000000000..c52e14df7a86
--- /dev/null
+++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue22288.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue22288.xaml.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue22288.xaml.cs
new file mode 100644
index 000000000000..7b3fcad6ee4d
--- /dev/null
+++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue22288.xaml.cs
@@ -0,0 +1,14 @@
+namespace Maui.Controls.Sample.Issues;
+using System;
+using Microsoft.Maui;
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+[Issue(IssueTracker.Github, 22288, "Top Button Content Causes Infinite Layout", PlatformAffected.iOS)]
+public partial class Issue22288 : ContentPage
+{
+ public Issue22288()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs b/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs
index d63afdb155e6..f9986be709a9 100644
--- a/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs
+++ b/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs
@@ -2,7 +2,6 @@
using System;
using CoreGraphics;
using Foundation;
-using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Internals;
using ObjCRuntime;
using UIKit;
@@ -12,7 +11,7 @@ namespace Microsoft.Maui.Controls.Platform
{
public static class ButtonExtensions
{
- static CGRect GetTitleBoundingRect(this UIButton platformButton, Thickness padding)
+ static CGRect GetTitleBoundingRect(this UIButton platformButton)
{
if (platformButton.CurrentAttributedTitle != null ||
platformButton.CurrentTitle != null)
@@ -21,28 +20,10 @@ static CGRect GetTitleBoundingRect(this UIButton platformButton, Thickness paddi
platformButton.CurrentAttributedTitle ??
new NSAttributedString(platformButton.CurrentTitle, new UIStringAttributes { Font = platformButton.TitleLabel.Font });
- // Use the available height when calculating the bounding rect
- var lineHeight = platformButton.TitleLabel.Font.LineHeight;
- var availableHeight = platformButton.Bounds.Size.Height;
-
- // If the line break mode is one of the truncation modes, limit the height to the line height
- if (platformButton.TitleLabel.LineBreakMode == UILineBreakMode.HeadTruncation ||
- platformButton.TitleLabel.LineBreakMode == UILineBreakMode.MiddleTruncation ||
- platformButton.TitleLabel.LineBreakMode == UILineBreakMode.TailTruncation ||
- platformButton.TitleLabel.LineBreakMode == UILineBreakMode.Clip)
- {
- availableHeight = lineHeight;
- }
-
- var availableSize = new CGSize(platformButton.Bounds.Size.Width, availableHeight);
-
- var boundingRect = title.GetBoundingRect(
- availableSize,
- NSStringDrawingOptions.UsesLineFragmentOrigin | NSStringDrawingOptions.UsesFontLeading | NSStringDrawingOptions.UsesDeviceMetrics,
+ return title.GetBoundingRect(
+ platformButton.Bounds.Size,
+ NSStringDrawingOptions.UsesLineFragmentOrigin | NSStringDrawingOptions.UsesFontLeading,
null);
-
- // NSStringDrawingOptions.UsesDeviceMetrics can split at characters instead of words but ignore the height. Pass the height constraint back in.
- return new CGRect(boundingRect.Location, new CGSize(boundingRect.Width, availableHeight));
}
return CGRect.Empty;
@@ -50,10 +31,34 @@ static CGRect GetTitleBoundingRect(this UIButton platformButton, Thickness paddi
public static void UpdatePadding(this UIButton platformButton, Button button)
{
+ double spacingVertical = 0;
+ double spacingHorizontal = 0;
+
+ if (button.ImageSource != null)
+ {
+ if (button.ContentLayout.IsHorizontal())
+ {
+ spacingHorizontal = button.ContentLayout.Spacing;
+ }
+ else
+ {
+ var imageHeight = platformButton.ImageView.Image?.Size.Height ?? 0f;
+
+ if (imageHeight < platformButton.Bounds.Height)
+ {
+ spacingVertical = button.ContentLayout.Spacing +
+ platformButton.GetTitleBoundingRect().Height;
+ }
+
+ }
+ }
+
var padding = button.Padding;
if (padding.IsNaN)
padding = ButtonHandler.DefaultPadding;
+ padding += new Thickness(spacingHorizontal / 2, spacingVertical / 2);
+
platformButton.UpdatePadding(padding);
}
@@ -72,31 +77,19 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt
var image = platformButton.CurrentImage;
+
// if the image is too large then we just position at the edge of the button
// depending on the position the user has picked
// This makes the behavior consistent with android
var contentMode = UIViewContentMode.Center;
- var padding = button.Padding;
- if (padding.IsNaN)
- padding = ButtonHandler.DefaultPadding;
-
- // If the button's image takes up too much space, we will want to hide the title
- var hidesTitle = false;
-
if (image != null && !string.IsNullOrEmpty(platformButton.CurrentTitle))
{
// TODO: Do not use the title label as it is not yet updated and
// if we move the image, then we technically have more
// space and will require a new layout pass.
- // Resize the image if necessary and then update the image variable
- if (ResizeImageIfNecessary(platformButton, button, image, spacing, padding))
- {
- image = platformButton.CurrentImage;
- }
-
- var titleRect = platformButton.GetTitleBoundingRect(padding);
+ var titleRect = platformButton.GetTitleBoundingRect();
var titleWidth = titleRect.Width;
var titleHeight = titleRect.Height;
var imageWidth = image.Size.Width;
@@ -105,15 +98,10 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt
var buttonHeight = platformButton.Bounds.Height;
var sharedSpacing = spacing / 2;
- // The titleWidth will include the part of the title that is potentially truncated. Let's figure out the max width of the title in the button for our calculations.
- // Note: we do not calculate spacing in maxTitleWidth since the original button laid out by iOS will not contain the spacing in the measurements.
- var maxTitleWidth = platformButton.Bounds.Width - (imageWidth + (nfloat)padding.Left + (nfloat)padding.Right);
- var titleWidthMove = (nfloat)Math.Min(maxTitleWidth, titleWidth);
-
// These are just used to shift the image and title to center
// Which makes the later math easier to follow
- imageInsets.Left += titleWidthMove / 2;
- imageInsets.Right -= titleWidthMove / 2;
+ imageInsets.Left += titleWidth / 2;
+ imageInsets.Right -= titleWidth / 2;
titleInsets.Left -= imageWidth / 2;
titleInsets.Right += imageWidth / 2;
@@ -123,12 +111,14 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt
{
contentMode = UIViewContentMode.Top;
}
+ else
+ {
+ imageInsets.Top -= (titleHeight / 2) + sharedSpacing;
+ imageInsets.Bottom += titleHeight / 2;
- imageInsets.Top -= (titleHeight / 2) + sharedSpacing;
- imageInsets.Bottom += (titleHeight / 2) + sharedSpacing;
-
- titleInsets.Top += (imageHeight / 2) + sharedSpacing;
- titleInsets.Bottom -= (imageHeight / 2) + sharedSpacing;
+ titleInsets.Top += imageHeight / 2;
+ titleInsets.Bottom -= (imageHeight / 2) + sharedSpacing;
+ }
}
else if (layout.Position == ButtonContentLayout.ImagePosition.Bottom)
{
@@ -136,12 +126,14 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt
{
contentMode = UIViewContentMode.Bottom;
}
-
- imageInsets.Top += (titleHeight / 2) + sharedSpacing;
- imageInsets.Bottom -= (titleHeight / 2) + sharedSpacing;
+ else
+ {
+ imageInsets.Top += titleHeight / 2;
+ imageInsets.Bottom -= (titleHeight / 2) + sharedSpacing;
+ }
titleInsets.Top -= (imageHeight / 2) + sharedSpacing;
- titleInsets.Bottom += (imageHeight / 2) + sharedSpacing;
+ titleInsets.Bottom += imageHeight / 2;
}
else if (layout.Position == ButtonContentLayout.ImagePosition.Left)
{
@@ -149,13 +141,14 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt
{
contentMode = UIViewContentMode.Left;
}
+ else
+ {
+ imageInsets.Left -= (titleWidth / 2) + sharedSpacing;
+ imageInsets.Right += titleWidth / 2;
+ }
- imageInsets.Left -= (titleWidthMove / 2) + sharedSpacing;
- imageInsets.Right += (titleWidthMove / 2) + sharedSpacing;
-
- titleInsets.Left += (imageWidth / 2) + sharedSpacing;
+ titleInsets.Left += imageWidth / 2;
titleInsets.Right -= (imageWidth / 2) + sharedSpacing;
-
}
else if (layout.Position == ButtonContentLayout.ImagePosition.Right)
{
@@ -163,21 +156,17 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt
{
contentMode = UIViewContentMode.Right;
}
-
- imageInsets.Left += (titleWidthMove / 2) + sharedSpacing;
- imageInsets.Right -= (titleWidthMove / 2) + sharedSpacing;
+ else
+ {
+ imageInsets.Left += titleWidth / 2;
+ imageInsets.Right -= (titleWidth / 2) + sharedSpacing;
+ }
titleInsets.Left -= (imageWidth / 2) + sharedSpacing;
- titleInsets.Right += (imageWidth / 2) + sharedSpacing;
+ titleInsets.Right += imageWidth / 2;
}
}
- // If we just have an image, we can still resize it here
- else if (image is not null)
- {
- ResizeImageIfNecessary(platformButton, button, image, 0, padding);
- }
-
platformButton.ImageView.ContentMode = contentMode;
// This is used to match the behavior between platforms.
@@ -193,141 +182,15 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt
platformButton.UpdatePadding(button);
-#pragma warning disable CA1416, CA1422
+#pragma warning disable CA1416, CA1422 // TODO: [UnsupportedOSPlatform("ios15.0")]
if (platformButton.ImageEdgeInsets != imageInsets ||
platformButton.TitleEdgeInsets != titleInsets)
{
platformButton.ImageEdgeInsets = imageInsets;
platformButton.TitleEdgeInsets = titleInsets;
platformButton.Superview?.SetNeedsLayout();
- return;
}
#pragma warning restore CA1416, CA1422
-
- var titleRectHeight = platformButton.GetTitleBoundingRect(padding).Height;
-
- var buttonContentHeight =
- + (nfloat)Math.Max(titleRectHeight, platformButton.CurrentImage?.Size.Height ?? 0)
- + (nfloat)padding.Top
- + (nfloat)padding.Bottom;
-
- if (image is not null && !string.IsNullOrEmpty(platformButton.CurrentTitle))
- {
- if (layout.Position == ButtonContentLayout.ImagePosition.Top || layout.Position == ButtonContentLayout.ImagePosition.Bottom)
- {
- if (!hidesTitle)
- {
- buttonContentHeight += spacing;
- buttonContentHeight += (nfloat)Math.Min(titleRectHeight, platformButton.CurrentImage?.Size.Height ?? 0);
- }
- // If the title is hidden, we don't need to add the spacing or the title to this measurement
- else
- {
- if (titleRectHeight > platformButton.CurrentImage.Size.Height)
- {
- buttonContentHeight -= titleRectHeight;
- buttonContentHeight += platformButton.CurrentImage.Size.Height;
- }
- }
- }
-
-#pragma warning disable CA1416, CA1422
- // If the button's content is larger than the button, we need to adjust the ContentEdgeInsets.
- // Apply a small buffer to the image size comparison since iOS can return a size that is off by a fraction of a pixel
- if (buttonContentHeight - button.Height > 1 && button.HeightRequest == -1)
- {
- var contentInsets = platformButton.ContentEdgeInsets;
-
- var additionalVerticalSpace = (buttonContentHeight - button.Height) / 2;
-
- platformButton.ContentEdgeInsets = new UIEdgeInsets(
- (nfloat)(additionalVerticalSpace + (nfloat)padding.Top),
- contentInsets.Left,
- (nfloat)(additionalVerticalSpace + (nfloat)padding.Bottom),
- contentInsets.Right);
-
- platformButton.Superview?.SetNeedsLayout();
- platformButton.Superview?.LayoutIfNeeded();
- }
-#pragma warning restore CA1416, CA1422
- }
- }
-
- static bool ResizeImageIfNecessary(UIButton platformButton, Button button, UIImage image, nfloat spacing, Thickness padding)
- {
- // If the image is on the left or right, we still have an implicit width constraint
- if (button.HeightRequest == -1 && button.WidthRequest == -1 && (button.ContentLayout.Position == ButtonContentLayout.ImagePosition.Top || button.ContentLayout.Position == ButtonContentLayout.ImagePosition.Bottom))
- {
- return false;
- }
-
- nfloat availableHeight = nfloat.MaxValue;
- nfloat availableWidth = nfloat.MaxValue;
-
- // Apply a small buffer to the image size comparison since iOS can return a size that is off by a fraction of a pixel.
- var buffer = 0.1;
-
- if (platformButton.Bounds != CGRect.Empty
- && (button.Height != double.NaN || button.Width != double.NaN))
- {
- var contentWidth = platformButton.Bounds.Width - (nfloat)padding.Left - (nfloat)padding.Right;
-
- if (image.Size.Width - contentWidth > buffer)
- {
- availableWidth = contentWidth;
- }
-
- var contentHeight = platformButton.Bounds.Height - ((nfloat)padding.Top + (nfloat)padding.Bottom);
- if (image.Size.Height - contentHeight > buffer)
- {
- availableHeight = contentHeight;
- }
- }
-
- availableHeight = button.HeightRequest == -1 ? nfloat.PositiveInfinity : (nfloat)Math.Max(availableHeight, 0);
- // availableWidth = button.WidthRequest == -1 ? platformButton.Bounds.Width : (nfloat)Math.Max(availableWidth, 0);
-
- availableWidth = (nfloat)Math.Max(availableWidth, 0);
-
- try
- {
- if (image.Size.Height - availableHeight > buffer || image.Size.Width - availableWidth > buffer)
- {
- image = ResizeImageSource(image, availableWidth, availableHeight);
- }
- else
- {
- return false;
- }
-
- image = image?.ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
-
- platformButton.SetImage(image, UIControlState.Normal);
-
- platformButton.Superview?.SetNeedsLayout();
-
- return true;
- }
- catch (Exception)
- {
- button.Handler.MauiContext?.CreateLogger()?.LogWarning("Can not load Button ImageSource");
- }
-
- return false;
- }
-
- static UIImage ResizeImageSource(UIImage sourceImage, nfloat maxWidth, nfloat maxHeight)
- {
- if (sourceImage is null || sourceImage.CGImage is null)
- return null;
-
- var sourceSize = sourceImage.Size;
- float maxResizeFactor = (float)Math.Min(maxWidth / sourceSize.Width, maxHeight / sourceSize.Height);
-
- if (maxResizeFactor > 1)
- return sourceImage;
-
- return UIImage.FromImage(sourceImage.CGImage, sourceImage.CurrentScale / maxResizeFactor, sourceImage.Orientation);
}
public static void UpdateText(this UIButton platformButton, Button button)
@@ -354,4 +217,4 @@ public static void UpdateLineBreakMode(this UIButton nativeButton, Button button
};
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Controls/tests/UITests/Tests/Issues/Issue18242.cs b/src/Controls/tests/UITests/Tests/Issues/Issue18242.cs
index 469044ba0d94..85b0eee10787 100644
--- a/src/Controls/tests/UITests/Tests/Issues/Issue18242.cs
+++ b/src/Controls/tests/UITests/Tests/Issues/Issue18242.cs
@@ -15,6 +15,8 @@ public Issue18242(TestDevice device) : base(device)
[Test]
public void Issue18242Test()
{
+ this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Mac, TestDevice.iOS }, "iOS will be fixed in https://github.com/dotnet/maui/pull/20953");
+
App.WaitForElement("WaitForStubControl");
VerifyScreenshot();
diff --git a/src/Controls/tests/UITests/Tests/Issues/Issue22288.cs b/src/Controls/tests/UITests/Tests/Issues/Issue22288.cs
new file mode 100644
index 000000000000..a5e8722495b7
--- /dev/null
+++ b/src/Controls/tests/UITests/Tests/Issues/Issue22288.cs
@@ -0,0 +1,34 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.AppiumTests.Issues
+{
+ public class Issue22288 : _IssuesUITest
+ {
+ public Issue22288(TestDevice device) : base(device) { }
+
+ public override string Issue => "Top Button Content Causes Infinite Layout";
+
+ [Test]
+ [Category(UITestCategories.Button)]
+ public void AppDoesntFreezeWhenRotatingDevice()
+ {
+ this.IgnoreIfPlatforms(new TestDevice[] { TestDevice.Mac, TestDevice.Windows });
+ try
+ {
+ App.SetOrientationPortrait();
+ var portraitRect = App.WaitForElement("outerScrollView").GetRect();
+ App.SetOrientationLandscape();
+ var landscapeRect = App.WaitForElement("outerScrollView").GetRect();
+
+ Assert.Greater(landscapeRect.Width, portraitRect.Width);
+
+ }
+ finally
+ {
+ App.SetOrientationPortrait();
+ }
+ }
+ }
+}
\ No newline at end of file