Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public override void LayoutSubviews()
base.LayoutSubviews();

const float padding = 5f;
var imageSize = _imageView.SizeThatFits(Bounds.Size);
var imageSize = _imageView.SizeThatFitsImage(Bounds.Size);
var fullStringSize = _label.SizeThatFits(Bounds.Size);

if (imageSize.Width > 0 && (string.IsNullOrEmpty(Text) || fullStringSize.Width > Bounds.Width / 3))
Expand Down
98 changes: 98 additions & 0 deletions src/Controls/tests/DeviceTests/Elements/Image/ImageTests.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using Xunit;

Expand All @@ -24,5 +25,102 @@ await InvokeOnMainThreadAsync(async () =>
Assert.True(platformImage.Value.Size.Width > 0);
});
}

[Fact("Large images resize correctly")]
public async Task LargeImagesResizeCorrectly()
{
SetupBuilder();
var layout = new StackLayout { MaximumWidthRequest = 200 };
var image = new Image
{
Background = Colors.Red,
Source = "big_white_horizontal.png",
Aspect = Aspect.AspectFit
};

layout.Add(image);

// big_white_horizontal is 2000x1000px, so it should perfectly fit into a 200px wide layout
await InvokeOnMainThreadAsync(async () =>
{
var handler = CreateHandler<LayoutHandler>(layout);

await image.Wait();

await handler.ToPlatform().AssertContainsColor(Colors.White, MauiContext);
await handler.ToPlatform().AssertDoesNotContainColor(Colors.Red, MauiContext);
});

// We check for both image and layout to ensure that image is the right size
Assert.Equal(100, image.Height);
Assert.Equal(200, image.Width);

Assert.Equal(100, layout.Height);
Assert.Equal(200, layout.Width);
}


// big_white_horizontal is 2000x1000px
// So when it has to fit a 50x200 space, it should resize to 50x25 (width is the smallest scaled dimension)
// When it has to fit in a 200x50 space, it should resize to 100x50 (height is the smallest scaled dimension)
[Theory("Aspect Fit resizes on smallest dimension")]
[InlineData(50, 200, 50, 25)]
[InlineData(200, 50, 100, 50)]
public async Task AspectFitResizesOnSmallestDimensions(int widthRequest, int heightRequest, int expectedWidth, int expectedHeight)
{
SetupBuilder();
var layout = new Grid { MaximumHeightRequest = heightRequest, MaximumWidthRequest = widthRequest };
var image = new Image
{
Source = "big_white_horizontal.png",
Aspect = Aspect.AspectFit
};

layout.Add(image);

await InvokeOnMainThreadAsync(async () =>
{
var handler = CreateHandler<LayoutHandler>(layout);

await image.Wait();

await handler.ToPlatform().AssertContainsColor(Colors.White, MauiContext);
});

Assert.Equal(expectedHeight, image.Height);
Assert.Equal(expectedWidth, image.Width);

Assert.Equal(expectedHeight, layout.Height);
Assert.Equal(expectedWidth, layout.Width);
}

[Fact]
public async Task ImagesRespectExplicitConstraints()
{
SetupBuilder();

var layout = new Grid();
var image = new Image
{
Source = "big_white_horizontal.png",
Aspect = Aspect.AspectFit,
HeightRequest = 100
};

layout.Add(image);

await InvokeOnMainThreadAsync(async () =>
{
var handler = CreateHandler<LayoutHandler>(layout);

await image.Wait();

await handler.ToPlatform().AssertContainsColor(Colors.White, MauiContext);
});

// We asked the image to have a fixed height, so it should resize accordingly even if it could grow in width
Assert.Equal(100, image.Height);
Assert.Equal(200, image.Width);
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 26 additions & 1 deletion src/Core/src/Handlers/ViewHandlerExtensions.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,32 @@ internal static Size GetDesiredSizeFromHandler(this IViewHandler viewHandler, do
widthConstraint = Math.Min(widthConstraint, virtualView.MaximumWidth);
heightConstraint = Math.Min(heightConstraint, virtualView.MaximumHeight);

var sizeThatFits = platformView.SizeThatFits(new CoreGraphics.CGSize((float)widthConstraint, (float)heightConstraint));
CGSize sizeThatFits;

// Calling SizeThatFits on an ImageView always returns the image's dimensions, so we need to call the extension method
// This also affects ImageButtons
if (platformView is UIImageView imageView)
{
widthConstraint = IsExplicitSet(virtualView.Width) ? virtualView.Width : widthConstraint;
heightConstraint = IsExplicitSet(virtualView.Height) ? virtualView.Height : heightConstraint;

sizeThatFits = imageView.SizeThatFitsImage(new CGSize((float)widthConstraint, (float)heightConstraint));
}
else if (platformView is UIButton imageButton && imageButton.ImageView?.Image is not null)
{
widthConstraint = IsExplicitSet(virtualView.Width) ? virtualView.Width : widthConstraint;
heightConstraint = IsExplicitSet(virtualView.Height) ? virtualView.Height : heightConstraint;

sizeThatFits = imageButton.ImageView.SizeThatFitsImage(new CGSize((float)widthConstraint, (float)heightConstraint));
}
else if (platformView is WrapperView wrapper)
{
sizeThatFits = wrapper.SizeThatFitsWrapper(new CGSize((float)widthConstraint, (float)heightConstraint), virtualView.Width, virtualView.Height);
}
else
{
sizeThatFits = platformView.SizeThatFits(new CGSize((float)widthConstraint, (float)heightConstraint));
}

var size = new Size(
sizeThatFits.Width == float.PositiveInfinity ? double.PositiveInfinity : sizeThatFits.Width,
Expand Down
35 changes: 34 additions & 1 deletion src/Core/src/Platform/iOS/ImageViewExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CoreGraphics;
using ObjCRuntime;
using UIKit;

Expand All @@ -17,7 +18,7 @@ public static void Clear(this UIImageView imageView)
public static void UpdateAspect(this UIImageView imageView, IImage image)
{
imageView.ContentMode = image.Aspect.ToUIViewContentMode();
imageView.ClipsToBounds = imageView.ContentMode == UIViewContentMode.ScaleAspectFill;
imageView.ClipsToBounds = imageView.ContentMode == UIViewContentMode.ScaleAspectFill || imageView.ContentMode == UIViewContentMode.Center;
}

public static void UpdateIsAnimationPlaying(this UIImageView imageView, IImageSourcePart image)
Expand Down Expand Up @@ -54,5 +55,37 @@ public static void UpdateSource(this UIImageView imageView, UIImage? uIImage, II
imageView.Image = uiImage;
}, scale, cancellationToken);
}

/// <summary>
/// Gets the size that fits on the screen for a <see cref="UIImageView"/> to be consistent cross-platform.
/// </summary>
/// <remarks>The default iOS implementation of SizeThatFits only returns the image's dimensions and ignores the constraints.</remarks>
/// <param name="imageView">The <see cref="UIImageView"/> to be measured.</param>
/// <param name="constraints">The specified size constraints.</param>
/// <returns>The size where the image would fit depending on the aspect ratio.</returns>
internal static CGSize SizeThatFitsImage(this UIImageView imageView, CGSize constraints)
{
// If there's no image, we don't need to take up any space
if (imageView.Image is null)
return new CGSize(0, 0);

var heightConstraint = constraints.Height;
var widthConstraint = constraints.Width;
var imageSize = imageView.Image.Size;

var widthRatio = Math.Min(imageSize.Width, widthConstraint) / imageSize.Width;
var heightRatio = Math.Min(imageSize.Height, heightConstraint) / imageSize.Height;

// In cases where we the image must fit its given constraints, we must shrink based on the smallest dimension (scale factor)
// that can fit it
if(imageView.ContentMode == UIViewContentMode.ScaleAspectFit)
{
var scaleFactor = Math.Min(widthRatio, heightRatio);
return new CGSize(imageSize.Width * scaleFactor, imageSize.Height * scaleFactor);
}

// Cases where AspectMode is ScaleToFill or Center
return constraints;
}
}
}
29 changes: 29 additions & 0 deletions src/Core/src/Platform/iOS/WrapperView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using CoreGraphics;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Platform;
using static Microsoft.Maui.Primitives.Dimension;
using UIKit;

namespace Microsoft.Maui.Platform
Expand Down Expand Up @@ -119,9 +120,37 @@ public override CGSize SizeThatFits(CGSize size)

var child = Subviews[0];

// Calling SizeThatFits on an ImageView always returns the image's dimensions, so we need to call the extension method
// This also affects ImageButtons
if (child is UIImageView imageView)
{
return imageView.SizeThatFitsImage(size);
}
else if (child is UIButton imageButton && imageButton.ImageView?.Image is not null)
{
return imageButton.ImageView.SizeThatFitsImage(size);
}

return child.SizeThatFits(size);
}

internal CGSize SizeThatFitsWrapper(CGSize originalSpec, double virtualViewWidth, double virtualViewHeight)
{
if (Subviews.Length == 0)
return base.SizeThatFits(originalSpec);

var child = Subviews[0];

if (child is UIImageView || (child is UIButton imageButton && imageButton.ImageView?.Image is not null))
{
var widthConstraint = IsExplicitSet(virtualViewWidth) ? virtualViewWidth : originalSpec.Width;
var heightConstraint = IsExplicitSet(virtualViewHeight) ? virtualViewHeight : originalSpec.Height;
return SizeThatFits(new CGSize(widthConstraint, heightConstraint));
}

return SizeThatFits(originalSpec);
}

public override void SetNeedsLayout()
{
base.SetNeedsLayout();
Expand Down