Skip to content

Commit

Permalink
Fix Regression with iOS button resizing with text or image change (#2…
Browse files Browse the repository at this point in the history
…5122)

* allow remeasure for title and image change

* add more to the test

* screenshots

* screenshots 2

* round the titleRect manual measurement

* remove one condition for adding additional padding

* change internal field to method

* Always measure the titleRect and fix edge case with slim titleRects

* screenshots

* more screenshots
  • Loading branch information
tj-devel709 authored Oct 14, 2024
1 parent 7a74d0f commit ee80709
Show file tree
Hide file tree
Showing 28 changed files with 988 additions and 40 deletions.
71 changes: 32 additions & 39 deletions src/Controls/src/Core/Button/Button.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ namespace Microsoft.Maui.Controls
{
public partial class Button : ICrossPlatformLayout
{
// _originalImageSize and _originalImageSource are used to ensure we don't resize the image
// larger than the original image size and to ensure if a new image is loaded, we use that image's size for resizing.
CGSize _originalImageSize = CGSize.Empty;

// _isFirstMeasure is a flag to make sure we manually recalculate the titleRect when there are dynamic changes to the button.
// There are times the platformButton.TitleLabel is updated on dynamic changes and reacts to the change by truncating the label when we actually
// have space in our constraints. We provide the space to be used in our first measure of the titleRect and then use the newly laid out titleRect in later iterations.
bool _isFirstMeasure = true;
string _originalImageSource = string.Empty;

/// <summary>
/// Measure the desired size of the button based on the image and title size taking into account
Expand Down Expand Up @@ -86,8 +84,9 @@ Size ICrossPlatformLayout.CrossPlatformMeasure(double widthConstraint, double he
if (image is not null)
{
// Save the original image size for later image resizing
if (_originalImageSize == CGSize.Empty)
if (_originalImageSize == CGSize.Empty || _originalImageSource == string.Empty || _originalImageSource != button.ImageSource.ToString())
{
_originalImageSource = button.ImageSource.ToString();
_originalImageSize = image.Size;
}

Expand Down Expand Up @@ -144,11 +143,8 @@ Size ICrossPlatformLayout.CrossPlatformMeasure(double widthConstraint, double he
}
}

// if we are in a scenario with unlimited width and the image is on top or bottom,
// of if the horizontalOption is not fill and the image is on top or bottom,
// let's make sure the title is not cut off by ensuring we have enough padding for the image and title.
// if the image is on top or bottom, let's make sure the title is not cut off by ensuring we have enough padding for the image and title.
if (image is not null
&& (widthConstraint == double.PositiveInfinity || button.HorizontalOptions != LayoutOptions.Fill)
&& (layout.Position == ButtonContentLayout.ImagePosition.Top || layout.Position == ButtonContentLayout.ImagePosition.Bottom))
{
var maxTitleRect = ComputeTitleRect(platformButton, button, image, double.PositiveInfinity, double.PositiveInfinity, borderWidth, padding, true);
Expand All @@ -163,8 +159,6 @@ Size ICrossPlatformLayout.CrossPlatformMeasure(double widthConstraint, double he
var returnSize = new Size(Math.Min(buttonContentWidth, widthConstraint),
Math.Min(buttonContentHeight, heightConstraint));

_isFirstMeasure = false;

// Rounding the values up to the nearest whole number to match UIView.SizeThatFits
return new Size((int)Math.Ceiling(returnSize.Width), (int)Math.Ceiling(returnSize.Height));
}
Expand All @@ -183,8 +177,6 @@ Size ICrossPlatformLayout.CrossPlatformArrange(Rect bounds)
// Layout the image and title of the button
LayoutButton(platformButton, this, bounds);

_isFirstMeasure = true;

return new Size(bounds.Width, bounds.Height);
}

Expand Down Expand Up @@ -292,39 +284,40 @@ CGRect ComputeTitleRect (UIButton platformButton, Button button, UIImage image,
return CGRect.Empty;
}

// Use the current TitleLabel if it is set and valid
var titleRect = platformButton.TitleLabel.Bounds;
var titleWidthConstraint = widthConstraint - ((nfloat)borderWidth * 2);
var titleHeightConstraint = heightConstraint - ((nfloat)borderWidth * 2);

if ((isMeasuring && _isFirstMeasure) || titleRect.Height == 0 || titleRect.Width == 0)
if (image is not null && !string.IsNullOrEmpty(platformButton.CurrentTitle) && titleWidthConstraint != double.PositiveInfinity)
{
var titleWidthConstraint = widthConstraint - ((nfloat)borderWidth * 2);
var titleHeightConstraint = heightConstraint - ((nfloat)borderWidth * 2);
// In non-UIButtonConfiguration setups, the title will always be truncated by the image's width
// even when the image is on top or bottom.
titleWidthConstraint -= image.Size.Width;
}

if (image is not null && !string.IsNullOrEmpty(platformButton.CurrentTitle) && titleWidthConstraint != double.PositiveInfinity)
{
// In non-UIButtonConfiguration setups, the title will always be truncated by the image's width
// even when the image is on top or bottom.
titleWidthConstraint -= image.Size.Width;
}
if (image is not null && button.ContentLayout.Position == ButtonContentLayout.ImagePosition.Left || button.ContentLayout.Position == ButtonContentLayout.ImagePosition.Right)
{
titleWidthConstraint -= (nfloat)(button.ContentLayout.Spacing + padding.Left + padding.Right);
}

if (image is not null && button.ContentLayout.Position == ButtonContentLayout.ImagePosition.Left || button.ContentLayout.Position == ButtonContentLayout.ImagePosition.Right)
{
titleWidthConstraint -= (nfloat)(button.ContentLayout.Spacing + padding.Left + padding.Right);
}
else if (image is null)
{
titleWidthConstraint -= (nfloat)(padding.Left + padding.Right);
}

else if (image is null)
{
titleWidthConstraint -= (nfloat)(padding.Left + padding.Right);
}
var titleRect = platformButton.GetTitleBoundingRect(titleWidthConstraint, titleHeightConstraint);

titleRect = platformButton.GetTitleBoundingRect(titleWidthConstraint, titleHeightConstraint);
}
var currentTitleText = platformButton.CurrentTitle;

// Measure the width of the sample character string using the same font as the TitleLabel. If a character cannot fit in the titleRect, let's use a zero size.
var minimumCharacterWidth = new Foundation.NSString("A").GetSizeUsingAttributes(new UIStringAttributes { Font = platformButton.TitleLabel.Font });
if (double.IsNaN(titleRect.Width) || double.IsNaN(titleRect.Height) || titleRect.Width < minimumCharacterWidth.Width)
// We will only do this for buttons with image on left and right because the left and right padding are handled differently
// when the image is on the top or bottom
if (currentTitleText.Length > 0 && button.ContentLayout.Position == ButtonContentLayout.ImagePosition.Left || button.ContentLayout.Position == ButtonContentLayout.ImagePosition.Right)
{
titleRect = Rect.Zero;
// Measure the width of the first character in the string using the same font as the TitleLabel. If a character cannot fit in the titleRect, let's use a zero size.
var minimumCharacterWidth = new Foundation.NSString(currentTitleText.Substring(0,1)).GetSizeUsingAttributes(new UIStringAttributes { Font = platformButton.TitleLabel.Font });
if (double.IsNaN(titleRect.Width) || double.IsNaN(titleRect.Height) || titleRect.Width < minimumCharacterWidth.Width)
{
titleRect = Rect.Zero;
}
}

return titleRect;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ internal static CGRect GetTitleBoundingRect(this UIButton platformButton, double
NSStringDrawingOptions.UsesLineFragmentOrigin | NSStringDrawingOptions.UsesFontLeading | NSStringDrawingOptions.UsesDeviceMetrics,
null);

return new CGRect(boundingRectWithDeviceMetrics.Location, new CGSize(Math.Max(boundingRect.Width, boundingRectWithDeviceMetrics.Width), Math.Min(availableHeight, boundingRect.Height)));
return new CGRect(boundingRectWithDeviceMetrics.Location,
new CGSize(Math.Ceiling(Math.Max(boundingRect.Width, boundingRectWithDeviceMetrics.Width)),
Math.Ceiling(Math.Min(availableHeight, boundingRect.Height))));
}

return CGRect.Empty;
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
<MauiImage Update="Resources\Images\dotnet_bot.svg" Color="#FFFFFF" BaseSize="168,208" />
<MauiImage Update="Resources\Images\dotnet_bot_resized.svg" Color="#FFFFFF" BaseSize="20,20" />
<MauiImage Update="Resources\Images\dotnet_bot_resized2.svg" Color="#FFFFFF" BaseSize="40,40" />
<MauiImage Update="Resources\Images\dotnet_bot_resized3.svg" Color="#FFFFFF" BaseSize="70,70" />
<MauiImage Include="Resources\Images\dotnet_bot.svg" Link="Resources\Images\small_dotnet_bot.svg" Color="#FFFFFF" BaseSize="64,64" />
<MauiImage Include="Resources\AppIcons\appicon.svg" ForegroundFile="Resources\AppIcons\appicon_foreground.svg" IsAppIcon="true" />
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#FFFFFF" BaseSize="168,208" />
Expand Down
108 changes: 108 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue25074.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue25074"
Title="Issue25074">
<VerticalStackLayout>
<Button
AutomationId="Button1"
x:Name="Button1"
Background="Purple"
ImageSource="dotnet_bot_resized.png"
ContentLayout="Top, 0"
TextColor="White"
Text="small"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
<Button
AutomationId="Button2"
x:Name="Button2"
Background="Purple"
ImageSource="dotnet_bot_resized3.png"
ContentLayout="Top, 0"
TextColor="White"
Text="small"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
<Button
AutomationId="Button3"
x:Name="Button3"
Background="Purple"
ImageSource="dotnet_bot_resized.png"
ContentLayout="Top, 0"
TextColor="White"
Text="Start with an even longer title"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
<Button
AutomationId="Button4"
x:Name="Button4"
Background="Purple"
ImageSource="dotnet_bot_resized3.png"
ContentLayout="Top, 0"
TextColor="White"
Text="Start with an even longer title"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
<Button
AutomationId="Button5"
x:Name="Button5"
Background="Purple"
ImageSource="dotnet_bot_resized.png"
ContentLayout="Top, 0"
TextColor="White"
Text="small"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
<Button
AutomationId="Button6"
x:Name="Button6"
Background="Purple"
ImageSource="dotnet_bot_resized3.png"
ContentLayout="Top, 0"
TextColor="White"
Text="small"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
<Button
AutomationId="Button7"
x:Name="Button7"
Background="Purple"
ImageSource="dotnet_bot_resized.png"
ContentLayout="Top, 0"
TextColor="White"
Text="Start with an even longer title"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
<Button
AutomationId="Button8"
x:Name="Button8"
Background="Purple"
ImageSource="dotnet_bot_resized3.png"
ContentLayout="Top, 0"
TextColor="White"
Text="Start with an even longer title"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
<Button
AutomationId="Button9"
x:Name="Button9"
Background="Purple"
ImageSource="dotnet_bot_resized.png"
ContentLayout="Top, 0"
TextColor="White"
Text="small"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
<Button
AutomationId="Button10"
x:Name="Button10"
Background="Purple"
ImageSource="dotnet_bot_resized3.png"
ContentLayout="Top, 0"
TextColor="White"
Text="Start with an even longer title"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ContentPage>
52 changes: 52 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue25074.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace Maui.Controls.Sample.Issues;

[XamlCompilation(XamlCompilationOptions.Compile)]
[Issue(IssueTracker.Github, 25074, "Buttons update size when text or image change", PlatformAffected.iOS)]
public partial class Issue25074 : ContentPage
{
bool toggle = true;
public Issue25074()
{
InitializeComponent();
}

private void OnCounterClicked(object sender, EventArgs e)
{
if (toggle)
{
Button1.Text = "Give the button a much longer title";
Button2.Text = "Give the button a much longer title";
Button3.Text = "small";
Button4.Text = "small";
Button5.ImageSource = "dotnet_bot_resized3.png";
Button6.ImageSource = "dotnet_bot_resized.png";
Button7.ImageSource = "dotnet_bot_resized3.png";
Button8.ImageSource = "dotnet_bot_resized.png";

Button9.Text = "Give the button a much longer title";
Button9.ImageSource = "dotnet_bot_resized3.png";

Button10.Text = "small";
Button10.ImageSource = "dotnet_bot_resized.png";
}
else
{
Button1.Text = "small";
Button2.Text = "small";
Button3.Text = "Start with an even longer title";
Button4.Text = "Start with an even longer title";
Button5.ImageSource = "dotnet_bot_resized.png";
Button6.ImageSource = "dotnet_bot_resized3.png";
Button7.ImageSource = "dotnet_bot_resized.png";
Button8.ImageSource = "dotnet_bot_resized3.png";

Button9.Text = "small";
Button9.ImageSource = "dotnet_bot_resized.png";

Button10.Text = "Start with an even longer title";
Button10.ImageSource = "dotnet_bot_resized3.png";
}

toggle = !toggle;
}
}
18 changes: 18 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue25074_2.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue25074_2"
Title="Issue25074_2">
<VerticalStackLayout>
<Button
x:Name="CounterBtn"
AutomationId="Button1"
Background="Purple"
ImageSource="dotnet_bot.png"
ContentLayout="Top, 0"
TextColor="White"
Text="This is a long title that may be truncated."
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ContentPage>
30 changes: 30 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue25074_2.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;

namespace Maui.Controls.Sample.Issues;

[XamlCompilation(XamlCompilationOptions.Compile)]
[Issue(IssueTracker.Github, 25074_2, "Button title can extend past previously truncated size", PlatformAffected.iOS)]
public partial class Issue25074_2 : ContentPage
{
public Issue25074_2()
{
InitializeComponent();
}

int count = 0;

private void OnCounterClicked(object sender, EventArgs e)
{
count++;

if (count % 2 == 0)
{
CounterBtn.ImageSource = "dotnet_bot.png";
}
else
{
CounterBtn.ImageSource = "dotnet_bot_resized.png";
}
}
}
Loading

0 comments on commit ee80709

Please sign in to comment.