Iris Classon
Iris Classon - In Love with Code

MAUI ImageButton AspectFit and the Missing Image

MAUI ImageButton AspectFit and the Missing Image
I’m writing a longer article about button bugs in MAUI and came across a discussion I started in the MAUI repository regarding the ImageButton element. (I also asked this question on StackOverflow), and managed to ask another unanswered question. Seems to be my thing haha.

<?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="MauiButtons.MainPage">
            <ImageButton Source="cup.jpeg"
                         BackgroundColor="Yellow"
                         HeightRequest="100"
                         Aspect="AspectFit"
                         Clicked="ImageButton_OnClicked"
                         HorizontalOptions="Start"
                         VerticalOptions="Start">
            </ImageButton>
</ContentPage>

I had come across an issue with aspect ratio sizing in MAUI. In contrast to Xamarin, on iOS, the Aspect property didn’t seem to have the same behavior as on Android. When the Aspect property was set to AspectFit with HeightRequest set to, for example 80, the WidthRequest would not be set and remain -1. The frame property on the ImageButton would however be the original width of the image, while on Android it would be the Width that retains the aspect ratio based on the new height. This means, that with a large image, the Frame would be so wide that the image on the button would be pushed out of view.

Sidenote: The Frame property of a visual element like ImageButton refers to the position and size of the element within its parent container.

Our Image elements also had a similar issue, and some of our custom button implementations inherited and relied on the Image element. Due to the dynamic behavior of the app we wouldn’t know the both height and width, and relied heavily on aspect ratio in Xamarin. We sorted this out with a custom handler, such as the one below. Of course, this is a simplified version of what you’ll end up with, so customize it to fit your requirements.

Handler for the Image Aspect property:

#if ANDROID
        Microsoft.Maui.Handlers.ImageHandler.Mapper.AppendToMapping("SetAspectSize", (handler, view) =>
        {
            if (handler.PlatformView.Drawable is { IntrinsicHeight: > 0, IntrinsicWidth: > 0 } originalImage
                && view is Image { Aspect: Aspect.AspectFit } image)
            {
                var aspectRatio = (double)originalImage.IntrinsicWidth / (double)originalImage.IntrinsicHeight;
                if (double.IsNaN(aspectRatio) || double.IsInfinity(aspectRatio))
                {
                    // Error handling code.
                    return;
                }
        
                switch (image.WidthRequest)
                {
                    case > 0 when image.HeightRequest < 0:
                        image.HeightRequest = image.WidthRequest / aspectRatio;
                        break;
                    case < 0 when image.HeightRequest > 0:
                        image.WidthRequest = image.HeightRequest * aspectRatio;
                        break;
                }
            }
        });
#endif

As it has been a few releases since we had that problem I wanted to see if the problem remained. I was pleased to see that Image Aspect was correctly handled, but the ImageButton still had some strange behavior. Now, I know some people are going to argue its by design and that MAUI layout are different – and they are- but I don’t think it’s unreasonable to expect the element to render the same on MAUI and iOS. I can’t say why it differs, but I will take a deep dive when I have time, and if it is a bug (it could be intended behavior), then I’ll submit an issue.

Here is the handler for the ImageButton. Notice how we need to reload the resources. Otherwise the Drawable will be null. The await means we have an async void, which in turns means we wont bubble up exceptions. I don’t like it, and I’d avoid using a this if possible (we made our own ImageButtons by inheriting from Image), but if this is your only solution, make sure to catch!

ImageButton Aspect handler:

#if ANDROID
        Microsoft.Maui.Handlers.ImageButtonHandler.Mapper.AppendToMapping("SetAspectSize", async (handler, view) =>
        {
            try
            {
                await handler.SourceLoader.UpdateImageSourceAsync();
        
                if (handler.PlatformView.Drawable is { IntrinsicHeight: > 0, IntrinsicWidth: > 0 } originalImage
                    && view is ImageButton { Aspect: Aspect.AspectFit } image)
                {
                    var aspectRatio = (double)originalImage.IntrinsicWidth / (double)originalImage.IntrinsicHeight;
                    if (double.IsNaN(aspectRatio) || double.IsInfinity(aspectRatio))
                    {
                        // Error handling code.
                        return;
                    }
        
                    switch (image.WidthRequest)
                    {
                        case > 0 when image.HeightRequest < 0:
                            image.HeightRequest = image.WidthRequest / aspectRatio;
                            break;
                        case < 0 when image.HeightRequest > 0:
                            image.WidthRequest = image.HeightRequest * aspectRatio;
                            break;
                    }
                }
            }
            catch (Exception e)
            {
                // Do something here.
            }
        });
#endif

Potential problems with the code:

  • async void
  • Height and Width might be changed after the if() and we could end up with a divide by zero etc.
  • Thread safety issues
  • …and probably more things I missed

Anyway, as always, hope this helps.

Comments

Leave a comment below, or by email.

Last modified on 2024-02-27

comments powered by Disqus