.NET MAUI Fixing Corner Artifacts on Android Cards
If you’ve ever built custom rounded card views on Android, you’ve probably met this bug: a faint pixel-gap right at the corners, or a stale border color that doesn’t update when the user flips between light and dark theme. It looks fine in your design tool. It looks fine on iOS. But on Android, not so much. There’s a tiny artifact that somehow makes everything feel unpolished.
Our Card control is a core UI building block used throughout the app — lists, settings panels, device controls. On Android it’s backed by two layered drawables: a MaterialShapeDrawable for the filled background with rounded corners, and a GradientDrawable on the foreground for the highlight stroke. In theory they sit perfectly on top of each other. In practice, Android’s rounding math leaves a sub-pixel gap where the two layers meet at the corners. That gap shows up as a faint artifact, especially noticeable on highlighted cards or when switching themes.
First commit: Cover the gap with an InsetDrawable
The foreground stroke was previously drawn with a slightly smaller corner radius than the background, trying to account for the border width. That left a visible gap. The fix wraps the stroke in an InsetDrawable with a -1dp inset instead, pushing it outward to cover the gap:
var stroke = new GradientDrawable();
stroke.SetColor(Color.Transparent);
stroke.SetCornerRadius(Card.CardCornerRadius.FromDip()); // same radius as background now
// -1dp inset shifts the drawable outward, covering the rounding gap
var insetStroke = new InsetDrawable(stroke, -1.FromDip());
cardView.Foreground = insetStroke;Also added SetClipChildren(true) so child views don’t bleed outside the rounded corners.
Second commit: Update colors on theme switch
Cards implement the IThemeSwitchableView interface on Android. When the theme changes, OnSwitchTheme fires OnPropertyChanged, which triggers the handler mapper:
// Card.cs (Android only)
public void OnSwitchTheme(AppTheme theme) => OnPropertyChanged(nameof(OnSwitchTheme));
// CardHandler property mapper
[nameof(Card.OnSwitchTheme)] = MapOnThemeChanged,MapOnThemeChanged calls the new shared UpdateCardAppearance with updateFillColor: true, which refreshes the MaterialShapeDrawable fill to match the current theme:
private static void UpdateCardAppearance(CardHandler handler, Card card, bool updateFillColor = false)
{
if (updateFillColor && handler.PlatformView.Background is MaterialShapeDrawable background)
{
background.FillColor = ColorStateList.ValueOf(PlejdColors.BaseSurface.ToPlatform());
}
// ... also updates stroke colors
}Third commit: Two drawables, two strokes
The original code only set the stroke on the foreground GradientDrawable. The refactor adds a matching stroke to the background MaterialShapeDrawable as well, so both layers highlight/unhighlight consistently:
private static void SetForegroundStroke(GradientDrawable foreground, bool highlighted)
{
foreground.SetStroke(
width: highlighted ? 2.FromDip() : 0,
highlighted ? PlejdColors.ElementDetail.ToPlatform() : Color.Transparent);
}
private static void SetBackgroundStroke(MaterialShapeDrawable background, bool highlighted)
{
background.SetStroke(
strokeWidth: highlighted ? 2.FromDip() : 0,
ColorStateList.ValueOf(highlighted ? PlejdColors.ElementDetail.ToPlatform() : Color.Transparent));
}Fourth commit: Guard unnecessary invalidations
Invalidate() triggers a redraw. A small guard was added so it only fires when something actually changed — avoiding redundant redraws on views that don’t match either drawable type:
var viewChanged = false;
if (...foreground matched...) { SetForegroundStroke(...); viewChanged = true; }
if (...background matched...) { SetBackgroundStroke(...); viewChanged = true; }
if (viewChanged) handler.PlatformView.Invalidate();TL;DR: Android’s MaterialShapeDrawable + GradientDrawable layering leaves a sub-pixel gap at rounded corners. An InsetDrawable wrapper covers it, and hooking into IThemeSwitchableView ensures both stroke layers re-color correctly when the user switches themes.
Comments
Last modified on 2025-10-03
