Skip to content

Commit

Permalink
Fix the iOS button to resize the image, respect padding, and respect …
Browse files Browse the repository at this point in the history
…spacing
  • Loading branch information
tj-devel709 committed Apr 10, 2024
1 parent 1d69366 commit 8bf574c
Showing 1 changed file with 160 additions and 43 deletions.
203 changes: 160 additions & 43 deletions src/Controls/src/Core/Platform/iOS/Extensions/ButtonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using CoreGraphics;
using Foundation;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Internals;
using ObjCRuntime;
using UIKit;
Expand All @@ -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)
Expand All @@ -20,45 +21,39 @@ 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;
}

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

Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -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;
}
}
Expand All @@ -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)
{
Expand All @@ -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)
Expand All @@ -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;
}
}

Expand All @@ -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<ButtonHandler>()?.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);
Expand All @@ -217,4 +334,4 @@ public static void UpdateLineBreakMode(this UIButton nativeButton, Button button
};
}
}
}
}

0 comments on commit 8bf574c

Please sign in to comment.