-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[Android] SwipeItem ignores FontImageSource rendered size and always scales icons to container height, unlike iOS #34210
Description
GitHub Issue for dotnet/maui - REVISED
Title
[Android] SwipeItem ignores FontImageSource rendered size and always scales icons to container height, unlike iOS
Labels
platform/androidt/bugarea-controls-swipeviewpartner/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
- Create a SwipeView with SwipeItem using FontImageSource
- Set FontImageSource.Size to a small value (e.g., 20)
- Run on both iOS and Android
- 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)
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)
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):
- Reduce FontImageSource.Size (10→16→14→10) - Ignored by Android
- Reduce SwipeView content height - Makes entire card cramped
- Custom Android handler - Requires duplicating private MAUI code
- 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
- Related to issue [Android] SwipeItem IconImageSource should allow more configuration #23074 (color/tint configuration) but different - this is about size/scaling behavior
- Real-world production app: ALPA Mobile (airline pilot scheduling app)
- Affects critical user workflows (swipe-to-favorite, swipe-to-notify)
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

