From 8bf574c30a7aa4e582e9476d7c538858df065c58 Mon Sep 17 00:00:00 2001 From: tj-devel709 Date: Wed, 10 Apr 2024 09:33:07 -0500 Subject: [PATCH] Fix the iOS button to resize the image, respect padding, and respect spacing --- .../iOS/Extensions/ButtonExtensions.cs | 203 ++++++++++++++---- 1 file changed, 160 insertions(+), 43 deletions(-) diff --git a/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs b/src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs index f9986be709a9..59c28fd4dc2f 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,28 @@ 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 (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 +102,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; @@ -114,9 +123,9 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt else { imageInsets.Top -= (titleHeight / 2) + sharedSpacing; - imageInsets.Bottom += titleHeight / 2; + imageInsets.Bottom += (titleHeight / 2) + sharedSpacing; - titleInsets.Top += imageHeight / 2; + titleInsets.Top += (imageHeight / 2) + sharedSpacing; titleInsets.Bottom -= (imageHeight / 2) + sharedSpacing; } } @@ -128,12 +137,12 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt } else { - imageInsets.Top += titleHeight / 2; + 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) { @@ -143,11 +152,11 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt } else { - imageInsets.Left -= (titleWidth / 2) + sharedSpacing; - imageInsets.Right += titleWidth / 2; + imageInsets.Left -= (titleWidthMove / 2) + sharedSpacing; + imageInsets.Right += (titleWidthMove / 2) + sharedSpacing; } - titleInsets.Left += imageWidth / 2; + titleInsets.Left += (imageWidth / 2) + sharedSpacing; titleInsets.Right -= (imageWidth / 2) + sharedSpacing; } else if (layout.Position == ButtonContentLayout.ImagePosition.Right) @@ -158,12 +167,12 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt } 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; } } @@ -189,10 +198,118 @@ public static void UpdateContentLayout(this UIButton platformButton, Button butt platformButton.ImageEdgeInsets = imageInsets; platformButton.TitleEdgeInsets = titleInsets; platformButton.Superview?.SetNeedsLayout(); + return; + } + + var titleRectHeight = platformButton.GetTitleBoundingRect(padding).Height; + + var buttonContentHeight = + + (nfloat)Math.Max(titleRectHeight, platformButton.CurrentImage?.Size.Height ?? 0) + + (nfloat)padding.Top + + (nfloat)padding.Bottom; + + if (layout.Position == ButtonContentLayout.ImagePosition.Top || layout.Position == ButtonContentLayout.ImagePosition.Bottom) + { + buttonContentHeight += spacing; + buttonContentHeight += (nfloat)Math.Min(titleRectHeight, platformButton.CurrentImage?.Size.Height ?? 0); + } + + // 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(); } #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) { var text = TextTransformUtilites.GetTransformedText(button.Text, button.TextTransform); @@ -217,4 +334,4 @@ public static void UpdateLineBreakMode(this UIButton nativeButton, Button button }; } } -} \ No newline at end of file +}