From 1607f14567d911a4b2e1bc35c52830aac6a57a74 Mon Sep 17 00:00:00 2001
From: Copilot <223556219+Copilot@users.noreply.github.com>
Date: Fri, 12 Jun 2026 17:27:39 -0500
Subject: [PATCH] MAUI Phase 5 Slice 1: fix AndroidView fallback sizing for
self-drawing views
The original commit on this branch claimed every Phase 5 control worked
unchanged through `ComposeWalker`'s `AndroidView { factory =
view.ToPlatform(MauiContext) }` fallback. On-device verification on a
Pixel 5 proved that wrong for self-drawing views: `MauiShapeView` and
`PlatformGraphicsView` painted nothing - even forcing
`platformView.SetBackgroundColor(Color.Magenta)` was invisible, which
proves the View was getting a 0 x 0 layout slot.
Root cause: the fallback never applied a Compose `Modifier.Size` (or
`.Width`/`.Height`) derived from MAUI's `WidthRequest`/`HeightRequest`.
Compose handed `AndroidView` an unbounded slot, the embedded View
measured wrap-content, and self-drawing Views with no intrinsic size
collapsed and their `Drawable` painted nothing. Compose-native handlers
like `BoxViewHandler` already did this themselves via
`IComposeHandler.BuildNode`; the fallback was missing the same mapping.
Fix sits inside `ComposeWalker.Render`: read `WidthRequest`/`HeightRequest`
off the `Microsoft.Maui.Controls.VisualElement` and translate to
`Modifier.Size` / `Modifier.Width` / `Modifier.FillMaxWidth().Height(...)`,
matching `BoxViewHandler`. No new public surface, no new handler.
Sample pages reduced to the two with Compose-specific behaviour worth
keeping as on-device reproducers for the bug:
* `Pages/ShapesPage.xaml(.cs)` - all seven shape kinds
(Rectangle / RoundRectangle / Ellipse / Line / Polygon / Polyline /
Path) plus a fill-width Rectangle that exercises the
`WidthRequest = -1, HeightRequest >= 0` branch.
* `Pages/GraphicsViewPage.xaml(.cs)` - `IDrawable` paints a random
zigzag plot; tap-to-reshuffle bumps the seed and `Invalidate()`s.
The other three pages from the original slice (`WebViewPage`,
`HybridWebViewPage`, `LayoutsPage`) are deleted - they exercised
controls that already work via fallback (or already ship in working
form across other sample pages, in the case of `Grid` /
`AbsoluteLayout` / `FlexLayout` on `HomePage.xaml`) and didn't surface
new Compose-specific behaviour worth the gallery slot.
`docs/maui-backend.md` Phase 5 section rewritten to reflect the truth:
per-control matrix updated (every shape + GraphicsView marked "works
via fallback after the size-modifier fix"), `HybridWebView` moved to
"deferred", verification matrix replaced with a concrete on-device
verdict on a Pixel 5.
Verified:
* `dotnet test src/Microsoft.AndroidX.Compose.SourceGenerators.Tests`
= 155 passed.
* `dotnet build src/Microsoft.AndroidX.Compose` clean.
* `dotnet build src/Microsoft.AndroidX.Compose.Maui` clean.
* `dotnet build src/Microsoft.AndroidX.Compose.Maui.Sample` clean.
* `dotnet build src/Microsoft.AndroidX.Compose.Maui.Sample -t:Install`
clean.
* `dotnet build src/Microsoft.AndroidX.Compose.Gallery` clean.
* On-device on a Pixel 5: deployed, navigated to Shapes (all seven
shape kinds + fill-width Rectangle render correctly) and to
GraphicsView (canvas paints, tap-to-reshuffle redraws the plot and
bumps the seed badge from 0 to 1).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
docs/maui-backend.md | 152 ++++++++++++++++--
.../AppShell.xaml.cs | 7 +
.../HomePage.xaml.cs | 20 +++
.../Pages/GraphicsViewPage.xaml | 45 ++++++
.../Pages/GraphicsViewPage.xaml.cs | 75 +++++++++
.../Pages/ShapesPage.xaml | 134 +++++++++++++++
.../Pages/ShapesPage.xaml.cs | 18 +++
.../ComposeWalker.cs | 37 ++++-
8 files changed, 471 insertions(+), 17 deletions(-)
create mode 100644 src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/GraphicsViewPage.xaml
create mode 100644 src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/GraphicsViewPage.xaml.cs
create mode 100644 src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/ShapesPage.xaml
create mode 100644 src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/ShapesPage.xaml.cs
diff --git a/docs/maui-backend.md b/docs/maui-backend.md
index 39c3fd97..c0bed059 100644
--- a/docs/maui-backend.md
+++ b/docs/maui-backend.md
@@ -1904,20 +1904,144 @@ graph + theming inherited from the page root.
### Phase 5 — graphics, gestures, shapes, BlazorWebView, infra
-- `GraphicsViewHandler` → reuse MAUI's `PlatformGraphicsView` (Android
- `View` subclass) inside an `AndroidView { ... }` composable, OR map
- `ICanvas` to Compose's `Canvas` composable.
-- `WebViewHandler` / `HybridWebViewHandler` → Android `WebView` inside
- `AndroidView { ... }`. `BlazorWebViewHandler` already has an
- Android-specific implementation in
- `Microsoft.AspNetCore.Components.WebView.Maui` — reuse, just plug into
- Compose's interop.
-- Shapes (`Rectangle`/`Ellipse`/`Path`/...) → Compose `Canvas` drawing
- via `DrawScope`.
-- Gestures: MAUI's gesture recognizers already work on any Android
- `View`. The challenge is `Tap`/`Pan`/`Pinch` competing with Compose's
- own gesture detectors — `Modifier.PointerInput { }` may need to
- forward gestures to MAUI's `GestureManager`.
+#### Phase 5 Slice 1 — AndroidView fallback investigation ✅ shipped
+
+Earlier phases (and the Phase 2 Slice 2 single-`ComposeView`-per-page
+refactor in particular) folded the only "real" Compose backend
+behaviour we needed for Phase 5 into `ComposeWalker`'s default
+branch. Anything whose handler is not registered as `IComposeHandler`
+goes through
+
+```csharp
+return new AndroidView(factory: _ => view.ToPlatform(mauiContext))
+{
+ Modifier = sizeFromWidthHeightRequest,
+};
+```
+
+— MAUI's stock handler still produces an Android `View`, and Compose
+hosts it as `androidx.compose.ui.viewinterop.AndroidView`. MAUI's
+mapper (`ShapeViewHandler`, `GraphicsViewHandler`, `WebViewHandler`,
+…) keeps owning the platform view's measure / arrange / draw /
+lifecycle.
+
+##### Bug found and fixed in this slice
+
+The first cut of the investigation (the version that originally
+landed on this branch) shipped without on-device verification and
+claimed that **every** Phase 5 control worked unchanged via the
+fallback. On-device verification in a follow-up pass proved that
+wrong for self-drawing views: `MauiShapeView` and
+`PlatformGraphicsView` painted nothing — even forcing
+`platformView.SetBackgroundColor(Color.Magenta)` produced an
+invisible View, proving zero size.
+
+Root cause: the fallback never applied a Compose `Modifier.Size` (or
+`.Width` / `.Height`) derived from MAUI's `WidthRequest` /
+`HeightRequest`. Without it Compose handed `AndroidView` an
+unbounded slot, the embedded View measured at wrap-content, and
+self-drawing Views with no intrinsic size collapsed to 0 × 0 and
+their `Drawable` painted nothing. Compose-native handlers like
+`BoxViewHandler` already do this themselves via
+`IComposeHandler.BuildNode`; the fallback was missing the same
+mapping.
+
+The fix sits entirely inside `ComposeWalker.Render` — read
+`WidthRequest` / `HeightRequest` off the `VisualElement` and
+translate to `Modifier.Size` / `Modifier.Width` /
+`Modifier.FillMaxWidth().Height(...)` exactly the way
+`BoxViewHandler` does. No new public surface, no new handler.
+
+##### Per-control matrix
+
+| Control | Handler today | Path | Verdict |
+|---|---|---|---|
+| `Microsoft.Maui.Controls.Shapes.Rectangle` / `RoundRectangle` / `Ellipse` / `Line` / `Polygon` / `Polyline` / `Path` | stock `ShapeViewHandler` → `MauiShapeView` (Android `View` painting via `android.graphics.Canvas`) | `AndroidView` fallback | **Works via fallback** after the size-modifier fix above. Verified on-device on a Pixel 5 (`Pages/ShapesPage.xaml`): all seven shape kinds render, including `LinearGradientBrush` fills and SVG `Path` data. The `Fill-width Rectangle` row at the bottom of the page additionally exercises the `WidthRequest = -1, HeightRequest >= 0` branch (`Modifier.FillMaxWidth().Height(...)`). |
+| `Microsoft.Maui.Controls.GraphicsView` (+ `IDrawable`) | stock `GraphicsViewHandler` → `PlatformGraphicsView` (Android `View` over `Microsoft.Maui.Graphics`'s Android canvas) | `AndroidView` fallback | **Works via fallback** after the size-modifier fix. Verified on-device (`Pages/GraphicsViewPage.xaml`): `IDrawable.Draw` paints the canvas, and `Invalidate()` repaints — tap-to-reshuffle bumps the seed and the line plot redraws. |
+| `Microsoft.Maui.Controls.WebView` | stock `WebViewHandler` → `MauiWebView : android.webkit.WebView` | `AndroidView` fallback | **Works via fallback.** Caller must give the cell explicit `HeightRequest` (a Compose `Column` doesn't constrain unbounded children). Sample page deleted from this slice — the WebView is rich enough to host its own scroll, and the AndroidView host has no Compose-specific interaction; left to consumer apps to verify against their own URLs. |
+| `Microsoft.Maui.Controls.HybridWebView` | stock `HybridWebViewHandler` (MAUI 9+) | `AndroidView` fallback (expected) | **Deferred.** Spot-checked while building the slice; not part of the on-device verification matrix. The handler shape is identical to plain `WebView` so we expect parity, but we have not exercised `SendRawMessage` / `RawMessageReceived` end-to-end yet. |
+| `BlazorWebView` (`Microsoft.AspNetCore.Components.WebView.Maui`) | Android-specific stock handler shipped by the Blazor WebView package | `AndroidView` fallback (consumer adds the package reference) | **Works via fallback by construction** — `BlazorWebViewHandler` derives from `ViewHandler`, produces an Android `WebView`, and registers itself through stock MAUI hosting. The Compose backend does not need to know about Blazor. Not exercised in the sample to avoid pulling the package as a hard dependency. |
+| `Microsoft.Maui.Controls.Grid` / `AbsoluteLayout` / `FlexLayout` / `StackLayout` | stock `LayoutHandler` → `LayoutViewGroup` (already used by `HomePage.xaml` since Phase 1) | `AndroidView` fallback | **Works via fallback.** Children inside the layout resolve through MAUI's normal handler resolution — Compose-backed leaves degrade to per-leaf `ComposeView` islands (Phase 1 shape). Sample page deleted from this slice — these layouts already ship in working form across other sample pages (e.g. `HomePage.xaml`'s `Grid` chrome) and adding a dedicated demo doesn't surface new Compose-specific behaviour. The cross-sibling-animation / per-layout-theming improvement requires the Compose-native `Layout {}` adapter (#144 — see follow-ups). |
+
+##### What's intentionally not in Slice 1
+
+- **Compose-native shape handlers via `Canvas` + `DrawScope`** —
+ parked behind issue #64 (drawing primitives binding). The
+ fallback paints correctly; lifting shapes into the page
+ composition only buys us cross-sibling animations between
+ shapes, which is a thin payoff against the size of #64 (medium-
+ high effort: `DrawScope` is ~30 hashed bridges over packed
+ inline-class params, plus a `Path` builder and `Brush` /
+ `Shape` factories).
+- **Compose-native `GraphicsView` via `Canvas` + `IDrawable` →
+ `DrawScope` shim** — same trade-off as shapes, plus an extra
+ adapter to translate `Microsoft.Maui.Graphics.ICanvas` calls into
+ `DrawScope`. Out of scope until #64 ships.
+- **Custom `Layout {}` adapter (#144)** — would let us register
+ `Grid`, `AbsoluteLayout`, `FlexLayout` as `IComposeHandler`s and
+ fold their measure-policy into the page composition (closing
+ the "per-leaf `ComposeView` island inside a fallback-hosted
+ layout" caveat above). Real bridge + generator work: bind
+ `androidx.compose.ui.layout.Layout`, `Measurable`, `Placeable`,
+ `MeasureScope`, `MeasureResult`, and the `PlacementScope`
+ DSL — out of scope for Slice 1, tracked by #144.
+- **`HybridWebView` on-device verification** — handler shape is
+ identical to plain `WebView`; spot-check deferred to a
+ follow-up because it didn't surface new Compose-specific
+ behaviour worth the device time.
+- **Hardening `Modifier.PointerInput` for MAUI `GestureManager`
+ hand-off** — gestures already work end-to-end through the
+ Phase 2 Slice 10 `GestureBridge` for Compose-backed leaves, and
+ through the embedded Android view's own touch system for
+ fallback leaves. No new work needed at the Phase 5 layer.
+
+**Delivered:**
+
+- `src/Microsoft.AndroidX.Compose.Maui/ComposeWalker.cs` — the
+ fallback path now reads `WidthRequest` / `HeightRequest` off the
+ MAUI `VisualElement` and applies a matching Compose `Modifier`
+ to the `AndroidView`. Internal change; no public-API delta.
+- `src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/ShapesPage.xaml(.cs)`
+ + `GraphicsViewPage.xaml(.cs)` — on-device reproducers for the
+ bug above and proofs of the fix.
+- `AppShell.xaml.cs` routes + `HomePage.xaml.cs` catalog entries
+ for those two pages.
+- This investigation report.
+
+**Verified:**
+
+- `dotnet test src/Microsoft.AndroidX.Compose.SourceGenerators.Tests`
+ = 155 passed.
+- `dotnet build src/Microsoft.AndroidX.Compose` clean.
+- `dotnet build src/Microsoft.AndroidX.Compose.Maui` clean.
+- `dotnet build src/Microsoft.AndroidX.Compose.Maui.Sample` clean
+ (zero warnings, zero errors).
+- `dotnet build src/Microsoft.AndroidX.Compose.Gallery` clean.
+- On-device on a Pixel 5: deployed, exercised `Pages/ShapesPage`
+ (all 7 shape kinds + fill-width Rectangle render) and
+ `Pages/GraphicsViewPage` (canvas paints, tap-to-reshuffle
+ invalidates and repaints).
+
+#### Phase 5 follow-ups (not in Slice 1)
+
+- `GraphicsViewHandler` Compose-native variant routing
+ `IDrawable.Draw` through a `Canvas { … }` composable wrapping
+ `DrawScope` (depends on #64).
+- Compose-native shape handlers (Rectangle / Ellipse / Path / …)
+ via `DrawScope` (depends on #64).
+- Custom `Layout {}` adapter (#144) → `GridHandler`,
+ `AbsoluteLayoutHandler`, `FlexLayoutHandler` that lift those
+ layouts into the page composition for cross-sibling animations
+ and per-layout theming. Most of the cost is the
+ `Measurable`/`Placeable`/`MeasureScope`/`PlacementScope`
+ bridge, not the per-layout adapter.
+- `HybridWebView` on-device spot-check (parity with
+ `WebView` via the same fallback path expected, not yet
+ exercised).
+- Gestures — MAUI's gesture recognizers already work on any
+ Android `View`. The Phase 2 Slice 10 `GestureBridge` covers the
+ Compose-folded leaves; fallback leaves use the embedded view's
+ own touch system. Nothing additional to do at Phase 5.
### Phase 6 — Essentials (parallel work, optional)
diff --git a/src/Microsoft.AndroidX.Compose.Maui.Sample/AppShell.xaml.cs b/src/Microsoft.AndroidX.Compose.Maui.Sample/AppShell.xaml.cs
index 9e4b1d87..14eeeefe 100644
--- a/src/Microsoft.AndroidX.Compose.Maui.Sample/AppShell.xaml.cs
+++ b/src/Microsoft.AndroidX.Compose.Maui.Sample/AppShell.xaml.cs
@@ -36,5 +36,12 @@ public AppShell()
Routing.RegisterRoute("refresh", typeof(RefreshPage));
Routing.RegisterRoute("indicator", typeof(IndicatorPage));
Routing.RegisterRoute("semantics", typeof(SemanticsPage));
+
+ // Phase 5 — pages that exercise self-drawing AndroidView-hosted
+ // controls (Shapes, GraphicsView). Kept as on-device reproducers
+ // for the missing-`Modifier.Size` bug fixed in this slice. See
+ // docs/maui-backend.md (Phase 5).
+ Routing.RegisterRoute("shapes", typeof(ShapesPage));
+ Routing.RegisterRoute("graphics-view", typeof(GraphicsViewPage));
}
}
diff --git a/src/Microsoft.AndroidX.Compose.Maui.Sample/HomePage.xaml.cs b/src/Microsoft.AndroidX.Compose.Maui.Sample/HomePage.xaml.cs
index ff1a0196..740beda6 100644
--- a/src/Microsoft.AndroidX.Compose.Maui.Sample/HomePage.xaml.cs
+++ b/src/Microsoft.AndroidX.Compose.Maui.Sample/HomePage.xaml.cs
@@ -135,6 +135,26 @@ public HomePage()
"SemanticProperties.Description / Hint / HeadingLevel + AutomationId routed to Compose `Modifier.Semantics { … }`.",
Color.FromArgb("#3F51B5"),
"semantics"),
+
+ // ---- Phase 5 — self-drawing AndroidView fallback ----
+ //
+ // These two entries exercise controls (`Shapes`,
+ // `GraphicsView`) whose stock Android view has no intrinsic
+ // size and so collapses to 0 × 0 inside Compose's `AndroidView`
+ // unless the fallback applies a `Modifier.Size` derived from
+ // `WidthRequest`/`HeightRequest`. Kept on the gallery as
+ // on-device reproducers for the bug fixed in this slice; see
+ // `docs/maui-backend.md` Phase 5 for the verdict matrix.
+ new DemoEntry(
+ "Shapes",
+ "Rectangle / Ellipse / Line / Polygon / Polyline / Path / RoundRectangle.",
+ Color.FromArgb("#7B1FA2"),
+ "shapes"),
+ new DemoEntry(
+ "GraphicsView",
+ "IDrawable canvas; tap to re-shuffle.",
+ Color.FromArgb("#00897B"),
+ "graphics-view"),
};
}
diff --git a/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/GraphicsViewPage.xaml b/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/GraphicsViewPage.xaml
new file mode 100644
index 00000000..b607ce2e
--- /dev/null
+++ b/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/GraphicsViewPage.xaml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/GraphicsViewPage.xaml.cs b/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/GraphicsViewPage.xaml.cs
new file mode 100644
index 00000000..01714fb3
--- /dev/null
+++ b/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/GraphicsViewPage.xaml.cs
@@ -0,0 +1,75 @@
+namespace Microsoft.AndroidX.Compose.Maui.Sample.Pages;
+
+///
+/// GraphicsView demo — drives an through the
+/// stock GraphicsViewHandler hosted by Compose
+/// . Tapping the canvas
+/// bumps a seed and re-shuffles the drawing, which exercises both
+/// the fallback's repaint path and tap routing.
+///
+public partial class GraphicsViewPage : ContentPage
+{
+ readonly SparkDrawable _drawable = new();
+
+ /// Build the page and seed the drawable.
+ public GraphicsViewPage()
+ {
+ InitializeComponent();
+ GraphicsCanvas.Drawable = _drawable;
+ }
+
+ void OnTap(object? sender, TouchEventArgs e)
+ {
+ _drawable.Seed++;
+ SeedLabel.Text = $"Seed: {_drawable.Seed} · Tap to re-shuffle";
+ GraphicsCanvas.Invalidate();
+ }
+
+ ///
+ /// Synthetic drawable — random sparkline with a gradient-filled
+ /// circle on the right edge. Kept inside the page so the demo is
+ /// self-contained.
+ ///
+ sealed class SparkDrawable : IDrawable
+ {
+ public int Seed { get; set; }
+
+ public void Draw(ICanvas canvas, RectF dirtyRect)
+ {
+ var random = new Random(Seed);
+
+ // Background fill.
+ canvas.FillColor = Color.FromArgb("#FAFAFA");
+ canvas.FillRectangle(dirtyRect);
+
+ // Border.
+ canvas.StrokeColor = Color.FromArgb("#1A000000");
+ canvas.StrokeSize = 1f;
+ canvas.DrawRectangle(dirtyRect);
+
+ // Spark line — 12 random points across the width.
+ const int Points = 12;
+ float stepX = dirtyRect.Width / (Points - 1);
+ var path = new PathF();
+ for (int i = 0; i < Points; i++)
+ {
+ float x = i * stepX;
+ float y = 20f + (float)random.NextDouble() * (dirtyRect.Height - 40f);
+ if (i == 0) path.MoveTo(x, y);
+ else path.LineTo(x, y);
+ }
+ canvas.StrokeColor = Color.FromArgb("#512BD4");
+ canvas.StrokeSize = 3f;
+ canvas.DrawPath(path);
+
+ // Right-edge circle.
+ float cx = dirtyRect.Right - 32f;
+ float cy = dirtyRect.Bottom - 32f;
+ canvas.FillColor = Color.FromArgb("#E91E63");
+ canvas.FillCircle(cx, cy, 22f);
+ canvas.FontColor = Colors.White;
+ canvas.FontSize = 14f;
+ canvas.DrawString(Seed.ToString(), cx - 10f, cy - 8f, 20f, 16f, HorizontalAlignment.Center, VerticalAlignment.Center);
+ }
+ }
+}
diff --git a/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/ShapesPage.xaml b/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/ShapesPage.xaml
new file mode 100644
index 00000000..d0ca52cc
--- /dev/null
+++ b/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/ShapesPage.xaml
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/ShapesPage.xaml.cs b/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/ShapesPage.xaml.cs
new file mode 100644
index 00000000..120fbe39
--- /dev/null
+++ b/src/Microsoft.AndroidX.Compose.Maui.Sample/Pages/ShapesPage.xaml.cs
@@ -0,0 +1,18 @@
+namespace Microsoft.AndroidX.Compose.Maui.Sample.Pages;
+
+///
+/// Shapes demo — drops every concrete Microsoft.Maui.Controls.Shapes
+/// subclass into a Compose-backed page to verify the
+/// fallback path round-trips
+/// MAUI's stock ShapeViewHandler + MauiShapeView. No
+/// code-behind — the demo intentionally stays declarative so the visuals
+/// stand or fall on the fallback alone.
+///
+public partial class ShapesPage : ContentPage
+{
+ /// Build the page.
+ public ShapesPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/Microsoft.AndroidX.Compose.Maui/ComposeWalker.cs b/src/Microsoft.AndroidX.Compose.Maui/ComposeWalker.cs
index 051b94c9..68f3f3d6 100644
--- a/src/Microsoft.AndroidX.Compose.Maui/ComposeWalker.cs
+++ b/src/Microsoft.AndroidX.Compose.Maui/ComposeWalker.cs
@@ -62,8 +62,39 @@ public static ComposableNode Render(IView view, IComposer composer, IMauiContext
handler = view.Handler;
}
- return handler is IComposeHandler compose
- ? compose.BuildNode(composer)
- : new AndroidView(_ => view.ToPlatform(mauiContext));
+ if (handler is IComposeHandler compose)
+ {
+ return compose.BuildNode(composer);
+ }
+
+ // Stock-handler fallback: host the platform view inside Compose's
+ // `AndroidView { … }` interop. We have to apply a Compose
+ // `Modifier.Size` (or `Width`/`Height`) from the MAUI virtual
+ // view's `WidthRequest`/`HeightRequest` ourselves — without it
+ // Compose hands the AndroidView an unbounded slot and the hosted
+ // Android `View` measures wrap-content. For self-drawing MAUI
+ // views with no intrinsic size (`MauiShapeView`,
+ // `PlatformGraphicsView`) that collapses to 0×0 and the canvas
+ // paints nothing even though MAUI's mapper (`ShapeViewHandler`,
+ // `GraphicsViewHandler`) ran during `ToPlatform()` and set up a
+ // `Drawable`. Compose-aware handlers (`BoxViewHandler`,
+ // `LabelHandler`, …) apply this themselves via
+ // `IComposeHandler.BuildNode`; the fallback covers everyone else.
+ Modifier? modifier = null;
+ if (view is Microsoft.Maui.Controls.VisualElement ve)
+ {
+ modifier = (ve.WidthRequest, ve.HeightRequest) switch
+ {
+ ( >= 0d, >= 0d ) => Modifier.Size(new Dp((float)ve.WidthRequest), new Dp((float)ve.HeightRequest)),
+ ( >= 0d, _ ) => Modifier.Width(new Dp((float)ve.WidthRequest)),
+ ( _, >= 0d ) => Modifier.FillMaxWidth().Height(new Dp((float)ve.HeightRequest)),
+ _ => null,
+ };
+ }
+
+ return new AndroidView(factory: _ => view.ToPlatform(mauiContext))
+ {
+ Modifier = modifier,
+ };
}
}