From 84067e1f94bc219944f355cb573d270db0087bbd Mon Sep 17 00:00:00 2001 From: tj-devel709 Date: Wed, 10 Apr 2024 09:33:07 -0500 Subject: [PATCH 01/73] Changes from the original Button PR #21759 --- .../iOS/Extensions/ButtonExtensions.cs | 254 ++++++++++++++---- 1 file changed, 195 insertions(+), 59 deletions(-) diff --git a/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs b/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs index 1f7d3d80ff5a..a29d57c0bc04 100644 --- a/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs +++ b/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs @@ -2,6 +2,7 @@ using System; using CoreGraphics; using Foundation; +using Microsoft.Extensions.Logging; using Microsoft.Maui.Controls.Internals; using ObjCRuntime; using UIKit; @@ -11,7 +12,7 @@ namespace Microsoft.Maui.Controls.Platform { public static class ButtonExtensions { - static CGRect GetTitleBoundingRect(this UIButton platformButton) + static CGRect GetTitleBoundingRect(this UIButton platformButton, Thickness padding) { if (platformButton.CurrentAttributedTitle != null || platformButton.CurrentTitle != null) @@ -20,10 +21,28 @@ static CGRect GetTitleBoundingRect(this UIButton platformButton) platformButton.CurrentAttributedTitle ?? new NSAttributedString(platformButton.CurrentTitle, new UIStringAttributes { Font = platformButton.TitleLabel.Font }); - return title.GetBoundingRect( - platformButton.Bounds.Size, - NSStringDrawingOptions.UsesLineFragmentOrigin | NSStringDrawingOptions.UsesFontLeading, + // 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, 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; @@ -31,34 +50,10 @@ static CGRect GetTitleBoundingRect(this UIButton platformButton) 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); } @@ -77,19 +72,31 @@ 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. - var titleRect = platformButton.GetTitleBoundingRect(); + // 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 titleWidth = titleRect.Width; var titleHeight = titleRect.Height; var imageWidth = image.Size.Width; @@ -98,10 +105,15 @@ 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 += titleWidth / 2; - imageInsets.Right -= titleWidth / 2; + imageInsets.Left += titleWidthMove / 2; + imageInsets.Right -= titleWidthMove / 2; titleInsets.Left -= imageWidth / 2; titleInsets.Right += imageWidth / 2; @@ -111,14 +123,12 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt { contentMode = UIViewContentMode.Top; } - else - { - imageInsets.Top -= (titleHeight / 2) + sharedSpacing; - imageInsets.Bottom += titleHeight / 2; - titleInsets.Top += imageHeight / 2; - titleInsets.Bottom -= (imageHeight / 2) + sharedSpacing; - } + imageInsets.Top -= (titleHeight / 2) + sharedSpacing; + imageInsets.Bottom += (titleHeight / 2) + sharedSpacing; + + titleInsets.Top += (imageHeight / 2) + sharedSpacing; + titleInsets.Bottom -= (imageHeight / 2) + sharedSpacing; } else if (layout.Position == ButtonContentLayout.ImagePosition.Bottom) { @@ -126,14 +136,12 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt { contentMode = UIViewContentMode.Bottom; } - else - { - imageInsets.Top += titleHeight / 2; - imageInsets.Bottom -= (titleHeight / 2) + sharedSpacing; - } + + imageInsets.Top += (titleHeight / 2) + sharedSpacing; + imageInsets.Bottom -= (titleHeight / 2) + sharedSpacing; titleInsets.Top -= (imageHeight / 2) + sharedSpacing; - titleInsets.Bottom += imageHeight / 2; + titleInsets.Bottom += (imageHeight / 2) + sharedSpacing; } else if (layout.Position == ButtonContentLayout.ImagePosition.Left) { @@ -141,14 +149,13 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt { contentMode = UIViewContentMode.Left; } - else - { - imageInsets.Left -= (titleWidth / 2) + sharedSpacing; - imageInsets.Right += titleWidth / 2; - } - titleInsets.Left += imageWidth / 2; + imageInsets.Left -= (titleWidthMove / 2) + sharedSpacing; + imageInsets.Right += (titleWidthMove / 2) + sharedSpacing; + + titleInsets.Left += (imageWidth / 2) + sharedSpacing; titleInsets.Right -= (imageWidth / 2) + sharedSpacing; + } else if (layout.Position == ButtonContentLayout.ImagePosition.Right) { @@ -156,17 +163,21 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt { contentMode = UIViewContentMode.Right; } - else - { - imageInsets.Left += titleWidth / 2; - imageInsets.Right -= (titleWidth / 2) + sharedSpacing; - } + + imageInsets.Left += (titleWidthMove / 2) + sharedSpacing; + imageInsets.Right -= (titleWidthMove / 2) + sharedSpacing; titleInsets.Left -= (imageWidth / 2) + sharedSpacing; - titleInsets.Right += imageWidth / 2; + titleInsets.Right += (imageWidth / 2) + sharedSpacing; } } + // 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. @@ -182,7 +193,7 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt platformButton.UpdatePadding(button); -#pragma warning disable CA1416, CA1422 // TODO: [UnsupportedOSPlatform("ios15.0")] +#pragma warning disable CA1416, CA1422 if (platformButton.ImageEdgeInsets != imageInsets || platformButton.TitleEdgeInsets != titleInsets) { @@ -191,6 +202,131 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt platformButton.GetSuperViewIfWindowSet()?.SetNeedsLayout(); } #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) @@ -217,4 +353,4 @@ public static void UpdateLineBreakMode(this UIButton nativeButton, Button button }; } } -} \ No newline at end of file +} From 6bc8ab2ec2183996cf68d43184f4e66f127b8140 Mon Sep 17 00:00:00 2001 From: tj-devel709 Date: Thu, 16 May 2024 17:02:35 -0500 Subject: [PATCH 02/73] Move the button layout code to ArrangeOverride --- src/Controls/src/Core/Button/Button.iOS.cs | 299 +++++++++++++++++- .../iOS/Extensions/ButtonExtensions.cs | 271 +--------------- 2 files changed, 300 insertions(+), 270 deletions(-) diff --git a/src/Controls/src/Core/Button/Button.iOS.cs b/src/Controls/src/Core/Button/Button.iOS.cs index 8a7c0ea836ae..a3e34e11b0d6 100644 --- a/src/Controls/src/Core/Button/Button.iOS.cs +++ b/src/Controls/src/Core/Button/Button.iOS.cs @@ -5,18 +5,315 @@ using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; +using UIKit; +using CoreGraphics; +using Microsoft.Extensions.Logging; namespace Microsoft.Maui.Controls { public partial class Button { + internal CGSize _originalImageSize = CGSize.Empty; + protected override Size ArrangeOverride(Rect bounds) { var result = base.ArrangeOverride(bounds); - Handler?.UpdateValue(nameof(ContentLayout)); + + var button = this; + var platformButton = Handler?.PlatformView as UIButton; + + if (button == null || platformButton == null) + { + return result; + } + + if (platformButton.Bounds.Width == 0) + { + return result; + } + + var imageInsets = new UIEdgeInsets(); + var titleInsets = new UIEdgeInsets(); + + var layout = button.ContentLayout; + var spacing = (nfloat)layout.Spacing; + + 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 is not null && !string.IsNullOrEmpty(platformButton.CurrentTitle)) + { + // Save the original image size the first time before resizing + if (_originalImageSize == CGSize.Empty) + { + _originalImageSize = platformButton.CurrentImage.Size; + } + + // Resize the image if necessary and then update the image variable + if (ResizeImageIfNecessary(platformButton, button, image, spacing, padding, bounds, _originalImageSize)) + { + image = platformButton.CurrentImage; + } + + // 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. + var titleRect = platformButton.GetTitleBoundingRect(padding); + var titleWidth = titleRect.Width; + var titleHeight = titleRect.Height; + var imageWidth = image.Size.Width; + var imageHeight = image.Size.Height; + var buttonWidth = platformButton.Bounds.Width; + 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; + titleInsets.Left -= imageWidth / 2; + titleInsets.Right += imageWidth / 2; + + if (layout.Position == ButtonContentLayout.ImagePosition.Top) + { + if (imageHeight > buttonHeight) + { + contentMode = UIViewContentMode.Top; + } + + imageInsets.Top -= (titleHeight / 2) + sharedSpacing; + imageInsets.Bottom += (titleHeight / 2) + sharedSpacing; + + titleInsets.Top += (imageHeight / 2) + sharedSpacing; + titleInsets.Bottom -= (imageHeight / 2) + sharedSpacing; + } + else if (layout.Position == ButtonContentLayout.ImagePosition.Bottom) + { + if (imageHeight > buttonHeight) + { + contentMode = UIViewContentMode.Bottom; + } + + imageInsets.Top += (titleHeight / 2) + sharedSpacing; + imageInsets.Bottom -= (titleHeight / 2) + sharedSpacing; + + titleInsets.Top -= (imageHeight / 2) + sharedSpacing; + titleInsets.Bottom += (imageHeight / 2) + sharedSpacing; + } + else if (layout.Position == ButtonContentLayout.ImagePosition.Left) + { + if (imageWidth > buttonWidth) + { + contentMode = UIViewContentMode.Left; + } + + imageInsets.Left -= (titleWidthMove / 2) + sharedSpacing; + imageInsets.Right += (titleWidthMove / 2) + sharedSpacing; + + titleInsets.Left += (imageWidth / 2) + sharedSpacing; + titleInsets.Right -= (imageWidth / 2) + sharedSpacing; + + } + else if (layout.Position == ButtonContentLayout.ImagePosition.Right) + { + if (imageWidth > buttonWidth) + { + contentMode = UIViewContentMode.Right; + } + + imageInsets.Left += (titleWidthMove / 2) + sharedSpacing; + imageInsets.Right -= (titleWidthMove / 2) + sharedSpacing; + + titleInsets.Left -= (imageWidth / 2) + sharedSpacing; + titleInsets.Right += (imageWidth / 2) + sharedSpacing; + } + } + + // If we just have an image, we can still resize it here + else if (image is not null) + { + ResizeImageIfNecessary(platformButton, button, image, 0, padding, bounds, _originalImageSize); + } + + platformButton.ImageView.ContentMode = contentMode; + + // This is used to match the behavior between platforms. + // If the image is too big then we just hide the label because + // the image is pushing the title out of the visible view. + // We can't use insets because then the title shows up outside the + // bounds of the UIButton. We could set the UIButton to clip bounds + // but that feels like it might cause confusing side effects + if (contentMode == UIViewContentMode.Center) + platformButton.TitleLabel.Layer.Hidden = false; + else + platformButton.TitleLabel.Layer.Hidden = true; + + platformButton.UpdatePadding(button); + +#pragma warning disable CA1416, CA1422 + if (platformButton.ImageEdgeInsets != imageInsets || + platformButton.TitleEdgeInsets != titleInsets) + { + platformButton.ImageEdgeInsets = imageInsets; + platformButton.TitleEdgeInsets = titleInsets; + platformButton.Superview?.SetNeedsLayout(); + platformButton.Superview?.LayoutIfNeeded(); + } +#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 + + var maxButtonHeight = Math.Min(buttonContentHeight, bounds.Height); + + // 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 (maxButtonHeight - button.Height > 1 && button.HeightRequest == -1) + { + var contentInsets = platformButton.ContentEdgeInsets; + + var additionalVerticalSpace = (maxButtonHeight - 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 + } + return result; } + static bool ResizeImageIfNecessary(UIButton platformButton, Button button, UIImage image, nfloat spacing, Thickness padding, Rect bounds, CGSize originalImageSize) + { + var currentImageWidth = image.Size.Width; + var currentImageHeight = image.Size.Height; + + nfloat availableWidth = (nfloat)bounds.Width; + nfloat availableHeight = (nfloat)bounds.Height; + + // 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 (bounds != Rect.Zero && (!double.IsNaN(bounds.Height) || !double.IsNaN(bounds.Width))) + { + var contentWidth = (nfloat)bounds.Width - (nfloat)padding.Left - (nfloat)padding.Right; + + if (currentImageWidth - contentWidth > buffer) + { + availableWidth = contentWidth; + } + + var contentHeight = (nfloat)bounds.Height - ((nfloat)padding.Top + (nfloat)padding.Bottom); + if (currentImageHeight - contentHeight > buffer) + { + availableHeight = contentHeight; + } + } + + // make sure we do not have negative values + availableWidth = (nfloat)Math.Max(availableWidth, 0.1f); + availableHeight = (nfloat)Math.Max(availableHeight, 0.1f); + + try + { + // if the image is too large then we will size it smaller + if (currentImageHeight - availableHeight > buffer || currentImageWidth - availableWidth > buffer) + { + image = ResizeImageSource(image, availableWidth, availableHeight, originalImageSize); + } + // if the image is already sized down but now has more space, we will size it up no more than the original image size + else if (availableHeight - ((nfloat)padding.Top + (nfloat)padding.Bottom) - currentImageHeight > buffer && availableWidth - (nfloat)padding.Left - (nfloat)padding.Right - currentImageWidth > buffer + && currentImageHeight != originalImageSize.Height && currentImageWidth != originalImageSize.Width) + { + image = ResizeImageSource(image, availableWidth, availableHeight, originalImageSize, true); + } + 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, CGSize originalImageSize, bool shouldScaleUp = false) + { + if (sourceImage is null || sourceImage.CGImage is null) + return null; + + maxWidth = (nfloat)Math.Min(maxWidth, originalImageSize.Width); + maxHeight = (nfloat)Math.Min(maxHeight, originalImageSize.Height); + + var sourceSize = sourceImage.Size; + + float maxResizeFactor = (float)Math.Min(maxWidth / sourceSize.Width, maxHeight / sourceSize.Height); + + if (maxResizeFactor > 1 && !shouldScaleUp) + return sourceImage; + + return UIImage.FromImage(sourceImage.CGImage, sourceImage.CurrentScale / maxResizeFactor, sourceImage.Orientation); + } + public static void MapText(ButtonHandler handler, Button button) => MapText((IButtonHandler)handler, button); diff --git a/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs b/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs index a29d57c0bc04..3d445c7ee977 100644 --- a/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs +++ b/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs @@ -12,7 +12,7 @@ namespace Microsoft.Maui.Controls.Platform { public static class ButtonExtensions { - static CGRect GetTitleBoundingRect(this UIButton platformButton, Thickness padding) + internal static CGRect GetTitleBoundingRect(this UIButton platformButton, Thickness padding) { if (platformButton.CurrentAttributedTitle != null || platformButton.CurrentTitle != null) @@ -59,274 +59,7 @@ public static void UpdatePadding(this UIButton platformButton, Button button) public static void UpdateContentLayout(this UIButton platformButton, Button button) { - if (platformButton.Bounds.Width == 0) - { - return; - } - - var imageInsets = new UIEdgeInsets(); - var titleInsets = new UIEdgeInsets(); - - var layout = button.ContentLayout; - var spacing = (nfloat)layout.Spacing; - - 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 titleWidth = titleRect.Width; - var titleHeight = titleRect.Height; - var imageWidth = image.Size.Width; - var imageHeight = image.Size.Height; - var buttonWidth = platformButton.Bounds.Width; - 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; - titleInsets.Left -= imageWidth / 2; - titleInsets.Right += imageWidth / 2; - - if (layout.Position == ButtonContentLayout.ImagePosition.Top) - { - if (imageHeight > buttonHeight) - { - contentMode = UIViewContentMode.Top; - } - - imageInsets.Top -= (titleHeight / 2) + sharedSpacing; - imageInsets.Bottom += (titleHeight / 2) + sharedSpacing; - - titleInsets.Top += (imageHeight / 2) + sharedSpacing; - titleInsets.Bottom -= (imageHeight / 2) + sharedSpacing; - } - else if (layout.Position == ButtonContentLayout.ImagePosition.Bottom) - { - if (imageHeight > buttonHeight) - { - contentMode = UIViewContentMode.Bottom; - } - - imageInsets.Top += (titleHeight / 2) + sharedSpacing; - imageInsets.Bottom -= (titleHeight / 2) + sharedSpacing; - - titleInsets.Top -= (imageHeight / 2) + sharedSpacing; - titleInsets.Bottom += (imageHeight / 2) + sharedSpacing; - } - else if (layout.Position == ButtonContentLayout.ImagePosition.Left) - { - if (imageWidth > buttonWidth) - { - contentMode = UIViewContentMode.Left; - } - - imageInsets.Left -= (titleWidthMove / 2) + sharedSpacing; - imageInsets.Right += (titleWidthMove / 2) + sharedSpacing; - - titleInsets.Left += (imageWidth / 2) + sharedSpacing; - titleInsets.Right -= (imageWidth / 2) + sharedSpacing; - - } - else if (layout.Position == ButtonContentLayout.ImagePosition.Right) - { - if (imageWidth > buttonWidth) - { - contentMode = UIViewContentMode.Right; - } - - imageInsets.Left += (titleWidthMove / 2) + sharedSpacing; - imageInsets.Right -= (titleWidthMove / 2) + sharedSpacing; - - titleInsets.Left -= (imageWidth / 2) + sharedSpacing; - titleInsets.Right += (imageWidth / 2) + sharedSpacing; - } - } - - // 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. - // If the image is too big then we just hide the label because - // the image is pushing the title out of the visible view. - // We can't use insets because then the title shows up outside the - // bounds of the UIButton. We could set the UIButton to clip bounds - // but that feels like it might cause confusing side effects - if (contentMode == UIViewContentMode.Center) - platformButton.TitleLabel.Layer.Hidden = false; - else - platformButton.TitleLabel.Layer.Hidden = true; - - platformButton.UpdatePadding(button); - -#pragma warning disable CA1416, CA1422 - if (platformButton.ImageEdgeInsets != imageInsets || - platformButton.TitleEdgeInsets != titleInsets) - { - platformButton.ImageEdgeInsets = imageInsets; - platformButton.TitleEdgeInsets = titleInsets; - platformButton.GetSuperViewIfWindowSet()?.SetNeedsLayout(); - } -#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); + (button as IView)?.InvalidateMeasure(); } public static void UpdateText(this UIButton platformButton, Button button) From 762721c5cf2b59ce995a59b7b0fee49af6902088 Mon Sep 17 00:00:00 2001 From: tj-devel709 Date: Thu, 16 May 2024 22:52:31 -0500 Subject: [PATCH 03/73] Add UITests and remove internal --- src/Controls/src/Core/Button/Button.iOS.cs | 2 +- .../TestCases.HostApp/Issues/Issue22306.xaml | 31 +++++++++++++++++++ .../Issues/Issue22306.xaml.cs | 28 +++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue22306.xaml create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/Issue22306.xaml.cs diff --git a/src/Controls/src/Core/Button/Button.iOS.cs b/src/Controls/src/Core/Button/Button.iOS.cs index a3e34e11b0d6..f07f75c43e75 100644 --- a/src/Controls/src/Core/Button/Button.iOS.cs +++ b/src/Controls/src/Core/Button/Button.iOS.cs @@ -13,7 +13,7 @@ namespace Microsoft.Maui.Controls { public partial class Button { - internal CGSize _originalImageSize = CGSize.Empty; + CGSize _originalImageSize = CGSize.Empty; protected override Size ArrangeOverride(Rect bounds) { diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue22306.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue22306.xaml new file mode 100644 index 000000000000..3540da96f15c --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue22306.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + +