Skip to content

[Android] SwipeItem ignores FontImageSource rendered size and always scales icons to container height, unlike iOS #34210

@Quackstro

Description

@Quackstro

GitHub Issue for dotnet/maui - REVISED

Title

[Android] SwipeItem ignores FontImageSource rendered size and always scales icons to container height, unlike iOS

Labels

  • platform/android
  • t/bug
  • area-controls-swipeview
  • partner/alpa

Description

On Android, SwipeItem icons are rendered significantly larger than on iOS when using FontImageSource, despite identical code. Android ignores the rendered image size and always scales icons to half the container height, while iOS respects the source image dimensions and only scales down proportionally when needed.

This creates severe platform inconsistency and makes it impossible to control icon sizes on Android using FontImageSource.Size.


Steps to Reproduce

  1. Create a SwipeView with SwipeItem using FontImageSource
  2. Set FontImageSource.Size to a small value (e.g., 20)
  3. Run on both iOS and Android
  4. Observe icons render 3-4x larger on Android

XAML (JumpseatFlightFinderResultsPage.xaml)

<CollectionView ItemsSource="{Binding FlightSearchResults}">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="flightSearch:Flight">
            <StackLayout BackgroundColor="Transparent" Padding="0,7,0,7">
                <SwipeView>
                    <SwipeView.RightItems>
                        <SwipeItems>
                            <SwipeItem
                                IconImageSource="{Binding FavoritesIcon}"
                                BackgroundColor="#0c2344"
                                Command="{Binding ToggleFavoriteCommand}"
                                CommandParameter="{Binding .}"/>
                            <SwipeItem 
                                IconImageSource="{Binding NotifyMeIcon}"
                                BackgroundColor="#053C89"
                                Command="{Binding NotifyMeCommand}"
                                CommandParameter="{Binding .}"/>
                        </SwipeItems>
                    </SwipeView.RightItems>
                    <!-- SwipeView content: flight card layout -->
                    <StackLayout>
                        <!-- Flight details -->
                    </StackLayout>
                </SwipeView>
            </StackLayout>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

C# Code-Behind (Flight.cs - View Model)

public ImageSource FavoritesIcon
{
    get
    {
        const int iconSize = 20;  // ← Small icon size specified
        
        if (IsFavorite)
            return new FontImageSource()
            {
                FontFamily = "FontAwesome",
                Glyph = "\uf5c0",  // Star half-stroke icon
                Size = iconSize,   // ← This is effectively ignored on Android!
                Color = Colors.White
            };
        else
            return new FontImageSource()
            {
                FontFamily = "FontAwesome",
                Glyph = "\uf005",  // Star icon
                Size = iconSize,   // ← This is effectively ignored on Android!
                Color = Colors.White
            };
    }
}

public ImageSource NotifyMeIcon
{
    get
    {
        const int iconSize = 20;  // ← Small icon size specified
        
        if (IsNotifyMe)
            return new FontImageSource()
            {
                FontFamily = "FontAwesome",
                Glyph = "\uf1f6",  // Bell slash
                Size = iconSize,   // ← This is effectively ignored on Android!
                Color = Colors.White
            };
        else
            return new FontImageSource()
            {
                FontFamily = "FontAwesome",
                Glyph = "\uf0f3",  // Bell
                Size = iconSize,   // ← This is effectively ignored on Android!
                Color = Colors.White
            };
    }
}

Expected Behavior

Both platforms: Icons render at approximately the specified Size (20px), consistent between iOS and Android.


Actual Behavior

iOS ✅

Icons render small, approximately at the specified Size=20. iOS uses proportional scaling and respects the source image dimensions.

Android ❌

Icons render much larger (3-4x), approximately 50-60px, completely ignoring the FontImageSource.Size property.

Calculation:

  • SwipeView content height: ~100px
  • Android icon size: 100 / 2 = 50px (regardless of FontImageSource.Size=20)

Visual Comparison

iOS (correctly respects source image size)

iOS Screenshot

Notice: Bell 🔔 and star ⭐ icons in the blue swipe area are small and properly sized (~20px as specified).


Android (ignores source size and scales to container)

Android Screenshot

Notice: Same bell and star icons are approximately 3-4x larger (~50-60px), despite identical FontImageSource.Size=20 specification in code.


Both screenshots use identical XAML and C# code. The only difference is the platform, demonstrating that Android completely ignores the source image size.


Root Cause Analysis

Android Implementation (SwipeItemMenuItemHandler.Android.cs)

Lines 102-107 - GetIconSize():

static int GetIconSize(ISwipeItemMenuItemHandler handler)
{
    if (handler.VirtualView is not IImageSourcePart imageSourcePart || imageSourcePart.Source is null)
        return 0;

    var mauiSwipeView = handler.PlatformView.Parent.GetParentOfType<MauiSwipeView>();

    if (mauiSwipeView is null || handler.MauiContext?.Context is null)
        return 0;

    int contentHeight = mauiSwipeView.MeasuredHeight;
    int contentWidth = (int)handler.MauiContext.Context.ToPixels(SwipeViewExtensions.SwipeItemWidth);

    return Math.Min(contentHeight, contentWidth) / 2;  // ← HARDCODED: Always half of container!
}

The FontImageSource.Size property is never consulted!

Lines 135-154 - SetImageSource():

public override void SetImageSource(Drawable? platformImage)
{
    if (Handler?.PlatformView is not TextView button || Handler?.VirtualView is not ISwipeItemMenuItem item)
        return;

    if (platformImage is not null)
    {
        var iconSize = GetIconSize(Handler);  // ← Uses hardcoded calculation above
        var textColor = item.GetTextColor()?.ToPlatform();
        int drawableWidth = platformImage.IntrinsicWidth;   // ← Source size retrieved...
        int drawableHeight = platformImage.IntrinsicHeight; // ← ...but then ignored!

        // Aspect ratio calculation
        if (drawableWidth > drawableHeight)
        {
            var iconWidth = iconSize;  // ← Uses calculated size, NOT source size!
            var iconHeight = drawableHeight * iconWidth / drawableWidth;
            platformImage.SetBounds(0, 0, iconWidth, iconHeight);
        }
        else
        {
            var iconHeight = iconSize;  // ← Uses calculated size, NOT source size!
            var iconWidth = drawableWidth * iconHeight / drawableHeight;
            platformImage.SetBounds(0, 0, iconWidth, iconHeight);
        }

        if (textColor != null)
            platformImage.SetColorFilter(textColor.Value, FilterMode.SrcAtop);
    }

    button.SetCompoundDrawables(null, platformImage, null, null);
}

The source image dimensions (IntrinsicWidth/IntrinsicHeight) are retrieved but only used for aspect ratio calculation, not for final size. The icon is always scaled to the hardcoded iconSize value.


iOS Implementation (SwipeItemMenuItemHandler.iOS.cs)

Lines 95-120 - SetImageSource():

public override void SetImageSource(UIImage? platformImage)
{
    if (Handler?.PlatformView is not UIButton button || Handler?.VirtualView is not ISwipeItemMenuItem item)
        return;

    var frame = button.Frame;
    if (frame == CGRect.Empty)
        return;

    if (platformImage == null)
    {
        button.SetImage(null, UIControlState.Normal);
    }
    else
    {
        // ← KEY DIFFERENCE: Uses 50% of frame as MAXIMUM, not absolute size
        var maxWidth = frame.Width * 0.5f;
        var maxHeight = frame.Height * 0.5f;

        var resizedImage = MaxResizeSwipeItemIconImage(platformImage, maxWidth, maxHeight);
        
        button.SetImage(resizedImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate), UIControlState.Normal);
        var tintColor = item.GetTextColor();
        if (tintColor != null)
            button.TintColor = tintColor.ToPlatform();
    }
}

Lines 122-167 - MaxResizeSwipeItemIconImage():

static UIImage MaxResizeSwipeItemIconImage(UIImage sourceImage, nfloat maxWidth, nfloat maxHeight)
{
    var sourceSize = sourceImage.Size;  // ← Uses actual source size!
    var maxResizeFactor = Math.Min(maxWidth / sourceSize.Width, maxHeight / sourceSize.Height);

    if (maxResizeFactor > 1)
    {
        return sourceImage;  // ← KEY: If source is smaller than max, DON'T SCALE UP!
    }

    // Only scales DOWN if source is larger than max
    var width = maxResizeFactor * sourceSize.Width;
    var height = maxResizeFactor * sourceSize.Height;

    // Creates resized UIImage using Core Graphics...
    // (rendering code omitted for brevity)
}

iOS respects the source image size and only scales DOWN when needed. It never scales up small images!


Platform Comparison

Aspect iOS ✅ Android ❌
Size Calculation min(frame * 0.5, sourceSize) contentHeight / 2 (fixed)
Respects Source Size? ✅ Yes (as maximum constraint) ❌ No (completely ignored)
Scaling Behavior Only scales DOWN, never UP Always scales to calculated size
Result with Size=20 ~20px (respects small size) ~50px (ignores specified size)

Impact

Severity: Medium-High

Affected apps: Any app using SwipeView with FontImageSource icons on Android

Issues:

  • ❌ Severe platform inconsistency (Android ≠ iOS)
  • ❌ No way to control icon size on Android without workarounds
  • ❌ Icons appear unprofessional and oversized on Android
  • ❌ Degrades user experience

Attempted workarounds (all fail or have side effects):

  1. Reduce FontImageSource.Size (10→16→14→10) - Ignored by Android
  2. Reduce SwipeView content height - Makes entire card cramped
  3. Custom Android handler - Requires duplicating private MAUI code
  4. PNG assets instead of FontImageSource - Loses font rendering benefits, maintenance burden

Proposed Solution

Make Android behave like iOS - respect the source image dimensions and use container size as a maximum constraint, not an absolute size:

static int GetIconSize(ISwipeItemMenuItemHandler handler)
{
    if (handler.VirtualView is not IImageSourcePart imageSourcePart || imageSourcePart.Source is null)
        return 0;

    // NEW: Check if FontImageSource with explicit Size
    if (imageSourcePart.Source is FontImageSource fontSource && fontSource.Size > 0)
    {
        var context = handler.MauiContext?.Context;
        if (context != null)
        {
            // Convert logical size to pixels
            int pixelSize = (int)context.ToPixels(fontSource.Size);
            
            // Get container size as maximum constraint (like iOS)
            var mauiSwipeView = handler.PlatformView.Parent.GetParentOfType<MauiSwipeView>();
            if (mauiSwipeView != null)
            {
                int contentHeight = mauiSwipeView.MeasuredHeight;
                int maxIconSize = contentHeight / 2;
                
                // Like iOS: Use source size, but don't exceed container max
                return Math.Min(pixelSize, maxIconSize);
            }
            
            return pixelSize;
        }
    }

    // EXISTING: Fallback to calculated size for other image sources
    var mauiSwipeView2 = handler.PlatformView.Parent.GetParentOfType<MauiSwipeView>();
    if (mauiSwipeView2 is null || handler.MauiContext?.Context is null)
        return 0;

    int contentHeight2 = mauiSwipeView2.MeasuredHeight;
    int contentWidth = (int)handler.MauiContext.Context.ToPixels(SwipeViewExtensions.SwipeItemWidth);

    return Math.Min(contentHeight2, contentWidth) / 2;
}

This would:

  • ✅ Make Android behavior consistent with iOS
  • ✅ Respect the FontImageSource.Size property
  • ✅ Use container size as maximum constraint (like iOS)
  • ✅ Never scale up small icons (like iOS)
  • ✅ Maintain backward compatibility (fallback for non-FontImageSource)
  • ✅ Minimal code change (~15 lines)

Alternative Solutions Considered

1. Always respect source image IntrinsicWidth/IntrinsicHeight

Issue: Would affect all image types (PNG, FileImageSource, etc.), potentially breaking existing apps.

2. Add new SwipeItem.IconSize property

Issue: Adds API surface, more complex, doesn't solve existing code.

3. Match iOS proportional scaling exactly

Preferred: The proposed solution above matches iOS behavior closely.


Environment

  • .NET MAUI: 9.0.30 (issue exists in latest main branch source code)
  • Android: All versions
  • iOS: All versions (works correctly)
  • Affected platforms: Android only

Additional Context


Test Case

After fix, the following should produce consistent icon sizes on both platforms:

new FontImageSource() 
{ 
    FontFamily = "FontAwesome",
    Glyph = "\uf005",
    Size = 20,  // Should produce ~20px icons on BOTH platforms
    Color = Colors.White
}

Currently:

  • iOS: ~20px ✅
  • Android: ~50-60px ❌

Expected after fix:

  • iOS: ~20px ✅
  • Android: ~20px ✅

Attachments:

  • iOS screenshot: Shows properly sized icons (~20px)
  • Android screenshot: Shows oversized icons (~50-60px)
  • Both use identical code (FontImageSource.Size=20)

Reporter: ALPA Mobile Development Team
Date: 2026-02-24

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions