From 1eb6e64f37e1e073aedbd00bd4dcb6cd78bf007d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 31 Jul 2025 00:04:42 +0000
Subject: [PATCH 01/10] Initial plan
From 8097856bbf9afc3f595180e9deeb6fe1c66aef17 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 31 Jul 2025 00:27:30 +0000
Subject: [PATCH 02/10] Add Android SafeAreaEdges implementation for
ContentViewGroup and LayoutViewGroup
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
---
.../src/Platform/Android/ContentViewGroup.cs | 121 ++++++++++++++++++
.../src/Platform/Android/LayoutViewGroup.cs | 121 ++++++++++++++++++
.../src/Platform/Android/SafeAreaPadding.cs | 69 ++++++++++
3 files changed, 311 insertions(+)
create mode 100644 src/Core/src/Platform/Android/SafeAreaPadding.cs
diff --git a/src/Core/src/Platform/Android/ContentViewGroup.cs b/src/Core/src/Platform/Android/ContentViewGroup.cs
index fc290a3a4d93..ca4d8b0ae439 100644
--- a/src/Core/src/Platform/Android/ContentViewGroup.cs
+++ b/src/Core/src/Platform/Android/ContentViewGroup.cs
@@ -4,6 +4,7 @@
using Android.Runtime;
using Android.Util;
using Android.Views;
+using AndroidX.Core.View;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Platform;
@@ -13,10 +14,13 @@ public class ContentViewGroup : PlatformContentViewGroup, ICrossPlatformLayoutBa
{
IBorderStroke? _clip;
readonly Context _context;
+ SafeAreaPadding _safeArea = SafeAreaPadding.Empty;
+ bool _safeAreaInvalidated = true;
public ContentViewGroup(Context context) : base(context)
{
_context = context;
+ SetupWindowInsetsHandling();
}
public ContentViewGroup(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
@@ -24,21 +28,35 @@ public ContentViewGroup(IntPtr javaReference, JniHandleOwnership transfer) : bas
var context = Context;
ArgumentNullException.ThrowIfNull(context);
_context = context;
+ SetupWindowInsetsHandling();
}
public ContentViewGroup(Context context, IAttributeSet attrs) : base(context, attrs)
{
_context = context;
+ SetupWindowInsetsHandling();
}
public ContentViewGroup(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
{
_context = context;
+ SetupWindowInsetsHandling();
}
public ContentViewGroup(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes)
{
_context = context;
+ SetupWindowInsetsHandling();
+ }
+
+ void SetupWindowInsetsHandling()
+ {
+ ViewCompat.SetOnApplyWindowInsetsListener(this, (view, insets) =>
+ {
+ _safeAreaInvalidated = true;
+ RequestLayout();
+ return insets;
+ });
}
public ICrossPlatformLayout? CrossPlatformLayout
@@ -56,6 +74,103 @@ Graphics.Size CrossPlatformArrange(Graphics.Rect bounds)
return CrossPlatformLayout?.CrossPlatformArrange(bounds) ?? Graphics.Size.Zero;
}
+ bool RespondsToSafeArea()
+ {
+ return CrossPlatformLayout is ISafeAreaView2;
+ }
+
+ SafeAreaRegions GetSafeAreaRegionForEdge(int edge)
+ {
+ if (CrossPlatformLayout is ISafeAreaView2 safeAreaPage)
+ {
+ return safeAreaPage.GetSafeAreaRegionsForEdge(edge);
+ }
+
+ // Fallback to legacy ISafeAreaView behavior
+ if (CrossPlatformLayout is ISafeAreaView sav)
+ {
+ return sav.IgnoreSafeArea ? SafeAreaRegions.None : SafeAreaRegions.Container;
+ }
+
+ return SafeAreaRegions.None;
+ }
+
+ static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double originalSafeArea)
+ {
+ // Edge-to-edge content - no safe area padding
+ if (safeAreaRegion == SafeAreaRegions.None)
+ return 0;
+
+ // All other regions respect safe area in some form
+ // This includes:
+ // - Default: Platform default behavior
+ // - All: Obey all safe area insets
+ // - SoftInput: Always pad for keyboard/soft input
+ // - Container: Content flows under keyboard but stays out of bars/notch
+ // - Any combination of the above flags
+ return originalSafeArea;
+ }
+
+ Graphics.Rect AdjustForSafeArea(Graphics.Rect bounds)
+ {
+ ValidateSafeArea();
+
+ if (_safeArea.IsEmpty)
+ return bounds;
+
+ return new Graphics.Rect(
+ bounds.X + _safeArea.Left,
+ bounds.Y + _safeArea.Top,
+ bounds.Width - _safeArea.HorizontalThickness,
+ bounds.Height - _safeArea.VerticalThickness);
+ }
+
+ SafeAreaPadding GetAdjustedSafeAreaInsets()
+ {
+ // Get WindowInsets if available
+ var rootView = RootView;
+ if (rootView == null)
+ return SafeAreaPadding.Empty;
+
+ var windowInsets = ViewCompat.GetRootWindowInsets(rootView);
+ if (windowInsets == null)
+ return SafeAreaPadding.Empty;
+
+ var baseSafeArea = windowInsets.ToSafeAreaInsets();
+
+ // Apply safe area selectively per edge based on SafeAreaRegions
+ if (CrossPlatformLayout is ISafeAreaView2)
+ {
+ var left = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(0), baseSafeArea.Left);
+ var top = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(1), baseSafeArea.Top);
+ var right = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(2), baseSafeArea.Right);
+ var bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3), baseSafeArea.Bottom);
+
+ return new SafeAreaPadding(left, right, top, bottom);
+ }
+
+ // Legacy ISafeAreaView handling
+ if (CrossPlatformLayout is ISafeAreaView sav && sav.IgnoreSafeArea)
+ {
+ return SafeAreaPadding.Empty;
+ }
+
+ return baseSafeArea;
+ }
+
+ bool ValidateSafeArea()
+ {
+ if (!_safeAreaInvalidated)
+ return true;
+
+ _safeAreaInvalidated = false;
+
+ var oldSafeArea = _safeArea;
+ _safeArea = GetAdjustedSafeAreaInsets();
+
+ return oldSafeArea == _safeArea;
+ }
+
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
if (CrossPlatformLayout is null)
@@ -96,6 +211,12 @@ protected override void OnLayout(bool changed, int left, int top, int right, int
var destination = _context.ToCrossPlatformRectInReferenceFrame(left, top, right, bottom);
+ // Apply safe area adjustments if needed
+ if (RespondsToSafeArea())
+ {
+ destination = AdjustForSafeArea(destination);
+ }
+
CrossPlatformArrange(destination);
}
diff --git a/src/Core/src/Platform/Android/LayoutViewGroup.cs b/src/Core/src/Platform/Android/LayoutViewGroup.cs
index 6b4f40dad1d2..995426747e0c 100644
--- a/src/Core/src/Platform/Android/LayoutViewGroup.cs
+++ b/src/Core/src/Platform/Android/LayoutViewGroup.cs
@@ -4,6 +4,7 @@
using Android.Util;
using Android.Views;
using Android.Widget;
+using AndroidX.Core.View;
using Microsoft.Maui.Graphics;
using ARect = Android.Graphics.Rect;
using Rectangle = Microsoft.Maui.Graphics.Rect;
@@ -15,12 +16,15 @@ public class LayoutViewGroup : ViewGroup, ICrossPlatformLayoutBacking, IVisualTr
{
readonly ARect _clipRect = new();
readonly Context _context;
+ SafeAreaPadding _safeArea = SafeAreaPadding.Empty;
+ bool _safeAreaInvalidated = true;
public bool InputTransparent { get; set; }
public LayoutViewGroup(Context context) : base(context)
{
_context = context;
+ SetupWindowInsetsHandling();
}
public LayoutViewGroup(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
@@ -28,21 +32,35 @@ public LayoutViewGroup(IntPtr javaReference, JniHandleOwnership transfer) : base
var context = Context;
ArgumentNullException.ThrowIfNull(context);
_context = context;
+ SetupWindowInsetsHandling();
}
public LayoutViewGroup(Context context, IAttributeSet attrs) : base(context, attrs)
{
_context = context;
+ SetupWindowInsetsHandling();
}
public LayoutViewGroup(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
{
_context = context;
+ SetupWindowInsetsHandling();
}
public LayoutViewGroup(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes)
{
_context = context;
+ SetupWindowInsetsHandling();
+ }
+
+ void SetupWindowInsetsHandling()
+ {
+ ViewCompat.SetOnApplyWindowInsetsListener(this, (view, insets) =>
+ {
+ _safeAreaInvalidated = true;
+ RequestLayout();
+ return insets;
+ });
}
public bool ClipsToBounds { get; set; }
@@ -62,6 +80,103 @@ Graphics.Size CrossPlatformArrange(Graphics.Rect bounds)
return CrossPlatformLayout?.CrossPlatformArrange(bounds) ?? Graphics.Size.Zero;
}
+ bool RespondsToSafeArea()
+ {
+ return CrossPlatformLayout is ISafeAreaView2;
+ }
+
+ SafeAreaRegions GetSafeAreaRegionForEdge(int edge)
+ {
+ if (CrossPlatformLayout is ISafeAreaView2 safeAreaPage)
+ {
+ return safeAreaPage.GetSafeAreaRegionsForEdge(edge);
+ }
+
+ // Fallback to legacy ISafeAreaView behavior
+ if (CrossPlatformLayout is ISafeAreaView sav)
+ {
+ return sav.IgnoreSafeArea ? SafeAreaRegions.None : SafeAreaRegions.Container;
+ }
+
+ return SafeAreaRegions.None;
+ }
+
+ static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double originalSafeArea)
+ {
+ // Edge-to-edge content - no safe area padding
+ if (safeAreaRegion == SafeAreaRegions.None)
+ return 0;
+
+ // All other regions respect safe area in some form
+ // This includes:
+ // - Default: Platform default behavior
+ // - All: Obey all safe area insets
+ // - SoftInput: Always pad for keyboard/soft input
+ // - Container: Content flows under keyboard but stays out of bars/notch
+ // - Any combination of the above flags
+ return originalSafeArea;
+ }
+
+ Graphics.Rect AdjustForSafeArea(Graphics.Rect bounds)
+ {
+ ValidateSafeArea();
+
+ if (_safeArea.IsEmpty)
+ return bounds;
+
+ return new Graphics.Rect(
+ bounds.X + _safeArea.Left,
+ bounds.Y + _safeArea.Top,
+ bounds.Width - _safeArea.HorizontalThickness,
+ bounds.Height - _safeArea.VerticalThickness);
+ }
+
+ SafeAreaPadding GetAdjustedSafeAreaInsets()
+ {
+ // Get WindowInsets if available
+ var rootView = RootView;
+ if (rootView == null)
+ return SafeAreaPadding.Empty;
+
+ var windowInsets = ViewCompat.GetRootWindowInsets(rootView);
+ if (windowInsets == null)
+ return SafeAreaPadding.Empty;
+
+ var baseSafeArea = windowInsets.ToSafeAreaInsets();
+
+ // Apply safe area selectively per edge based on SafeAreaRegions
+ if (CrossPlatformLayout is ISafeAreaView2)
+ {
+ var left = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(0), baseSafeArea.Left);
+ var top = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(1), baseSafeArea.Top);
+ var right = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(2), baseSafeArea.Right);
+ var bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3), baseSafeArea.Bottom);
+
+ return new SafeAreaPadding(left, right, top, bottom);
+ }
+
+ // Legacy ISafeAreaView handling
+ if (CrossPlatformLayout is ISafeAreaView sav && sav.IgnoreSafeArea)
+ {
+ return SafeAreaPadding.Empty;
+ }
+
+ return baseSafeArea;
+ }
+
+ bool ValidateSafeArea()
+ {
+ if (!_safeAreaInvalidated)
+ return true;
+
+ _safeAreaInvalidated = false;
+
+ var oldSafeArea = _safeArea;
+ _safeArea = GetAdjustedSafeAreaInsets();
+
+ return oldSafeArea == _safeArea;
+ }
+
// TODO: Possibly reconcile this code with ViewHandlerExtensions.MeasureVirtualView
// If you make changes here please review if those changes should also
// apply to ViewHandlerExtensions.MeasureVirtualView
@@ -108,6 +223,12 @@ protected override void OnLayout(bool changed, int l, int t, int r, int b)
var destination = _context.ToCrossPlatformRectInReferenceFrame(l, t, r, b);
+ // Apply safe area adjustments if needed
+ if (RespondsToSafeArea())
+ {
+ destination = AdjustForSafeArea(destination);
+ }
+
CrossPlatformArrange(destination);
if (ClipsToBounds)
diff --git a/src/Core/src/Platform/Android/SafeAreaPadding.cs b/src/Core/src/Platform/Android/SafeAreaPadding.cs
new file mode 100644
index 000000000000..faf5e1f03167
--- /dev/null
+++ b/src/Core/src/Platform/Android/SafeAreaPadding.cs
@@ -0,0 +1,69 @@
+using System;
+using Android.Graphics;
+using AndroidX.Core.Graphics;
+using AndroidX.Core.View;
+
+namespace Microsoft.Maui.Platform;
+
+internal readonly record struct SafeAreaPadding(double Left, double Right, double Top, double Bottom)
+{
+ public static SafeAreaPadding Empty { get; } = new(0, 0, 0, 0);
+
+ public bool IsEmpty { get; } = Left == 0 && Right == 0 && Top == 0 && Bottom == 0;
+ public double HorizontalThickness { get; } = Left + Right;
+ public double VerticalThickness { get; } = Top + Bottom;
+
+ public Rect InsetRect(Rect bounds)
+ {
+ if (IsEmpty)
+ {
+ return bounds;
+ }
+
+ return new Rect(
+ (int)(bounds.Left + Left),
+ (int)(bounds.Top + Top),
+ (int)(bounds.Right - Right),
+ (int)(bounds.Bottom - Bottom));
+ }
+
+ public Insets ToInsets() =>
+ Insets.Of((int)Left, (int)Top, (int)Right, (int)Bottom);
+}
+
+internal static class WindowInsetsExtensions
+{
+ public static SafeAreaPadding ToSafeAreaInsets(this WindowInsetsCompat insets)
+ {
+ // Get system bars insets (status bar, navigation bar)
+ var systemBars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars());
+
+ // Get display cutout insets if available (API 28+)
+ var displayCutout = insets.GetInsets(WindowInsetsCompat.Type.DisplayCutout());
+
+ // Combine insets, taking the maximum for each edge
+ return new(
+ Math.Max(systemBars.Left, displayCutout.Left),
+ Math.Max(systemBars.Right, displayCutout.Right),
+ Math.Max(systemBars.Top, displayCutout.Top),
+ Math.Max(systemBars.Bottom, displayCutout.Bottom)
+ );
+ }
+
+ public static SafeAreaPadding ToSafeAreaInsetsWithKeyboard(this WindowInsetsCompat insets)
+ {
+ // Get base safe area insets
+ var safeArea = insets.ToSafeAreaInsets();
+
+ // Get keyboard insets if available (API 30+)
+ var keyboard = insets.GetInsets(WindowInsetsCompat.Type.Ime());
+
+ // For keyboard, we only care about the bottom inset and take the maximum
+ return new(
+ safeArea.Left,
+ safeArea.Right,
+ safeArea.Top,
+ Math.Max(safeArea.Bottom, keyboard.Bottom)
+ );
+ }
+}
\ No newline at end of file
From 2bef5826c7ccfdde5efa75718941260f4719b818 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 31 Jul 2025 00:30:54 +0000
Subject: [PATCH 03/10] Add enhanced keyboard support and Android tests for
SafeAreaEdges
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
---
.../Issues/SafeAreaAndroidTest.xaml | 137 +++++++++++++++++
.../Issues/SafeAreaAndroidTest.xaml.cs | 97 ++++++++++++
.../Tests/Issues/Issue28986Android.cs | 141 ++++++++++++++++++
.../src/Platform/Android/ContentViewGroup.cs | 26 +++-
.../src/Platform/Android/LayoutViewGroup.cs | 26 +++-
.../src/Platform/Android/SafeAreaPadding.cs | 9 ++
6 files changed, 422 insertions(+), 14 deletions(-)
create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/SafeAreaAndroidTest.xaml
create mode 100644 src/Controls/tests/TestCases.HostApp/Issues/SafeAreaAndroidTest.xaml.cs
create mode 100644 src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986Android.cs
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/SafeAreaAndroidTest.xaml b/src/Controls/tests/TestCases.HostApp/Issues/SafeAreaAndroidTest.xaml
new file mode 100644
index 000000000000..3ce594c20d2f
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/SafeAreaAndroidTest.xaml
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/SafeAreaAndroidTest.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/SafeAreaAndroidTest.xaml.cs
new file mode 100644
index 000000000000..35fbbdbdb081
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/SafeAreaAndroidTest.xaml.cs
@@ -0,0 +1,97 @@
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, 0, "Test SafeArea Android Implementation", PlatformAffected.Android, issueTestNumber: 0)]
+public partial class SafeAreaAndroidTest : ContentPage
+{
+ public SafeAreaAndroidTest()
+ {
+ InitializeComponent();
+ UpdateCurrentSettingsLabel();
+ }
+
+ private void OnGridSetNoneClicked(object sender, EventArgs e)
+ {
+ MainGrid.SafeAreaEdges = SafeAreaEdges.None;
+ UpdateCurrentSettingsLabel();
+ }
+
+ private void OnGridSetContainerClicked(object sender, EventArgs e)
+ {
+ MainGrid.SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
+ UpdateCurrentSettingsLabel();
+ }
+
+ private void OnGridSetAllClicked(object sender, EventArgs e)
+ {
+ MainGrid.SafeAreaEdges = SafeAreaEdges.All;
+ UpdateCurrentSettingsLabel();
+ }
+
+ private void OnGridSetBottomSoftInputClicked(object sender, EventArgs e)
+ {
+ var current = MainGrid.SafeAreaEdges;
+ MainGrid.SafeAreaEdges = new SafeAreaEdges(
+ current.Left, // Left
+ current.Top, // Top
+ current.Right, // Right
+ SafeAreaRegions.SoftInput // Bottom
+ );
+ UpdateCurrentSettingsLabel();
+ }
+
+ private void OnGridSetTopContainerClicked(object sender, EventArgs e)
+ {
+ var current = MainGrid.SafeAreaEdges;
+ MainGrid.SafeAreaEdges = new SafeAreaEdges(
+ current.Left, // Left (keep current)
+ SafeAreaRegions.Container, // Top
+ current.Right, // Right (keep current)
+ current.Bottom // Bottom (keep current)
+ );
+ UpdateCurrentSettingsLabel();
+ }
+
+ private void OnGridSetTopNoneClicked(object sender, EventArgs e)
+ {
+ var current = MainGrid.SafeAreaEdges;
+ MainGrid.SafeAreaEdges = new SafeAreaEdges(
+ current.Left, // Left (keep current)
+ SafeAreaRegions.None, // Top
+ current.Right, // Right (keep current)
+ current.Bottom // Bottom (keep current)
+ );
+ UpdateCurrentSettingsLabel();
+ }
+
+ private void UpdateCurrentSettingsLabel()
+ {
+ var edges = MainGrid.SafeAreaEdges;
+ var settingText = GetSafeAreaEdgesDescription(edges);
+ CurrentSettingsLabel.Text = $"Current MainGrid SafeAreaEdges: {settingText}";
+ }
+
+ private string GetSafeAreaEdgesDescription(SafeAreaEdges edges)
+ {
+ // Check for common patterns
+ if (edges.Left == SafeAreaRegions.None && edges.Top == SafeAreaRegions.None &&
+ edges.Right == SafeAreaRegions.None && edges.Bottom == SafeAreaRegions.None)
+ {
+ return "None (Edge-to-edge)";
+ }
+
+ if (edges.Left == SafeAreaRegions.All && edges.Top == SafeAreaRegions.All &&
+ edges.Right == SafeAreaRegions.All && edges.Bottom == SafeAreaRegions.All)
+ {
+ return "All (Full safe area)";
+ }
+
+ if (edges.Left == SafeAreaRegions.Container && edges.Top == SafeAreaRegions.Container &&
+ edges.Right == SafeAreaRegions.Container && edges.Bottom == SafeAreaRegions.Container)
+ {
+ return "Container (Respect notches/bars)";
+ }
+
+ // For mixed values, show individual edges
+ return $"Left:{edges.Left}, Top:{edges.Top}, Right:{edges.Right}, Bottom:{edges.Bottom}";
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986Android.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986Android.cs
new file mode 100644
index 000000000000..d49d47399251
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986Android.cs
@@ -0,0 +1,141 @@
+#if ANDROID
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues
+{
+ public class Issue28986Android : _IssuesUITest
+ {
+ public override string Issue => "Test SafeArea attached property for per-edge safe area control on Android";
+
+ public Issue28986Android(TestDevice device) : base(device)
+ {
+ }
+
+ [Test]
+ [Category(UITestCategories.Layout)]
+ public void SafeAreaMainGridBasicFunctionalityAndroid()
+ {
+ // 1. Test loads - verify essential elements are present
+ App.WaitForElement("ContentGrid");
+
+ // 2. Verify initial state - MainGrid should start with All (offset by safe area)
+ var initialSettings = App.FindElement("CurrentSettings").GetText();
+ Assert.That(initialSettings, Does.Contain("All (Full safe area)"));
+
+ var safePosition = App.WaitForElement("ContentGrid").GetRect();
+
+ // 3. Click button to set SafeAreaEdge to "None" on the MainGrid
+ App.Tap("GridResetNoneButton");
+
+ var unSafePosition = App.WaitForElement("ContentGrid").GetRect();
+
+ // On Android, verify that SafeAreaEdges None results in edge-to-edge layout
+ Assert.That(unSafePosition.Y, Is.EqualTo(0), "ContentGrid Y position should be 0 when SafeAreaEdges is set to None");
+ Assert.That(safePosition.Y, Is.Not.EqualTo(0), "ContentGrid Y position should not be 0 when SafeAreaEdges is set to All");
+ }
+
+ [Test]
+ [Category(UITestCategories.Layout)]
+ public void SafeAreaMainGridAllButtonFunctionalityAndroid()
+ {
+ App.WaitForElement("GridResetAllButton");
+ App.WaitForElement("ContentGrid");
+
+ // First set to None to establish baseline position
+ App.Tap("GridResetNoneButton");
+ var nonePosition = App.WaitForElement("ContentGrid").GetRect();
+
+ // Test "All" button functionality
+ App.Tap("GridResetAllButton");
+ var allPosition = App.WaitForElement("ContentGrid").GetRect();
+
+ // Verify MainGrid is set to All
+ var allSettings = App.FindElement("CurrentSettings").GetText();
+ Assert.That(allSettings, Does.Contain("All (Full safe area)"));
+
+ // Verify position changes - All should offset content away from screen edges
+ Assert.That(allPosition.Y, Is.Not.EqualTo(0), "ContentGrid Y position should not be 0 when SafeAreaEdges is set to All");
+ Assert.That(allPosition.Y, Is.GreaterThan(nonePosition.Y), "ContentGrid should be positioned lower when SafeAreaEdges is All vs None");
+ }
+
+ [Test]
+ [Category(UITestCategories.Layout)]
+ public void SafeAreaMainGridSequentialButtonTestingAndroid()
+ {
+ App.WaitForElement("ContentGrid");
+ App.WaitForElement("CurrentSettings");
+
+ // Test sequence: All -> None -> Container -> All with position validation
+
+ // 1. Set to All and capture position
+ App.Tap("GridResetAllButton");
+ var allPosition = App.WaitForElement("ContentGrid").GetRect();
+ var allSettings = App.FindElement("CurrentSettings").GetText();
+ Assert.That(allSettings, Does.Contain("All (Full safe area)"));
+
+ // 2. Set to None and verify position changes
+ App.Tap("GridResetNoneButton");
+ var nonePosition = App.WaitForElement("ContentGrid").GetRect();
+ var noneSettings = App.FindElement("CurrentSettings").GetText();
+ Assert.That(noneSettings, Does.Contain("None (Edge-to-edge)"));
+ Assert.That(nonePosition.Y, Is.EqualTo(0), "ContentGrid Y position should be 0 when SafeAreaEdges is None (edge-to-edge)");
+ Assert.That(allPosition.Y, Is.GreaterThan(nonePosition.Y), "All position should be lower than None position");
+
+ // 3. Set to Container and verify position changes
+ App.Tap("GridSetContainerButton");
+ var containerPosition = App.WaitForElement("ContentGrid").GetRect();
+ var containerSettings = App.FindElement("CurrentSettings").GetText();
+ Assert.That(containerSettings, Does.Contain("Container (Respect notches/bars)"));
+ Assert.That(containerPosition.Y, Is.GreaterThan(nonePosition.Y), "Container position should be lower than None position");
+
+ // 4. Return to All and verify position matches original
+ App.Tap("GridResetAllButton");
+ var finalAllPosition = App.WaitForElement("ContentGrid").GetRect();
+ var finalAllSettings = App.FindElement("CurrentSettings").GetText();
+ Assert.That(finalAllSettings, Does.Contain("All (Full safe area)"));
+ Assert.That(finalAllPosition.Y, Is.EqualTo(allPosition.Y), "Final All position should match initial All position");
+ }
+
+ [Test]
+ [Category(UITestCategories.Layout)]
+ public void SafeAreaPerEdgeValidationAndroid()
+ {
+ App.WaitForElement("ContentGrid");
+
+ // Test per-edge functionality specifically for Android
+ // First establish baseline with Container setting
+ App.Tap("GridSetContainerButton");
+ var containerPosition = App.WaitForElement("ContentGrid").GetRect();
+
+ // Test top edge setting to None - should allow content into status bar area
+ App.Tap("GridSetTopNoneButton");
+ var topNonePosition = App.WaitForElement("ContentGrid").GetRect();
+
+ // Verify the Y position is different when top edge is set to None
+ Assert.That(topNonePosition.Y, Is.LessThan(containerPosition.Y), "Content should move higher when top edge is set to None");
+
+ // Test bottom edge SoftInput behavior
+ App.Tap("GridSetBottomSoftInputButton");
+ var currentSettings = App.FindElement("CurrentSettings").GetText();
+ Assert.That(currentSettings, Does.Contain("SoftInput"), "Current settings should show SoftInput for bottom edge");
+
+ // Test keyboard interaction for SoftInput
+ App.Tap("SoftInputTestEntry");
+ var entryFocusedPosition = App.WaitForElement("ContentGrid").GetRect();
+
+ // Android specific: Verify layout adjusts when keyboard is shown with SoftInput edge
+ // Note: Height comparison is more reliable than Y position on Android
+ Assert.That(entryFocusedPosition.Height, Is.LessThan(topNonePosition.Height),
+ "ContentGrid height should be less when keyboard is shown with SoftInput bottom edge");
+
+ App.DismissKeyboard();
+
+ var keyboardDismissedPosition = App.WaitForElement("ContentGrid").GetRect();
+ Assert.That(keyboardDismissedPosition.Height, Is.GreaterThan(entryFocusedPosition.Height),
+ "ContentGrid height should increase when keyboard is dismissed");
+ }
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Core/src/Platform/Android/ContentViewGroup.cs b/src/Core/src/Platform/Android/ContentViewGroup.cs
index ca4d8b0ae439..980e2ca29d0d 100644
--- a/src/Core/src/Platform/Android/ContentViewGroup.cs
+++ b/src/Core/src/Platform/Android/ContentViewGroup.cs
@@ -95,18 +95,29 @@ SafeAreaRegions GetSafeAreaRegionForEdge(int edge)
return SafeAreaRegions.None;
}
- static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double originalSafeArea)
+ static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double originalSafeArea, double keyboardInset)
{
// Edge-to-edge content - no safe area padding
if (safeAreaRegion == SafeAreaRegions.None)
return 0;
+ // SoftInput region - always pad for keyboard/soft input
+ if (SafeAreaEdges.IsSoftInput(safeAreaRegion))
+ {
+ return Math.Max(originalSafeArea, keyboardInset);
+ }
+
+ // Container region - content flows under keyboard but stays out of bars/notch
+ if (SafeAreaEdges.IsContainer(safeAreaRegion))
+ {
+ // For now, treat Container same as Default (can be enhanced later for keyboard-specific behavior)
+ return originalSafeArea;
+ }
+
// All other regions respect safe area in some form
// This includes:
// - Default: Platform default behavior
// - All: Obey all safe area insets
- // - SoftInput: Always pad for keyboard/soft input
- // - Container: Content flows under keyboard but stays out of bars/notch
// - Any combination of the above flags
return originalSafeArea;
}
@@ -137,14 +148,15 @@ SafeAreaPadding GetAdjustedSafeAreaInsets()
return SafeAreaPadding.Empty;
var baseSafeArea = windowInsets.ToSafeAreaInsets();
+ var keyboardInsets = windowInsets.GetKeyboardInsets();
// Apply safe area selectively per edge based on SafeAreaRegions
if (CrossPlatformLayout is ISafeAreaView2)
{
- var left = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(0), baseSafeArea.Left);
- var top = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(1), baseSafeArea.Top);
- var right = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(2), baseSafeArea.Right);
- var bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3), baseSafeArea.Bottom);
+ var left = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(0), baseSafeArea.Left, keyboardInsets.Left);
+ var top = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(1), baseSafeArea.Top, keyboardInsets.Top);
+ var right = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(2), baseSafeArea.Right, keyboardInsets.Right);
+ var bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3), baseSafeArea.Bottom, keyboardInsets.Bottom);
return new SafeAreaPadding(left, right, top, bottom);
}
diff --git a/src/Core/src/Platform/Android/LayoutViewGroup.cs b/src/Core/src/Platform/Android/LayoutViewGroup.cs
index 995426747e0c..0fbb16e575c8 100644
--- a/src/Core/src/Platform/Android/LayoutViewGroup.cs
+++ b/src/Core/src/Platform/Android/LayoutViewGroup.cs
@@ -101,18 +101,29 @@ SafeAreaRegions GetSafeAreaRegionForEdge(int edge)
return SafeAreaRegions.None;
}
- static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double originalSafeArea)
+ static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double originalSafeArea, double keyboardInset)
{
// Edge-to-edge content - no safe area padding
if (safeAreaRegion == SafeAreaRegions.None)
return 0;
+ // SoftInput region - always pad for keyboard/soft input
+ if (SafeAreaEdges.IsSoftInput(safeAreaRegion))
+ {
+ return Math.Max(originalSafeArea, keyboardInset);
+ }
+
+ // Container region - content flows under keyboard but stays out of bars/notch
+ if (SafeAreaEdges.IsContainer(safeAreaRegion))
+ {
+ // For now, treat Container same as Default (can be enhanced later for keyboard-specific behavior)
+ return originalSafeArea;
+ }
+
// All other regions respect safe area in some form
// This includes:
// - Default: Platform default behavior
// - All: Obey all safe area insets
- // - SoftInput: Always pad for keyboard/soft input
- // - Container: Content flows under keyboard but stays out of bars/notch
// - Any combination of the above flags
return originalSafeArea;
}
@@ -143,14 +154,15 @@ SafeAreaPadding GetAdjustedSafeAreaInsets()
return SafeAreaPadding.Empty;
var baseSafeArea = windowInsets.ToSafeAreaInsets();
+ var keyboardInsets = windowInsets.GetKeyboardInsets();
// Apply safe area selectively per edge based on SafeAreaRegions
if (CrossPlatformLayout is ISafeAreaView2)
{
- var left = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(0), baseSafeArea.Left);
- var top = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(1), baseSafeArea.Top);
- var right = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(2), baseSafeArea.Right);
- var bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3), baseSafeArea.Bottom);
+ var left = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(0), baseSafeArea.Left, keyboardInsets.Left);
+ var top = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(1), baseSafeArea.Top, keyboardInsets.Top);
+ var right = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(2), baseSafeArea.Right, keyboardInsets.Right);
+ var bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3), baseSafeArea.Bottom, keyboardInsets.Bottom);
return new SafeAreaPadding(left, right, top, bottom);
}
diff --git a/src/Core/src/Platform/Android/SafeAreaPadding.cs b/src/Core/src/Platform/Android/SafeAreaPadding.cs
index faf5e1f03167..38b53d143447 100644
--- a/src/Core/src/Platform/Android/SafeAreaPadding.cs
+++ b/src/Core/src/Platform/Android/SafeAreaPadding.cs
@@ -66,4 +66,13 @@ public static SafeAreaPadding ToSafeAreaInsetsWithKeyboard(this WindowInsetsComp
Math.Max(safeArea.Bottom, keyboard.Bottom)
);
}
+
+ public static SafeAreaPadding GetKeyboardInsets(this WindowInsetsCompat insets)
+ {
+ // Get keyboard insets if available (API 30+)
+ var keyboard = insets.GetInsets(WindowInsetsCompat.Type.Ime());
+
+ // Return only keyboard insets (typically only bottom)
+ return new(0, 0, 0, keyboard.Bottom);
+ }
}
\ No newline at end of file
From 657d2bd0fea3ab8cbbd9b83229b33d4bb4c2d371 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 31 Jul 2025 00:34:08 +0000
Subject: [PATCH 04/10] Fix pixel density handling and add Android
SafeAreaEdges documentation
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
---
docs/SafeAreaEdges-Android.md | 118 ++++++++++++++++++
.../src/Platform/Android/ContentViewGroup.cs | 10 +-
.../src/Platform/Android/LayoutViewGroup.cs | 10 +-
.../src/Platform/Android/SafeAreaPadding.cs | 38 ++++--
4 files changed, 152 insertions(+), 24 deletions(-)
create mode 100644 docs/SafeAreaEdges-Android.md
diff --git a/docs/SafeAreaEdges-Android.md b/docs/SafeAreaEdges-Android.md
new file mode 100644
index 000000000000..dffafda4f6b6
--- /dev/null
+++ b/docs/SafeAreaEdges-Android.md
@@ -0,0 +1,118 @@
+# SafeAreaEdges Android Implementation
+
+This document describes the Android implementation of SafeAreaEdges functionality for .NET MAUI, which provides per-edge safe area control matching the functionality introduced for iOS in PR #30337.
+
+## Overview
+
+The Android SafeAreaEdges implementation allows developers to control how content respects system UI elements like status bars, navigation bars, display cutouts, and the keyboard on a per-edge basis.
+
+## Implementation Details
+
+### Core Components
+
+1. **SafeAreaPadding.cs** - Android equivalent of iOS SafeAreaPadding, handles inset calculations
+2. **ContentViewGroup.cs** - Updated to apply SafeAreaEdges logic during layout
+3. **LayoutViewGroup.cs** - Updated to apply SafeAreaEdges logic during layout
+4. **WindowInsetsExtensions** - Helper methods to convert Android WindowInsets to SafeAreaPadding
+
+### SafeAreaRegions Behavior on Android
+
+- **None**: Content goes edge-to-edge, ignoring all system UI elements
+- **All**: Content respects all safe area insets (status bar, navigation bar, display cutouts, keyboard)
+- **Container**: Content flows under keyboard but stays out of status/navigation bars and display cutouts
+- **SoftInput**: Always pad to avoid keyboard overlap
+- **Default**: Platform default behavior (currently same as Container)
+
+### Android-Specific Considerations
+
+#### WindowInsets Integration
+- Uses AndroidX.Core.View.WindowInsetsCompat for consistent API across Android versions
+- Supports WindowInsetsCompat.Type.SystemBars() for status/navigation bars
+- Supports WindowInsetsCompat.Type.DisplayCutout() for display cutouts (API 28+)
+- Supports WindowInsetsCompat.Type.Ime() for keyboard insets (API 30+)
+
+#### Pixel Density Handling
+- All insets are converted from Android pixels to device-independent units using Context.GetDisplayDensity()
+- This ensures consistent behavior across different screen densities
+
+#### WindowInsets Listener
+- Each view group sets up a WindowInsetsListener to receive inset changes
+- When insets change, the view invalidates and triggers a layout update
+- This ensures dynamic updates when keyboard appears/disappears or device orientation changes
+
+### Edge-Specific Logic
+
+The implementation processes each edge (Left, Top, Right, Bottom) independently:
+
+1. **SafeAreaRegions.None**: Returns 0 padding (edge-to-edge)
+2. **SafeAreaRegions.SoftInput**: Returns max of original safe area and keyboard inset
+3. **SafeAreaRegions.Container**: Returns original safe area (ignores keyboard)
+4. **SafeAreaRegions.All/Default**: Returns original safe area
+
+### Layout Integration
+
+Both ContentViewGroup and LayoutViewGroup apply SafeAreaEdges in their OnLayout methods:
+
+1. Check if the cross-platform layout implements ISafeAreaView2
+2. Calculate adjusted safe area insets based on SafeAreaEdges configuration
+3. Apply insets to the layout bounds before arranging child content
+
+## Usage Examples
+
+### Edge-to-Edge Content
+```xml
+
+
+
+```
+
+### Respect All Safe Areas
+```xml
+
+
+
+```
+
+### Per-Edge Control
+```xml
+
+
+
+```
+
+```csharp
+// Set bottom edge to handle keyboard, keep other edges edge-to-edge
+myGrid.SafeAreaEdges = new SafeAreaEdges(
+ SafeAreaRegions.None, // Left
+ SafeAreaRegions.None, // Top
+ SafeAreaRegions.None, // Right
+ SafeAreaRegions.SoftInput // Bottom
+);
+```
+
+## Testing
+
+### Unit Tests
+- Issue28986Android.cs provides Android-specific UI tests
+- Tests verify edge-to-edge vs safe area positioning
+- Tests verify keyboard interaction with SoftInput regions
+
+### Manual Testing
+- SafeAreaAndroidTest.xaml provides a comprehensive test page
+- Includes buttons to test all SafeAreaRegions combinations
+- Provides visual feedback for safe area behavior
+- Entry field for testing keyboard/SoftInput behavior
+
+## Platform Differences from iOS
+
+1. **System UI Elements**: Android has status bar and navigation bar instead of iOS notch/home indicator
+2. **Keyboard Behavior**: Android WindowInsets provide more granular keyboard information
+3. **Display Cutouts**: Android supports various cutout shapes beyond iOS notch
+4. **Edge Cases**: Android handles orientation changes and foldable devices differently
+
+## Future Enhancements
+
+1. **WindowInsetsAnimation**: Could be integrated for smooth keyboard animations
+2. **Navigation Bar Behavior**: Could distinguish between gesture navigation and button navigation
+3. **Foldable Support**: Could handle foldable device specific insets
+4. **Performance**: Could cache inset calculations for better performance
\ No newline at end of file
diff --git a/src/Core/src/Platform/Android/ContentViewGroup.cs b/src/Core/src/Platform/Android/ContentViewGroup.cs
index 980e2ca29d0d..6b745d98ae5d 100644
--- a/src/Core/src/Platform/Android/ContentViewGroup.cs
+++ b/src/Core/src/Platform/Android/ContentViewGroup.cs
@@ -129,11 +129,7 @@ Graphics.Rect AdjustForSafeArea(Graphics.Rect bounds)
if (_safeArea.IsEmpty)
return bounds;
- return new Graphics.Rect(
- bounds.X + _safeArea.Left,
- bounds.Y + _safeArea.Top,
- bounds.Width - _safeArea.HorizontalThickness,
- bounds.Height - _safeArea.VerticalThickness);
+ return _safeArea.InsetRectF(bounds);
}
SafeAreaPadding GetAdjustedSafeAreaInsets()
@@ -147,8 +143,8 @@ SafeAreaPadding GetAdjustedSafeAreaInsets()
if (windowInsets == null)
return SafeAreaPadding.Empty;
- var baseSafeArea = windowInsets.ToSafeAreaInsets();
- var keyboardInsets = windowInsets.GetKeyboardInsets();
+ var baseSafeArea = windowInsets.ToSafeAreaInsets(_context);
+ var keyboardInsets = windowInsets.GetKeyboardInsets(_context);
// Apply safe area selectively per edge based on SafeAreaRegions
if (CrossPlatformLayout is ISafeAreaView2)
diff --git a/src/Core/src/Platform/Android/LayoutViewGroup.cs b/src/Core/src/Platform/Android/LayoutViewGroup.cs
index 0fbb16e575c8..3e0f3dc7560b 100644
--- a/src/Core/src/Platform/Android/LayoutViewGroup.cs
+++ b/src/Core/src/Platform/Android/LayoutViewGroup.cs
@@ -135,11 +135,7 @@ Graphics.Rect AdjustForSafeArea(Graphics.Rect bounds)
if (_safeArea.IsEmpty)
return bounds;
- return new Graphics.Rect(
- bounds.X + _safeArea.Left,
- bounds.Y + _safeArea.Top,
- bounds.Width - _safeArea.HorizontalThickness,
- bounds.Height - _safeArea.VerticalThickness);
+ return _safeArea.InsetRectF(bounds);
}
SafeAreaPadding GetAdjustedSafeAreaInsets()
@@ -153,8 +149,8 @@ SafeAreaPadding GetAdjustedSafeAreaInsets()
if (windowInsets == null)
return SafeAreaPadding.Empty;
- var baseSafeArea = windowInsets.ToSafeAreaInsets();
- var keyboardInsets = windowInsets.GetKeyboardInsets();
+ var baseSafeArea = windowInsets.ToSafeAreaInsets(_context);
+ var keyboardInsets = windowInsets.GetKeyboardInsets(_context);
// Apply safe area selectively per edge based on SafeAreaRegions
if (CrossPlatformLayout is ISafeAreaView2)
diff --git a/src/Core/src/Platform/Android/SafeAreaPadding.cs b/src/Core/src/Platform/Android/SafeAreaPadding.cs
index 38b53d143447..681063c6d1a4 100644
--- a/src/Core/src/Platform/Android/SafeAreaPadding.cs
+++ b/src/Core/src/Platform/Android/SafeAreaPadding.cs
@@ -2,6 +2,7 @@
using Android.Graphics;
using AndroidX.Core.Graphics;
using AndroidX.Core.View;
+using Microsoft.Maui.Graphics;
namespace Microsoft.Maui.Platform;
@@ -27,13 +28,27 @@ public Rect InsetRect(Rect bounds)
(int)(bounds.Bottom - Bottom));
}
+ public Graphics.Rect InsetRectF(Graphics.Rect bounds)
+ {
+ if (IsEmpty)
+ {
+ return bounds;
+ }
+
+ return new Graphics.Rect(
+ bounds.X + Left,
+ bounds.Y + Top,
+ bounds.Width - HorizontalThickness,
+ bounds.Height - VerticalThickness);
+ }
+
public Insets ToInsets() =>
Insets.Of((int)Left, (int)Top, (int)Right, (int)Bottom);
}
internal static class WindowInsetsExtensions
{
- public static SafeAreaPadding ToSafeAreaInsets(this WindowInsetsCompat insets)
+ public static SafeAreaPadding ToSafeAreaInsets(this WindowInsetsCompat insets, Context context)
{
// Get system bars insets (status bar, navigation bar)
var systemBars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars());
@@ -42,37 +57,40 @@ public static SafeAreaPadding ToSafeAreaInsets(this WindowInsetsCompat insets)
var displayCutout = insets.GetInsets(WindowInsetsCompat.Type.DisplayCutout());
// Combine insets, taking the maximum for each edge
+ // Convert from pixels to device-independent units
return new(
- Math.Max(systemBars.Left, displayCutout.Left),
- Math.Max(systemBars.Right, displayCutout.Right),
- Math.Max(systemBars.Top, displayCutout.Top),
- Math.Max(systemBars.Bottom, displayCutout.Bottom)
+ Math.Max(systemBars.Left, displayCutout.Left) / context.GetDisplayDensity(),
+ Math.Max(systemBars.Right, displayCutout.Right) / context.GetDisplayDensity(),
+ Math.Max(systemBars.Top, displayCutout.Top) / context.GetDisplayDensity(),
+ Math.Max(systemBars.Bottom, displayCutout.Bottom) / context.GetDisplayDensity()
);
}
- public static SafeAreaPadding ToSafeAreaInsetsWithKeyboard(this WindowInsetsCompat insets)
+ public static SafeAreaPadding ToSafeAreaInsetsWithKeyboard(this WindowInsetsCompat insets, Context context)
{
// Get base safe area insets
- var safeArea = insets.ToSafeAreaInsets();
+ var safeArea = insets.ToSafeAreaInsets(context);
// Get keyboard insets if available (API 30+)
var keyboard = insets.GetInsets(WindowInsetsCompat.Type.Ime());
// For keyboard, we only care about the bottom inset and take the maximum
+ // Convert from pixels to device-independent units
return new(
safeArea.Left,
safeArea.Right,
safeArea.Top,
- Math.Max(safeArea.Bottom, keyboard.Bottom)
+ Math.Max(safeArea.Bottom, keyboard.Bottom / context.GetDisplayDensity())
);
}
- public static SafeAreaPadding GetKeyboardInsets(this WindowInsetsCompat insets)
+ public static SafeAreaPadding GetKeyboardInsets(this WindowInsetsCompat insets, Context context)
{
// Get keyboard insets if available (API 30+)
var keyboard = insets.GetInsets(WindowInsetsCompat.Type.Ime());
// Return only keyboard insets (typically only bottom)
- return new(0, 0, 0, keyboard.Bottom);
+ // Convert from pixels to device-independent units
+ return new(0, 0, 0, keyboard.Bottom / context.GetDisplayDensity());
}
}
\ No newline at end of file
From a75ab7319e1b1e1db6e4175a28af7c504463c496 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 Aug 2025 20:27:49 +0000
Subject: [PATCH 05/10] Implement window insets consumption to prevent
propagation to child views
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
---
.../src/Platform/Android/ContentViewGroup.cs | 46 ++++++++++++++++++-
.../src/Platform/Android/LayoutViewGroup.cs | 46 ++++++++++++++++++-
2 files changed, 90 insertions(+), 2 deletions(-)
diff --git a/src/Core/src/Platform/Android/ContentViewGroup.cs b/src/Core/src/Platform/Android/ContentViewGroup.cs
index 6b745d98ae5d..c4ef91926034 100644
--- a/src/Core/src/Platform/Android/ContentViewGroup.cs
+++ b/src/Core/src/Platform/Android/ContentViewGroup.cs
@@ -55,10 +55,54 @@ void SetupWindowInsetsHandling()
{
_safeAreaInvalidated = true;
RequestLayout();
- return insets;
+ return ConsumeInsets(insets);
});
}
+ WindowInsetsCompat ConsumeInsets(WindowInsetsCompat insets)
+ {
+ if (!RespondsToSafeArea())
+ return insets;
+
+ // Get the types of insets that we handle based on SafeAreaRegions
+ var systemBarsToConsume = WindowInsetsCompat.Type.SystemBars();
+ var displayCutoutToConsume = WindowInsetsCompat.Type.DisplayCutout();
+ var imeToConsume = WindowInsetsCompat.Type.Ime();
+
+ // Check which edges we're handling and should consume insets for
+ var leftRegion = GetSafeAreaRegionForEdge(0);
+ var topRegion = GetSafeAreaRegionForEdge(1);
+ var rightRegion = GetSafeAreaRegionForEdge(2);
+ var bottomRegion = GetSafeAreaRegionForEdge(3);
+
+ // Only consume insets for edges that are NOT None (i.e., where we apply safe area)
+ var shouldConsumeSystemBars = leftRegion != SafeAreaRegions.None || topRegion != SafeAreaRegions.None ||
+ rightRegion != SafeAreaRegions.None || bottomRegion != SafeAreaRegions.None;
+
+ var shouldConsumeCutout = shouldConsumeSystemBars; // Cutouts typically go with system bars
+
+ // Only consume IME insets if bottom edge has SoftInput region
+ var shouldConsumeIme = SafeAreaEdges.IsSoftInput(bottomRegion);
+
+ // Build the inset types to consume
+ var typesToConsume = 0;
+ if (shouldConsumeSystemBars)
+ typesToConsume |= systemBarsToConsume;
+ if (shouldConsumeCutout)
+ typesToConsume |= displayCutoutToConsume;
+ if (shouldConsumeIme)
+ typesToConsume |= imeToConsume;
+
+ // If we don't consume any insets, return original insets
+ if (typesToConsume == 0)
+ return insets;
+
+ // Consume the insets we handle and return the remaining insets
+ return new WindowInsetsCompat.Builder(insets)
+ .SetInsets(typesToConsume, AndroidX.Core.Graphics.Insets.None)
+ .Build();
+ }
+
public ICrossPlatformLayout? CrossPlatformLayout
{
get; set;
diff --git a/src/Core/src/Platform/Android/LayoutViewGroup.cs b/src/Core/src/Platform/Android/LayoutViewGroup.cs
index 3e0f3dc7560b..a4243e164f67 100644
--- a/src/Core/src/Platform/Android/LayoutViewGroup.cs
+++ b/src/Core/src/Platform/Android/LayoutViewGroup.cs
@@ -59,10 +59,54 @@ void SetupWindowInsetsHandling()
{
_safeAreaInvalidated = true;
RequestLayout();
- return insets;
+ return ConsumeInsets(insets);
});
}
+ WindowInsetsCompat ConsumeInsets(WindowInsetsCompat insets)
+ {
+ if (!RespondsToSafeArea())
+ return insets;
+
+ // Get the types of insets that we handle based on SafeAreaRegions
+ var systemBarsToConsume = WindowInsetsCompat.Type.SystemBars();
+ var displayCutoutToConsume = WindowInsetsCompat.Type.DisplayCutout();
+ var imeToConsume = WindowInsetsCompat.Type.Ime();
+
+ // Check which edges we're handling and should consume insets for
+ var leftRegion = GetSafeAreaRegionForEdge(0);
+ var topRegion = GetSafeAreaRegionForEdge(1);
+ var rightRegion = GetSafeAreaRegionForEdge(2);
+ var bottomRegion = GetSafeAreaRegionForEdge(3);
+
+ // Only consume insets for edges that are NOT None (i.e., where we apply safe area)
+ var shouldConsumeSystemBars = leftRegion != SafeAreaRegions.None || topRegion != SafeAreaRegions.None ||
+ rightRegion != SafeAreaRegions.None || bottomRegion != SafeAreaRegions.None;
+
+ var shouldConsumeCutout = shouldConsumeSystemBars; // Cutouts typically go with system bars
+
+ // Only consume IME insets if bottom edge has SoftInput region
+ var shouldConsumeIme = SafeAreaEdges.IsSoftInput(bottomRegion);
+
+ // Build the inset types to consume
+ var typesToConsume = 0;
+ if (shouldConsumeSystemBars)
+ typesToConsume |= systemBarsToConsume;
+ if (shouldConsumeCutout)
+ typesToConsume |= displayCutoutToConsume;
+ if (shouldConsumeIme)
+ typesToConsume |= imeToConsume;
+
+ // If we don't consume any insets, return original insets
+ if (typesToConsume == 0)
+ return insets;
+
+ // Consume the insets we handle and return the remaining insets
+ return new WindowInsetsCompat.Builder(insets)
+ .SetInsets(typesToConsume, AndroidX.Core.Graphics.Insets.None)
+ .Build();
+ }
+
public bool ClipsToBounds { get; set; }
public ICrossPlatformLayout? CrossPlatformLayout
From 432559b702a47a7677c82480fd0c35f88a8dea96 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 Aug 2025 21:12:12 +0000
Subject: [PATCH 06/10] Improve display cutout handling and add edge-to-edge
configuration for Android SafeAreaEdges
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
---
.../src/Platform/Android/ContentViewGroup.cs | 31 ++++++++++++-
.../src/Platform/Android/LayoutViewGroup.cs | 31 ++++++++++++-
.../src/Platform/Android/SafeAreaPadding.cs | 46 +++++++++++++++++--
3 files changed, 101 insertions(+), 7 deletions(-)
diff --git a/src/Core/src/Platform/Android/ContentViewGroup.cs b/src/Core/src/Platform/Android/ContentViewGroup.cs
index c4ef91926034..25d07db5bd01 100644
--- a/src/Core/src/Platform/Android/ContentViewGroup.cs
+++ b/src/Core/src/Platform/Android/ContentViewGroup.cs
@@ -51,6 +51,9 @@ public ContentViewGroup(Context context, IAttributeSet attrs, int defStyleAttr,
void SetupWindowInsetsHandling()
{
+ // Ensure the window is configured for edge-to-edge and display cutout handling
+ EnsureEdgeToEdgeConfiguration();
+
ViewCompat.SetOnApplyWindowInsetsListener(this, (view, insets) =>
{
_safeAreaInvalidated = true;
@@ -59,6 +62,25 @@ void SetupWindowInsetsHandling()
});
}
+ void EnsureEdgeToEdgeConfiguration()
+ {
+ // Try to ensure the window is configured for proper edge-to-edge and display cutout handling
+ try
+ {
+ var activity = _context.GetActivity();
+ if (activity?.Window != null && OperatingSystem.IsAndroidVersionAtLeast(30))
+ {
+ // For API 30+, ensure edge-to-edge configuration
+ AndroidX.Core.View.WindowCompat.SetDecorFitsSystemWindows(activity.Window, false);
+ }
+ }
+ catch (Exception ex)
+ {
+ // Log but don't crash if we can't configure the window
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Failed to configure edge-to-edge mode: {ex.Message}");
+ }
+ }
+
WindowInsetsCompat ConsumeInsets(WindowInsetsCompat insets)
{
if (!RespondsToSafeArea())
@@ -187,9 +209,14 @@ SafeAreaPadding GetAdjustedSafeAreaInsets()
if (windowInsets == null)
return SafeAreaPadding.Empty;
+ // Debug output to help troubleshoot display cutout issues
+ System.Diagnostics.Debug.WriteLine($"SafeArea Debug: {windowInsets.GetInsetsDebugInfo(_context)}");
+
var baseSafeArea = windowInsets.ToSafeAreaInsets(_context);
var keyboardInsets = windowInsets.GetKeyboardInsets(_context);
+ System.Diagnostics.Debug.WriteLine($"SafeArea Calculated: L={baseSafeArea.Left}, T={baseSafeArea.Top}, R={baseSafeArea.Right}, B={baseSafeArea.Bottom}");
+
// Apply safe area selectively per edge based on SafeAreaRegions
if (CrossPlatformLayout is ISafeAreaView2)
{
@@ -198,7 +225,9 @@ SafeAreaPadding GetAdjustedSafeAreaInsets()
var right = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(2), baseSafeArea.Right, keyboardInsets.Right);
var bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3), baseSafeArea.Bottom, keyboardInsets.Bottom);
- return new SafeAreaPadding(left, right, top, bottom);
+ var result = new SafeAreaPadding(left, right, top, bottom);
+ System.Diagnostics.Debug.WriteLine($"SafeArea Applied: L={result.Left}, T={result.Top}, R={result.Right}, B={result.Bottom}");
+ return result;
}
// Legacy ISafeAreaView handling
diff --git a/src/Core/src/Platform/Android/LayoutViewGroup.cs b/src/Core/src/Platform/Android/LayoutViewGroup.cs
index a4243e164f67..a38af522006e 100644
--- a/src/Core/src/Platform/Android/LayoutViewGroup.cs
+++ b/src/Core/src/Platform/Android/LayoutViewGroup.cs
@@ -55,6 +55,9 @@ public LayoutViewGroup(Context context, IAttributeSet attrs, int defStyleAttr, i
void SetupWindowInsetsHandling()
{
+ // Ensure the window is configured for edge-to-edge and display cutout handling
+ EnsureEdgeToEdgeConfiguration();
+
ViewCompat.SetOnApplyWindowInsetsListener(this, (view, insets) =>
{
_safeAreaInvalidated = true;
@@ -63,6 +66,25 @@ void SetupWindowInsetsHandling()
});
}
+ void EnsureEdgeToEdgeConfiguration()
+ {
+ // Try to ensure the window is configured for proper edge-to-edge and display cutout handling
+ try
+ {
+ var activity = _context.GetActivity();
+ if (activity?.Window != null && OperatingSystem.IsAndroidVersionAtLeast(30))
+ {
+ // For API 30+, ensure edge-to-edge configuration
+ AndroidX.Core.View.WindowCompat.SetDecorFitsSystemWindows(activity.Window, false);
+ }
+ }
+ catch (Exception ex)
+ {
+ // Log but don't crash if we can't configure the window
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Failed to configure edge-to-edge mode: {ex.Message}");
+ }
+ }
+
WindowInsetsCompat ConsumeInsets(WindowInsetsCompat insets)
{
if (!RespondsToSafeArea())
@@ -193,9 +215,14 @@ SafeAreaPadding GetAdjustedSafeAreaInsets()
if (windowInsets == null)
return SafeAreaPadding.Empty;
+ // Debug output to help troubleshoot display cutout issues
+ System.Diagnostics.Debug.WriteLine($"SafeArea Debug (Layout): {windowInsets.GetInsetsDebugInfo(_context)}");
+
var baseSafeArea = windowInsets.ToSafeAreaInsets(_context);
var keyboardInsets = windowInsets.GetKeyboardInsets(_context);
+ System.Diagnostics.Debug.WriteLine($"SafeArea Calculated (Layout): L={baseSafeArea.Left}, T={baseSafeArea.Top}, R={baseSafeArea.Right}, B={baseSafeArea.Bottom}");
+
// Apply safe area selectively per edge based on SafeAreaRegions
if (CrossPlatformLayout is ISafeAreaView2)
{
@@ -204,7 +231,9 @@ SafeAreaPadding GetAdjustedSafeAreaInsets()
var right = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(2), baseSafeArea.Right, keyboardInsets.Right);
var bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3), baseSafeArea.Bottom, keyboardInsets.Bottom);
- return new SafeAreaPadding(left, right, top, bottom);
+ var result = new SafeAreaPadding(left, right, top, bottom);
+ System.Diagnostics.Debug.WriteLine($"SafeArea Applied (Layout): L={result.Left}, T={result.Top}, R={result.Right}, B={result.Bottom}");
+ return result;
}
// Legacy ISafeAreaView handling
diff --git a/src/Core/src/Platform/Android/SafeAreaPadding.cs b/src/Core/src/Platform/Android/SafeAreaPadding.cs
index 681063c6d1a4..eb01d4bda235 100644
--- a/src/Core/src/Platform/Android/SafeAreaPadding.cs
+++ b/src/Core/src/Platform/Android/SafeAreaPadding.cs
@@ -56,13 +56,33 @@ public static SafeAreaPadding ToSafeAreaInsets(this WindowInsetsCompat insets, C
// Get display cutout insets if available (API 28+)
var displayCutout = insets.GetInsets(WindowInsetsCompat.Type.DisplayCutout());
- // Combine insets, taking the maximum for each edge
+ // For safe area calculation, we need to ensure content avoids both system bars and display cutouts
+ // Take the maximum of each edge to ensure we avoid both types of obstructions
// Convert from pixels to device-independent units
+ var density = context.GetDisplayDensity();
+
+ // Check if we have valid display cutout insets and ensure they're properly applied
+ // Sometimes display cutout insets might be 0 if the window isn't configured properly
+ var effectiveLeft = Math.Max(systemBars.Left, displayCutout.Left);
+ var effectiveRight = Math.Max(systemBars.Right, displayCutout.Right);
+ var effectiveTop = Math.Max(systemBars.Top, displayCutout.Top);
+ var effectiveBottom = Math.Max(systemBars.Bottom, displayCutout.Bottom);
+
+ // For debugging: Log if we have display cutout insets
+ if (displayCutout.Top > 0 || displayCutout.Bottom > 0 || displayCutout.Left > 0 || displayCutout.Right > 0)
+ {
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Display cutout found - L={displayCutout.Left}, T={displayCutout.Top}, R={displayCutout.Right}, B={displayCutout.Bottom}");
+ }
+ else
+ {
+ System.Diagnostics.Debug.WriteLine("SafeArea: No display cutout insets detected");
+ }
+
return new(
- Math.Max(systemBars.Left, displayCutout.Left) / context.GetDisplayDensity(),
- Math.Max(systemBars.Right, displayCutout.Right) / context.GetDisplayDensity(),
- Math.Max(systemBars.Top, displayCutout.Top) / context.GetDisplayDensity(),
- Math.Max(systemBars.Bottom, displayCutout.Bottom) / context.GetDisplayDensity()
+ effectiveLeft / density,
+ effectiveRight / density,
+ effectiveTop / density,
+ effectiveBottom / density
);
}
@@ -93,4 +113,20 @@ public static SafeAreaPadding GetKeyboardInsets(this WindowInsetsCompat insets,
// Convert from pixels to device-independent units
return new(0, 0, 0, keyboard.Bottom / context.GetDisplayDensity());
}
+
+ ///
+ /// Debug method to get detailed insets information for troubleshooting display cutout issues
+ ///
+ public static string GetInsetsDebugInfo(this WindowInsetsCompat insets, Context context)
+ {
+ var density = context.GetDisplayDensity();
+ var systemBars = insets.GetInsets(WindowInsetsCompat.Type.SystemBars());
+ var displayCutout = insets.GetInsets(WindowInsetsCompat.Type.DisplayCutout());
+ var keyboard = insets.GetInsets(WindowInsetsCompat.Type.Ime());
+
+ return $"SystemBars: L={systemBars.Left}, T={systemBars.Top}, R={systemBars.Right}, B={systemBars.Bottom} | " +
+ $"DisplayCutout: L={displayCutout.Left}, T={displayCutout.Top}, R={displayCutout.Right}, B={displayCutout.Bottom} | " +
+ $"IME: L={keyboard.Left}, T={keyboard.Top}, R={keyboard.Right}, B={keyboard.Bottom} | " +
+ $"Density: {density}";
+ }
}
\ No newline at end of file
From 8056b721d6b703d4bcecf69b46d9217cb933e05f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 Aug 2025 22:35:54 +0000
Subject: [PATCH 07/10] Apply safe area insets to Shell AppBarLayout on Android
to prevent content from going behind notches
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
---
.../Shell/Android/ShellContentFragment.cs | 82 +++++++++++++++++
.../src/Handlers/View/ViewHandler.Android.cs | 92 +++++++++++++++++++
2 files changed, 174 insertions(+)
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs
index 6caba76ae0de..9546b28ddaa3 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs
@@ -6,8 +6,10 @@
using Android.Views.Animations;
using AndroidX.AppCompat.Widget;
using AndroidX.CoordinatorLayout.Widget;
+using AndroidX.Core.View;
using AndroidX.Fragment.App;
using Google.Android.Material.AppBar;
+using Microsoft.Maui.Platform;
using AndroidAnimation = Android.Views.Animations.Animation;
using AnimationSet = Android.Views.Animations.AnimationSet;
using AToolbar = AndroidX.AppCompat.Widget.Toolbar;
@@ -142,6 +144,9 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container,
var appBar = _root.FindViewById(Resource.Id.shellcontent_appbar);
appBar.AddView(_toolbar);
+
+ // Apply safe area insets to AppBarLayout to prevent content from going behind cutouts/notch
+ SetupAppBarSafeAreaHandling(appBar);
_viewhandler = _page.ToHandler(shellContentMauiContext);
_shellPageContainer = new ShellPageContainer(Context, _viewhandler);
@@ -165,6 +170,83 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container,
return _root;
}
+ void SetupAppBarSafeAreaHandling(AppBarLayout appBar)
+ {
+ if (appBar == null || Context == null)
+ return;
+
+ // Ensure edge-to-edge configuration for proper cutout detection
+ EnsureEdgeToEdgeConfiguration();
+
+ // Set up WindowInsets listener for the AppBarLayout
+ ViewCompat.SetOnApplyWindowInsetsListener(appBar, (view, insets) =>
+ {
+ ApplySafeAreaToAppBar(appBar, insets);
+ // Don't consume insets here - let them propagate to child views
+ return insets;
+ });
+
+ // Initial application if insets are already available
+ var rootView = appBar.RootView;
+ if (rootView != null)
+ {
+ var windowInsets = ViewCompat.GetRootWindowInsets(rootView);
+ if (windowInsets != null)
+ {
+ ApplySafeAreaToAppBar(appBar, windowInsets);
+ }
+ }
+ }
+
+ void EnsureEdgeToEdgeConfiguration()
+ {
+ try
+ {
+ var activity = Context.GetActivity();
+ if (activity?.Window != null && OperatingSystem.IsAndroidVersionAtLeast(30))
+ {
+ // For API 30+, ensure edge-to-edge configuration for proper cutout detection
+ AndroidX.Core.View.WindowCompat.SetDecorFitsSystemWindows(activity.Window, false);
+ }
+ }
+ catch (Exception ex)
+ {
+ // Log but don't crash if we can't configure the window
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Failed to configure edge-to-edge mode: {ex.Message}");
+ }
+ }
+
+ void ApplySafeAreaToAppBar(AppBarLayout appBar, WindowInsetsCompat insets)
+ {
+ if (appBar == null || Context == null)
+ return;
+
+ try
+ {
+ // Get safe area insets including display cutouts
+ var safeArea = insets.ToSafeAreaInsets(Context);
+
+ // Apply top safe area inset as padding to push content down from notch/cutout
+ // Convert to pixels for Android view padding
+ var topPaddingPx = (int)(safeArea.Top * Context.GetDisplayDensity());
+
+ // Apply padding to the AppBarLayout to avoid cutout areas
+ // Preserve existing left/right/bottom padding if any
+ appBar.SetPadding(
+ appBar.PaddingLeft,
+ topPaddingPx,
+ appBar.PaddingRight,
+ appBar.PaddingBottom
+ );
+
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Applied AppBar top padding: {topPaddingPx}px (from {safeArea.Top} dip)");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Failed to apply safe area to AppBar: {ex.Message}");
+ }
+ }
+
void Destroy()
{
// If the user taps very quickly on back button multiple times to pop a page,
diff --git a/src/Core/src/Handlers/View/ViewHandler.Android.cs b/src/Core/src/Handlers/View/ViewHandler.Android.cs
index 0430d9cc3de5..746e9cb2a8ee 100644
--- a/src/Core/src/Handlers/View/ViewHandler.Android.cs
+++ b/src/Core/src/Handlers/View/ViewHandler.Android.cs
@@ -1,6 +1,8 @@
using System;
+using Android.Content;
using Android.Views;
using AndroidX.Core.View;
+using Microsoft.Maui.Platform;
using PlatformView = Android.Views.View;
namespace Microsoft.Maui.Handlers
@@ -236,6 +238,12 @@ internal static void MapToolbar(IElementHandler handler, IToolbarElement te)
return;
}
+ // Apply safe area handling to the navigation AppBarLayout if it's an AppBarLayout
+ if (appbarLayout is Google.Android.Material.AppBar.AppBarLayout navAppBar)
+ {
+ SetupNavigationAppBarSafeArea(navAppBar, handler.MauiContext?.Context);
+ }
+
if (appbarLayout.ChildCount > 0 &&
appbarLayout.GetChildAt(0) == nativeToolBar)
{
@@ -256,5 +264,89 @@ void OnPlatformViewFocusChange(object? sender, PlatformView.FocusChangeEventArgs
VirtualView.IsFocused = e.HasFocus;
}
}
+
+ static void SetupNavigationAppBarSafeArea(Google.Android.Material.AppBar.AppBarLayout appBarLayout, Context? context)
+ {
+ if (appBarLayout == null || context == null)
+ return;
+
+ // Track if we've already set up safe area handling for this AppBarLayout
+ var tag = appBarLayout.GetTag(Resource.Id.navigationlayout_appbar);
+ if (tag?.ToString() == "SafeAreaSetup")
+ return;
+
+ appBarLayout.SetTag(Resource.Id.navigationlayout_appbar, "SafeAreaSetup");
+
+ // Ensure edge-to-edge configuration for proper cutout detection
+ EnsureEdgeToEdgeConfigurationForAppBar(context);
+
+ // Set up WindowInsets listener for the AppBarLayout
+ ViewCompat.SetOnApplyWindowInsetsListener(appBarLayout, (view, insets) =>
+ {
+ ApplySafeAreaToNavigationAppBar(appBarLayout, insets, context);
+ // Don't consume insets here - let them propagate to child views
+ return insets;
+ });
+
+ // Initial application if insets are already available
+ var rootView = appBarLayout.RootView;
+ if (rootView != null)
+ {
+ var windowInsets = ViewCompat.GetRootWindowInsets(rootView);
+ if (windowInsets != null)
+ {
+ ApplySafeAreaToNavigationAppBar(appBarLayout, windowInsets, context);
+ }
+ }
+ }
+
+ static void EnsureEdgeToEdgeConfigurationForAppBar(Context context)
+ {
+ try
+ {
+ var activity = context.GetActivity();
+ if (activity?.Window != null && OperatingSystem.IsAndroidVersionAtLeast(30))
+ {
+ // For API 30+, ensure edge-to-edge configuration for proper cutout detection
+ AndroidX.Core.View.WindowCompat.SetDecorFitsSystemWindows(activity.Window, false);
+ }
+ }
+ catch (Exception ex)
+ {
+ // Log but don't crash if we can't configure the window
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Failed to configure edge-to-edge mode for AppBar: {ex.Message}");
+ }
+ }
+
+ static void ApplySafeAreaToNavigationAppBar(Google.Android.Material.AppBar.AppBarLayout appBarLayout, WindowInsetsCompat insets, Context context)
+ {
+ if (appBarLayout == null || context == null)
+ return;
+
+ try
+ {
+ // Get safe area insets including display cutouts
+ var safeArea = insets.ToSafeAreaInsets(context);
+
+ // Apply top safe area inset as padding to push content down from notch/cutout
+ // Convert to pixels for Android view padding
+ var topPaddingPx = (int)(safeArea.Top * context.GetDisplayDensity());
+
+ // Apply padding to the AppBarLayout to avoid cutout areas
+ // Preserve existing left/right/bottom padding if any
+ appBarLayout.SetPadding(
+ appBarLayout.PaddingLeft,
+ topPaddingPx,
+ appBarLayout.PaddingRight,
+ appBarLayout.PaddingBottom
+ );
+
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Applied Navigation AppBar top padding: {topPaddingPx}px (from {safeArea.Top} dip)");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Failed to apply safe area to Navigation AppBar: {ex.Message}");
+ }
+ }
}
}
\ No newline at end of file
From aa8aedf432d63f7af7a2d8f408f1b7ca31298722 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 Aug 2025 22:56:31 +0000
Subject: [PATCH 08/10] Add safe area handling to NavigationRootManager
AppBarLayout
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
---
.../Navigation/NavigationRootManager.cs | 93 +++++++++++++++++++
1 file changed, 93 insertions(+)
diff --git a/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs b/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs
index c5835d7a546d..c7ef565d6c41 100644
--- a/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs
+++ b/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs
@@ -4,9 +4,11 @@
using Android.Views;
using AndroidX.AppCompat.Widget;
using AndroidX.CoordinatorLayout.Widget;
+using AndroidX.Core.View;
using AndroidX.DrawerLayout.Widget;
using AndroidX.Fragment.App;
using Google.Android.Material.AppBar;
+using Microsoft.Maui.Platform;
using AView = Android.Views.View;
namespace Microsoft.Maui.Platform
@@ -74,6 +76,9 @@ internal void Connect(IView? view, IMauiContext? mauiContext = null)
.JavaCast();
_rootView = navigationLayout;
+
+ // Apply safe area insets to the navigation AppBarLayout to prevent content from going behind cutouts/notch
+ SetupNavigationAppBarSafeArea(navigationLayout);
}
// if the incoming view is a Drawer Layout then the Drawer Layout
@@ -180,6 +185,94 @@ void SetContentView(IView? view)
}
}
+ void SetupNavigationAppBarSafeArea(CoordinatorLayout navigationLayout)
+ {
+ if (navigationLayout == null || _mauiContext?.Context == null)
+ return;
+
+ var appBarLayout = navigationLayout.FindViewById(Resource.Id.navigationlayout_appbar);
+ if (appBarLayout == null)
+ return;
+
+ // Track if we've already set up safe area handling for this AppBarLayout
+ var tag = appBarLayout.GetTag(Resource.Id.navigationlayout_appbar);
+ if (tag?.ToString() == "SafeAreaSetup")
+ return;
+
+ appBarLayout.SetTag(Resource.Id.navigationlayout_appbar, "SafeAreaSetup");
+
+ // Ensure edge-to-edge configuration for proper cutout detection
+ EnsureEdgeToEdgeConfiguration();
+
+ // Set up WindowInsets listener for the AppBarLayout
+ ViewCompat.SetOnApplyWindowInsetsListener(appBarLayout, (view, insets) =>
+ {
+ ApplySafeAreaToNavigationAppBar(appBarLayout, insets);
+ // Don't consume insets here - let them propagate to child views
+ return insets;
+ });
+
+ // Initial application if insets are already available
+ var rootView = appBarLayout.RootView;
+ if (rootView != null)
+ {
+ var windowInsets = ViewCompat.GetRootWindowInsets(rootView);
+ if (windowInsets != null)
+ {
+ ApplySafeAreaToNavigationAppBar(appBarLayout, windowInsets);
+ }
+ }
+ }
+
+ void EnsureEdgeToEdgeConfiguration()
+ {
+ try
+ {
+ var activity = _mauiContext?.Context?.GetActivity();
+ if (activity?.Window != null && OperatingSystem.IsAndroidVersionAtLeast(30))
+ {
+ // For API 30+, ensure edge-to-edge configuration for proper cutout detection
+ AndroidX.Core.View.WindowCompat.SetDecorFitsSystemWindows(activity.Window, false);
+ }
+ }
+ catch (Exception ex)
+ {
+ // Log but don't crash if we can't configure the window
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Failed to configure edge-to-edge mode for NavigationRootManager: {ex.Message}");
+ }
+ }
+
+ void ApplySafeAreaToNavigationAppBar(AppBarLayout appBarLayout, WindowInsetsCompat insets)
+ {
+ if (appBarLayout == null || _mauiContext?.Context == null)
+ return;
+
+ try
+ {
+ // Get safe area insets including display cutouts
+ var safeArea = insets.ToSafeAreaInsets(_mauiContext.Context);
+
+ // Apply top safe area inset as padding to push content down from notch/cutout
+ // Convert to pixels for Android view padding
+ var topPaddingPx = (int)(safeArea.Top * _mauiContext.Context.GetDisplayDensity());
+
+ // Apply padding to the AppBarLayout to avoid cutout areas
+ // Preserve existing left/right/bottom padding if any
+ appBarLayout.SetPadding(
+ appBarLayout.PaddingLeft,
+ topPaddingPx,
+ appBarLayout.PaddingRight,
+ appBarLayout.PaddingBottom
+ );
+
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Applied NavigationRootManager AppBar top padding: {topPaddingPx}px (from {safeArea.Top} dip)");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"SafeArea: Failed to apply safe area to NavigationRootManager AppBar: {ex.Message}");
+ }
+ }
+
class ElementBasedFragment : ScopedFragment
{
public ElementBasedFragment(
From 0d5762e0185bd0b6fea1fe3266a8a607a0acde2b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 14 Aug 2025 23:09:25 +0000
Subject: [PATCH 09/10] Fix compilation errors by adding missing
Android.Content using statements
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
---
.../Compatibility/Handlers/Shell/Android/ShellContentFragment.cs | 1 +
.../src/Platform/Android/Navigation/NavigationRootManager.cs | 1 +
2 files changed, 2 insertions(+)
diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs
index 9546b28ddaa3..a2a08368af3b 100644
--- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs
+++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellContentFragment.cs
@@ -1,5 +1,6 @@
#nullable disable
using System;
+using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
diff --git a/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs b/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs
index c7ef565d6c41..d59f99a04509 100644
--- a/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs
+++ b/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs
@@ -1,4 +1,5 @@
using System;
+using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
From cb703c3faebff8f65e72ad36d54f7ad8d0b0555a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 15 Aug 2025 01:53:41 +0000
Subject: [PATCH 10/10] Update copilot instructions to require Android TFM
build validation
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
---
.github/copilot-instructions.md | 63 +++++++++++++++++++++++++++++++++
1 file changed, 63 insertions(+)
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 6545e1234b7d..2be3c06f13e1 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -88,6 +88,29 @@ For .NET installation on Linux, follow the official Microsoft documentation:
## Development Workflow
+### Pre-Commit Validation Requirements
+
+**MANDATORY: All platform-specific changes must be validated before committing.**
+
+For Android platform code changes:
+1. **Compile Android TFM**: Always test `net10.0-android36.0` and similar Android target frameworks
+2. **Verify Android references**: Ensure `using Android.*` statements compile correctly
+3. **Test platform APIs**: Confirm Android-specific extension methods work as expected
+
+Example validation commands:
+```bash
+# Core Android TFM validation
+dotnet build src/Core/src/Core.csproj --framework net10.0-android36.0
+
+# Controls Android TFM validation
+dotnet build src/Controls/src/Core/Controls.Core.csproj --framework net10.0-android36.0
+
+# Full BuildTasks validation
+dotnet build ./Microsoft.Maui.BuildTasks.slnf
+```
+
+**Do not skip Android TFM compilation testing** - Android workloads are available and functional in this environment.
+
### Building
#### Using Cake (Recommended)
@@ -99,6 +122,29 @@ dotnet cake
dotnet cake --target=dotnet-pack
```
+#### Android Target Framework Validation
+
+**CRITICAL: Always validate Android TFM builds before committing changes.**
+
+The Android workloads are properly installed and available in this environment. When making code changes that affect Android platform code, always verify that Android target frameworks compile successfully:
+
+```bash
+# Verify Android TFM compilation for specific projects
+dotnet build src/Core/src/Core.csproj --framework net10.0-android36.0
+dotnet build src/Controls/src/Core/Controls.Core.csproj --framework net10.0-android36.0
+
+# Build BuildTasks to ensure foundational compilation works
+dotnet build ./Microsoft.Maui.BuildTasks.slnf
+```
+
+**Required Android TFM Build Verification:**
+- Test Android-specific target frameworks (e.g., `net10.0-android36.0`) when modifying Android platform code
+- Verify that `using Android.*` statements compile correctly
+- Ensure Android-specific extension methods and APIs are properly referenced
+- Confirm that Android workload dependencies are resolved
+
+**Do not commit Android platform changes without verifying Android TFM compilation success.**
+
### Testing and Debugging
#### Testing Guidelines
@@ -184,6 +230,23 @@ For compatibility with specific branches:
- Install missing Android SDKs via [Android SDK Manager](https://learn.microsoft.com/xamarin/android/get-started/installation/android-sdk)
- Android SDK Manager available via: `android` command (after dotnet tool restore)
+#### Android TFM Build Requirements
+**Android workloads are installed and functional.** When working on Android platform code:
+
+- **Always test Android TFM compilation** before committing changes
+- **Verify Android-specific target frameworks** like `net10.0-android36.0` compile successfully
+- **Test Android platform references** including `using Android.*` namespaces
+- **Validate Android extension methods** and platform-specific APIs work correctly
+
+Build verification commands:
+```bash
+# Test Android TFM for Core components
+dotnet build src/Core/src/Core.csproj --framework net10.0-android36.0
+
+# Test Android TFM for Controls
+dotnet build src/Controls/src/Core/Controls.Core.csproj --framework net10.0-android36.0
+```
+
### iOS (requires macOS)
- Requires current stable Xcode installation from [App Store](https://apps.apple.com/us/app/xcode/id497799835?mt=12) or [Apple Developer portal](https://developer.apple.com/download/more/?name=Xcode)
- Pair to Mac required when developing on Windows