Skip to content

Commit 6786d61

Browse files
[iOS/Mac] Fix Image not enlarging to explicit HeightRequest/WidthRequest (#37267)
<!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Issue Details - On iOS, an Image does not scale up on the unconstrained axis when only HeightRequest or WidthRequest is set—the unconstrained axis remains capped at its native size. This is most visible with HorizontalStackLayout + HeightRequest and VerticalStackLayout + WidthRequest. The reverse combinations can appear to work because the other axis is large enough to mask the same issue. On Android, the Image scales correctly with HeightRequest and WidthRequest in all cases. ### Root Cause - In SizeThatFitsImage (iOS), the scale factor for ScaleAspectFit images was calculated as Math.Min(imageWidth, widthConstraint) / imageWidth. This could never exceed 1 (since it always divides the image's own size by itself once the constraint is larger), so an image could shrink to fit a smaller space but could never grow beyond its native size, even when an explicit HeightRequest or WidthRequest requested a larger size, regardless of StackLayout orientation. ### Description of Change - Updated SizeThatFitsImage in ImageViewExtensions.cs to accept flags indicating whether width and height constraints are explicit, and to allow images to scale up to requested sizes when either is set, instead of capping at native size. - Modified GetDesiredSizeFromHandler in ViewHandlerExtensions.iOS.cs to pass explicitness flags to SizeThatFitsImage, ensuring correct sizing behavior for images with explicit size requests. ### Issues Fixed Fixes #12879 ### Validated the behaviour in the following platforms - [ ] Windows - [ ] Android - [x] iOS - [x] Mac ### Output | Before | After | ----------|----------| | <img src="https://github.com/user-attachments/assets/c592756b-d87f-40dd-9073-0a4e03add828"> | <img src="https://github.com/user-attachments/assets/c884d1d2-a048-44ee-b1d9-ac59b9c29daf"> |
1 parent 7183cdc commit 6786d61

4 files changed

Lines changed: 115 additions & 9 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
namespace Maui.Controls.Sample.Issues;
2+
3+
[Issue(IssueTracker.Github, 12879, "Image.HeightRequest not respected when Image added in a Horizontal StackLayout", PlatformAffected.iOS)]
4+
public class Issue12879 : ContentPage
5+
{
6+
const double ExpectedSize = 200;
7+
const double Tolerance = 2;
8+
9+
Image _image;
10+
Label _imageStatusLabel;
11+
12+
public Issue12879()
13+
{
14+
_imageStatusLabel = new Label
15+
{
16+
Text = "Tap the button to check if the Image respects the explicit HeightRequest",
17+
AutomationId = "ImageStatusLabel"
18+
};
19+
20+
_image = new Image
21+
{
22+
Source = "blue.png",
23+
HeightRequest = ExpectedSize,
24+
AutomationId = "TestImage"
25+
};
26+
27+
Button checkImageButton = new Button
28+
{
29+
Text = "Check Image Size",
30+
AutomationId = "CheckImageButton"
31+
};
32+
checkImageButton.Clicked += OnCheckImageButtonClicked;
33+
34+
Content = new VerticalStackLayout
35+
{
36+
Spacing = 20,
37+
Children =
38+
{
39+
_imageStatusLabel,
40+
new HorizontalStackLayout
41+
{
42+
Children = { _image }
43+
},
44+
checkImageButton
45+
}
46+
};
47+
}
48+
49+
void OnCheckImageButtonClicked(object sender, EventArgs e)
50+
{
51+
bool imagePasses = Math.Abs(_image.Width - ExpectedSize) <= Tolerance && Math.Abs(_image.Height - _image.HeightRequest) <= Tolerance;
52+
53+
_imageStatusLabel.Text = imagePasses
54+
? "Success"
55+
: $"Fail: Image={_image.Width}x{_image.Height}, Expected={ExpectedSize}x{_image.HeightRequest}";
56+
}
57+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using NUnit.Framework;
2+
using UITest.Appium;
3+
using UITest.Core;
4+
5+
namespace Microsoft.Maui.TestCases.Tests.Issues;
6+
7+
public class Issue12879 : _IssuesUITest
8+
{
9+
public Issue12879(TestDevice device) : base(device)
10+
{
11+
}
12+
13+
public override string Issue => "Image.HeightRequest not respected when Image added in a Horizontal StackLayout";
14+
15+
[Test]
16+
[Category(UITestCategories.Image)]
17+
public void ImageScalesUpToExplicitHeightRequestInHorizontalStackLayout()
18+
{
19+
App.WaitForElement("CheckImageButton");
20+
App.Tap("CheckImageButton");
21+
22+
var statusText = App.WaitForElement("ImageStatusLabel").GetText();
23+
Assert.That(statusText, Is.EqualTo("Success"));
24+
}
25+
}

src/Core/src/Handlers/ViewHandlerExtensions.iOS.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,12 @@ internal static Size GetDesiredSizeFromHandler(this IViewHandler viewHandler, do
8484
// This also affects ImageButtons
8585
if (platformView is UIImageView imageView)
8686
{
87-
widthConstraint = IsExplicitSet(virtualView.Width) ? virtualView.Width : widthConstraint;
88-
heightConstraint = IsExplicitSet(virtualView.Height) ? virtualView.Height : heightConstraint;
87+
var widthIsExplicit = IsExplicitSet(virtualView.Width);
88+
var heightIsExplicit = IsExplicitSet(virtualView.Height);
89+
widthConstraint = widthIsExplicit ? virtualView.Width : widthConstraint;
90+
heightConstraint = heightIsExplicit ? virtualView.Height : heightConstraint;
8991

90-
sizeThatFits = imageView.SizeThatFitsImage(new CGSize((float)widthConstraint, (float)heightConstraint));
92+
sizeThatFits = imageView.SizeThatFitsImage(new CGSize((float)widthConstraint, (float)heightConstraint), default, widthIsExplicit, heightIsExplicit);
9193
}
9294
else if (platformView is LayoutView || platformView is MauiLabel)
9395
{

src/Core/src/Platform/iOS/ImageViewExtensions.cs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,15 @@ public static void UpdateSource(this UIImageView imageView, UIImage? uIImage, II
6868
/// <param name="imageView">The <see cref="UIImageView"/> to be measured.</param>
6969
/// <param name="constraints">The specified size constraints.</param>
7070
/// <param name="padding"></param>
71+
/// <param name="widthConstraintIsExplicit">Whether the width constraint comes from an explicit request (e.g. WidthRequest) and may therefore scale the image beyond its native width.</param>
72+
/// <param name="heightConstraintIsExplicit">Whether the height constraint comes from an explicit request (e.g. HeightRequest) and may therefore scale the image beyond its native height.</param>
7173
/// <returns>The size where the image would fit depending on the aspect ratio.</returns>
7274
internal static CGSize SizeThatFitsImage(
7375
this UIImageView imageView,
7476
CGSize constraints,
75-
Thickness padding = default)
77+
Thickness padding = default,
78+
bool widthConstraintIsExplicit = false,
79+
bool heightConstraintIsExplicit = false)
7680
{
7781
// If there's no image, we don't need to take up any space
7882
if (imageView.Image is null)
@@ -87,9 +91,8 @@ internal static CGSize SizeThatFitsImage(
8791
var horizontalThickness = padding.HorizontalThickness;
8892
var verticalThickness = padding.VerticalThickness;
8993

90-
var widthConstraint = constraints.Width - horizontalThickness;
91-
var heightConstraint = constraints.Height - verticalThickness;
92-
94+
double widthConstraint = constraints.Width - horizontalThickness;
95+
double heightConstraint = constraints.Height - verticalThickness;
9396

9497
var constrainedWidth = Math.Min(imageWidth, widthConstraint);
9598
var constrainedHeight = Math.Min(imageHeight, heightConstraint);
@@ -98,10 +101,29 @@ internal static CGSize SizeThatFitsImage(
98101
// that can fit it
99102
if (imageView.ContentMode == UIViewContentMode.ScaleAspectFit)
100103
{
101-
var widthRatio = constrainedWidth / imageWidth;
102-
var heightRatio = constrainedHeight / imageHeight;
104+
// Compute the raw (uncapped) ratio for each axis. When an axis constraint is +Infinity
105+
// (i.e. unconstrained, as happens on the cross axis of a StackLayout), its ratio is also
106+
// +Infinity, meaning it never limits the scale factor - only the other (finite/explicit) axis does.
107+
var widthRatio =
108+
double.IsPositiveInfinity(widthConstraint)
109+
? double.PositiveInfinity
110+
: widthConstraint / imageWidth;
111+
112+
var heightRatio =
113+
double.IsPositiveInfinity(heightConstraint)
114+
? double.PositiveInfinity
115+
: heightConstraint / imageHeight;
116+
103117
var scaleFactor = Math.Min(widthRatio, heightRatio);
104118

119+
// Only when NEITHER axis constraint came from an explicit request (WidthRequest/HeightRequest) -
120+
// i.e. we're simply fitting within available space, not honoring an explicit enlarge request -
121+
// do we cap the scale factor at 1 so the image never grows beyond its native size.
122+
if (!widthConstraintIsExplicit && !heightConstraintIsExplicit)
123+
{
124+
scaleFactor = Math.Min(scaleFactor, 1);
125+
}
126+
105127
return new CGSize(imageWidth * scaleFactor + horizontalThickness, imageHeight * scaleFactor + verticalThickness);
106128
}
107129

0 commit comments

Comments
 (0)