diff --git a/src/Controls/src/Core/ListView/ListView.cs b/src/Controls/src/Core/ListView/ListView.cs
index c85b9389e156..0834247786b8 100644
--- a/src/Controls/src/Core/ListView/ListView.cs
+++ b/src/Controls/src/Core/ListView/ListView.cs
@@ -17,7 +17,7 @@ namespace Microsoft.Maui.Controls
{
/// An that displays a collection of data as a vertical list.
[Obsolete("ListView is deprecated. Please use CollectionView instead.")]
- public class ListView : ItemsView, IListViewController, IElementConfiguration, IVisualTreeElement
+ public class ListView : ItemsView, IListViewController, IElementConfiguration, IVisualTreeElement, ISafeAreaIgnoredContainer
{
// The ListViewRenderer has some odd behavior with LogicalChildren
// https://github.com/xamarin/Xamarin.Forms/pull/12057
diff --git a/src/Controls/src/Core/TableView/TableView.cs b/src/Controls/src/Core/TableView/TableView.cs
index b20b11f41ebf..a1fed3f10b6d 100644
--- a/src/Controls/src/Core/TableView/TableView.cs
+++ b/src/Controls/src/Core/TableView/TableView.cs
@@ -14,7 +14,7 @@ namespace Microsoft.Maui.Controls
///
[Obsolete("Please use CollectionView instead.")]
[ContentProperty(nameof(Root))]
- public class TableView : View, ITableViewController, IElementConfiguration, IVisualTreeElement
+ public class TableView : View, ITableViewController, IElementConfiguration, ISafeAreaIgnoredContainer, IVisualTreeElement
{
/// Bindable property for .
public static readonly BindableProperty RowHeightProperty = BindableProperty.Create(nameof(RowHeight), typeof(int), typeof(TableView), -1);
diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageDoesNotDisappearWhenSwitchingTab.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageDoesNotDisappearWhenSwitchingTab.png
index 4ae7c76f7516..9f76af5a6874 100644
Binary files a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageDoesNotDisappearWhenSwitchingTab.png and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/ImageDoesNotDisappearWhenSwitchingTab.png differ
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla32040.cs b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla32040.cs
index c043c278fe28..fe829df80c4f 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla32040.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla32040.cs
@@ -6,6 +6,8 @@ public class Bugzilla32040 : TestContentPage
{
protected override void Init()
{
+ SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
+
var switchCell = new SwitchCell { Text = "blahblah", AutomationId = "blahblah" };
switchCell.Tapped += (s, e) =>
{
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla32206.cs b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla32206.cs
index 5bfdb7ae74bd..a60d0d17e7bc 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla32206.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla32206.cs
@@ -79,6 +79,8 @@ public ContentPage32206()
Interlocked.Increment(ref LandingPage32206.Counter);
System.Diagnostics.Debug.WriteLine("Page: " + LandingPage32206.Counter);
+ SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
+
Content = new ListView
{
ItemsSource = new List { "Apple", "Banana", "Cherry" },
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla33578.cs b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla33578.cs
index 552691589c16..be6f262d2463 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla33578.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla33578.cs
@@ -6,6 +6,7 @@ public class Bugzilla33578 : TestContentPage
{
protected override void Init()
{
+ SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
Content = new TableView
{
AutomationId = "table",
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla43941.cs b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla43941.cs
index 86d8c7394a3c..9a6daff54836 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla43941.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla43941.cs
@@ -18,6 +18,8 @@ public ContentPage43941()
Interlocked.Increment(ref LandingPage43941.Counter);
System.Diagnostics.Debug.WriteLine("Page: " + LandingPage43941.Counter);
+ SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
+
var list = new List();
for (var i = 0; i < 30; i++)
{
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla44338.cs b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla44338.cs
index ce3003f4b2fa..6b91d0ca33ae 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla44338.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla44338.cs
@@ -20,6 +20,8 @@ public string[] Items
protected override void Init()
{
+
+ SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
Content = new ListView
{
ItemsSource = Items,
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla52533.cs b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla52533.cs
index f70da04e4bbd..3be3299d23a4 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla52533.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla52533.cs
@@ -8,6 +8,7 @@ public class Bugzilla52533 : TestContentPage
protected override void Init()
{
+ SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
Content = new ListView { ItemTemplate = new DataTemplate(typeof(GridViewCell)), ItemsSource = Enumerable.Range(0, 10) };
}
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla58875.cs b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla58875.cs
index 1693a255235b..d1474e66de51 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla58875.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Bugzilla/Bugzilla58875.cs
@@ -23,6 +23,8 @@ public ListViewPage()
{
BindingContext = this;
+ SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
+
var listView = new ListView(ListViewCachingStrategy.RecycleElement)
{
ItemTemplate = new DataTemplate(() =>
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue32941.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue32941.cs
new file mode 100644
index 000000000000..78e1d63e50c6
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue32941.cs
@@ -0,0 +1,84 @@
+namespace Maui.Controls.Sample.Issues;
+
+[Issue(IssueTracker.Github, 32941, "Label Overlapped by Android Status Bar When Using SafeAreaEdges=Container in .NET MAUI", PlatformAffected.Android)]
+public class Issue32941 : TestShell
+{
+ protected override void Init()
+ {
+ var shellContent1 = new ShellContent
+ {
+ Title = "Home",
+ Route = "MainPage",
+ Content = new Issue32941_MainPage()
+ };
+ var shellContent2 = new ShellContent
+ {
+ Title = "SignOut",
+ Route = "SignOutPage",
+ Content = new Issue32941_SignOutPage()
+ };
+ Items.Add(shellContent1);
+ Items.Add(shellContent2);
+ }
+}
+
+public class Issue32941_MainPage : ContentPage
+{
+ public Issue32941_MainPage()
+ {
+ var goToSignOutButton = new Button
+ {
+ Text = "Go to SignOut",
+ AutomationId = "GoToSignOutButton"
+ };
+ goToSignOutButton.Clicked += async (s, e) => await Shell.Current.GoToAsync("//SignOutPage", false);
+
+ Content = new VerticalStackLayout
+ {
+ Spacing = 20,
+ Padding = new Thickness(20),
+ Children =
+ {
+ new Label
+ {
+ Text = "Main Page",
+ FontSize = 24,
+ AutomationId = "MainPageLabel"
+ },
+ goToSignOutButton
+ }
+ };
+ }
+}
+
+public class Issue32941_SignOutPage : ContentPage
+{
+ public Issue32941_SignOutPage()
+ {
+ Shell.SetNavBarIsVisible(this, false);
+ SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
+
+ var backButton = new Button
+ {
+ Text = "Back to Main",
+ AutomationId = "BackButton"
+ };
+ backButton.Clicked += async (s, e) => await Shell.Current.GoToAsync("//MainPage", true);
+
+ Content = new VerticalStackLayout
+ {
+ BackgroundColor = Colors.White,
+ Children =
+ {
+ new Label
+ {
+ Text = "SignOut / Session Expiry Page",
+ FontSize = 24,
+ BackgroundColor = Colors.Yellow,
+ AutomationId = "SignOutLabel"
+ },
+ backButton
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33034.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue33034.xaml
new file mode 100644
index 000000000000..fc5c32659318
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33034.xaml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33034.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue33034.xaml.cs
new file mode 100644
index 000000000000..a7296ec89a74
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33034.xaml.cs
@@ -0,0 +1,41 @@
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Maui.Controls.Sample.Issues;
+
+[XamlCompilation(XamlCompilationOptions.Compile)]
+[Issue(IssueTracker.Github, 33034, "SafeAreaEdges works correctly only on the first tab in Shell. Other tabs have content colliding with the display cutout in the landscape mode.", PlatformAffected.Android)]
+public partial class Issue33034 : TestShell
+{
+ public Issue33034()
+ {
+ InitializeComponent();
+ }
+
+ protected override void Init()
+ {
+ // Create TabBar with two tabs using the same content page
+ var tabBar = new TabBar();
+
+ var tab = new Tab { Title = "Tabs" };
+
+ tab.Items.Add(new ShellContent
+ {
+ Title = "First Tab",
+ AutomationId = "FirstTab",
+ ContentTemplate = new DataTemplate(typeof(Issue33034TabContent)),
+ Route = "tab1"
+ });
+
+ tab.Items.Add(new ShellContent
+ {
+ Title = "Second Tab",
+ AutomationId = "SecondTab",
+ ContentTemplate = new DataTemplate(typeof(Issue33034TabContent)),
+ Route = "tab2"
+ });
+
+ tabBar.Items.Add(tab);
+ Items.Add(tabBar);
+ }
+}
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33034TabContent.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issue33034TabContent.xaml
new file mode 100644
index 000000000000..d8d5418a5a1c
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33034TabContent.xaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33034TabContent.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue33034TabContent.xaml.cs
new file mode 100644
index 000000000000..82db89d8063d
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33034TabContent.xaml.cs
@@ -0,0 +1,13 @@
+using Microsoft.Maui.Controls;
+using Microsoft.Maui.Controls.Xaml;
+
+namespace Maui.Controls.Sample.Issues;
+
+[XamlCompilation(XamlCompilationOptions.Compile)]
+public partial class Issue33034TabContent : ContentPage
+{
+ public Issue33034TabContent()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue5924.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue5924.xaml.cs
index 1f33ec61bf54..a2823742e43a 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/Issue5924.xaml.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue5924.xaml.cs
@@ -6,6 +6,7 @@ public partial class Issue5924 : ContentPage
public Issue5924()
{
InitializeComponent();
+ SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
}
}
}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue1028.cs b/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue1028.cs
index fc14de1e74ab..8d95632ea2e3 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue1028.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue1028.cs
@@ -8,6 +8,7 @@ public class Issue1028 : TestContentPage
// Issue1028, ViewCell with StackLayout causes exception when nested in a table section. This occurs when the app's root page is a ContentPage with a TableView.
protected override void Init()
{
+ SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
Content = new TableView
{
Root = new TableRoot("Table Title") {
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue1658.cs b/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue1658.cs
index 238edf1dcb1f..fed0786a6114 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue1658.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue1658.cs
@@ -9,6 +9,8 @@ protected override void Init()
{
var page = new ContentPage();
+ page.SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
+
PushAsync(page);
page.Content = new ListView()
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue6258.cs b/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue6258.cs
index fde7d715e00e..190bdc6b17f3 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue6258.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/Issue6258.cs
@@ -8,6 +8,8 @@ protected override void Init()
{
var page = new ContentPage();
+ page.SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
+
PushAsync(page);
page.Content = new ListView()
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/ShellInsets.cs b/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/ShellInsets.cs
index cd181ca627c0..5207356f1d22 100644
--- a/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/ShellInsets.cs
+++ b/src/Controls/tests/TestCases.HostApp/Issues/XFIssue/ShellInsets.cs
@@ -153,6 +153,8 @@ void ListViewPage()
{
var page = CreateContentPage();
+ page.SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container);
+
page.Content = new Microsoft.Maui.Controls.ListView(ListViewCachingStrategy.RecycleElement)
{
ItemTemplate = new DataTemplate(() =>
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32941.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32941.cs
new file mode 100644
index 000000000000..025cba2aacf0
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32941.cs
@@ -0,0 +1,38 @@
+#if TEST_FAILS_ON_CATALYST && TEST_FAILS_ON_WINDOWS // SafeAreaEdges not supported on Catalyst and Windows
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues;
+
+public class Issue32941 : _IssuesUITest
+{
+ public Issue32941(TestDevice testDevice) : base(testDevice)
+ {
+ }
+
+ public override string Issue => "Label Overlapped by Android Status Bar When Using SafeAreaEdges=Container in .NET MAUI";
+
+ [Test]
+ [Category(UITestCategories.SafeAreaEdges)]
+ public void ShellContentShouldRespectSafeAreaEdges_After_Navigation()
+ {
+ App.WaitForElement("MainPageLabel");
+ App.Tap("GoToSignOutButton");
+ App.WaitForElement("SignOutLabel");
+
+ // Get the position of the label
+ var labelRect = App.FindElement("SignOutLabel").GetRect();
+
+ // The label should be positioned below the status bar (Y coordinate should be > 0)
+ // On Android with notch, status bar is typically 24-88dp depending on device
+ // The label should have adequate top padding from SafeAreaEdges=Container
+ Assert.That(labelRect.Y, Is.GreaterThan(0), "Label should not be at Y=0 (would be under status bar)");
+
+ // Verify the label is not overlapped by checking it has reasonable top spacing
+ // A label at Y < 20 is likely overlapped by the status bar
+ Assert.That(labelRect.Y, Is.GreaterThanOrEqualTo(20),
+ "Label Y position should be at least 20 pixels from top to avoid status bar overlap");
+ }
+}
+#endif
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33034.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33034.cs
new file mode 100644
index 000000000000..c6db39d01b09
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33034.cs
@@ -0,0 +1,30 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues
+{
+ public class Issue33034 : _IssuesUITest
+ {
+ public override string Issue => "SafeAreaEdges works correctly only on the first tab in Shell. Other tabs have content colliding with the display cutout in the landscape mode.";
+
+ public Issue33034(TestDevice device) : base(device) { }
+
+ [Test]
+ [Category(UITestCategories.SafeAreaEdges)]
+ public void SafeAreaShouldWorkOnAllShellTabs()
+ {
+ // Wait for the first tab to load
+ App.WaitForElement("LeftEdgeLabel");
+
+ // Get the X position of the left edge label on the first tab
+ var firstTabLabelRect = App.FindElement("LeftEdgeLabel").GetRect();
+ var firstTabLeftPosition = firstTabLabelRect.X;
+
+ // The label should have proper left padding (safe area inset)
+ // With our SafeArea fix, it should be > 0
+ Assert.That(firstTabLeftPosition, Is.GreaterThan(0),
+ $"Left edge label should have safe area inset on first tab. Position: {firstTabLeftPosition}");
+ }
+ }
+}
diff --git a/src/Core/AndroidNative/build/reports/problems/problems-report.html b/src/Core/AndroidNative/build/reports/problems/problems-report.html
deleted file mode 100644
index 28c86c49448d..000000000000
--- a/src/Core/AndroidNative/build/reports/problems/problems-report.html
+++ /dev/null
@@ -1,663 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Gradle Configuration Cache
-
-
-
-
-
-
- Loading...
-
-
-
-
-
-
-
diff --git a/src/Core/src/Core/ISafeAreaIgnoredContainer.cs b/src/Core/src/Core/ISafeAreaIgnoredContainer.cs
new file mode 100644
index 000000000000..b0d22e7f9c60
--- /dev/null
+++ b/src/Core/src/Core/ISafeAreaIgnoredContainer.cs
@@ -0,0 +1,11 @@
+namespace Microsoft.Maui
+{
+ ///
+ /// Marker interface for containers (like ListView, TableView, ViewCell) that manage their own
+ /// layout and should NOT have safe area insets applied to their content.
+ /// Views inside these containers will not receive safe area padding.
+ ///
+ internal interface ISafeAreaIgnoredContainer
+ {
+ }
+}
diff --git a/src/Core/src/Platform/Android/MauiWindowInsetListener.cs b/src/Core/src/Platform/Android/MauiWindowInsetListener.cs
index 9763ec85ed32..dac1d5556e64 100644
--- a/src/Core/src/Platform/Android/MauiWindowInsetListener.cs
+++ b/src/Core/src/Platform/Android/MauiWindowInsetListener.cs
@@ -120,6 +120,13 @@ internal void RegisterView(AView view)
}
else if (ReferenceEquals(registeredView, parentView))
{
+ // Before returning the listener, check if any ancestor is a safe-area-ignored container
+ // If so, don't apply inset listeners to this view or any of its children
+ if (IsInsideSafeAreaIgnoredContainer(view))
+ {
+ return null;
+ }
+
return entry.Listener;
}
}
@@ -132,13 +139,45 @@ internal void RegisterView(AView view)
}
///
- /// Sets up a view to use this listener for inset handling.
- /// This method registers the view and attaches the listener.
- /// Must be called on UI thread.
- ///
- /// The view to set up
- /// The same view for method chaining
- internal static AView SetupViewWithLocalListener(AView view, MauiWindowInsetListener? listener = null)
+ /// Checks if a view is inside a container that ignores safe area (ListView, TableView).
+ /// These containers manage their own layout and should NOT have safe area insets applied to their content.
+ ///
+ /// The view to check
+ /// True if the view is inside an ISafeAreaIgnoredContainer, false otherwise
+ private static bool IsInsideSafeAreaIgnoredContainer(AView view)
+ {
+ // Walk up from the current view to find its IView representation
+ var currentView = view;
+ while (currentView != null)
+ {
+ if (currentView is IViewHandler handler && handler.VirtualView is IView virtualView)
+ {
+ // Check if any ancestor in the virtual view hierarchy is ISafeAreaIgnoredContainer
+ var ancestor = virtualView.Parent as IView;
+ while (ancestor != null)
+ {
+ if (ancestor is ISafeAreaIgnoredContainer)
+ {
+ return true;
+ }
+ ancestor = ancestor.Parent as IView;
+ }
+ break;
+ }
+ currentView = currentView.Parent as AView;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Sets up a view to use this listener for inset handling.
+ /// This method registers the view and attaches the listener.
+ /// Must be called on UI thread.
+ ///
+ /// The view to set up
+ /// The same view for method chaining
+ internal static AView SetupViewWithLocalListener(AView view, MauiWindowInsetListener? listener = null)
{
listener ??= new MauiWindowInsetListener();
ViewCompat.SetOnApplyWindowInsetsListener(view, listener);
diff --git a/src/Core/src/Platform/Android/SafeAreaExtensions.cs b/src/Core/src/Platform/Android/SafeAreaExtensions.cs
index faf4cbbe06dc..7a3ab28bac69 100644
--- a/src/Core/src/Platform/Android/SafeAreaExtensions.cs
+++ b/src/Core/src/Platform/Android/SafeAreaExtensions.cs
@@ -57,10 +57,10 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
if (safeAreaView2 is not null)
{
// Apply safe area selectively per edge based on SafeAreaRegions
- var left = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(0, layout), baseSafeArea.Left, 0, isKeyboardShowing, keyboardInsets);
- var top = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(1, layout), baseSafeArea.Top, 1, isKeyboardShowing, keyboardInsets);
- var right = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(2, layout), baseSafeArea.Right, 2, isKeyboardShowing, keyboardInsets);
- var bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3, layout), baseSafeArea.Bottom, 3, isKeyboardShowing, keyboardInsets);
+ double left = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(0, layout), baseSafeArea.Left, 0, isKeyboardShowing, keyboardInsets);
+ double top = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(1, layout), baseSafeArea.Top, 1, isKeyboardShowing, keyboardInsets);
+ double right = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(2, layout), baseSafeArea.Right, 2, isKeyboardShowing, keyboardInsets);
+ double bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3, layout), baseSafeArea.Top, 3, isKeyboardShowing, keyboardInsets);
var globalWindowInsetsListener = MauiWindowInsetListener.FindListenerForView(view);
bool hasTrackedViews = globalWindowInsetsListener?.HasTrackedView == true;
@@ -108,6 +108,11 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
if (left == 0 && right == 0 && top == 0 && bottom == 0)
{
view.SetPadding(0, 0, 0, 0);
+ // Untrack view if it was previously tracked since padding is now 0
+ if (globalWindowInsetsListener?.IsViewTracked(view) == true)
+ {
+ globalWindowInsetsListener.ResetView(view);
+ }
return windowInsets;
}
@@ -142,60 +147,85 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
var screenWidth = realMetrics.WidthPixels;
var screenHeight = realMetrics.HeightPixels;
+ // Check if this is a top-level page view that should get full safe area treatment
+ // Top-level pages (e.g., ContentPage as direct Shell content) need full safe area padding
+ // during navigation to prevent content from being overlapped by system UI.
+ // We detect this by checking if the view's virtual view has no parent or if its parent
+ // is a navigation container (Shell, NavigationPage, etc.)
+ bool isTopLevelPage = false;
+ if (safeAreaView2 is IView virtualView)
+ {
+ // Check if this view has no parent (root level) or if parent is a Window/Shell/NavigationPage
+ var parent = virtualView.Parent;
+
+ // View is top-level if its parent is not also requesting safe area
+ // (parent doesn't implement ISafeAreaView2). This means the parent is a container
+ // like Shell, NavigationPage, Window, etc., not another ContentPage.
+ // Exception: If we have no tracked views yet AND the parent IS requesting safe area,
+ // this might be TabbedPage initialization - use position-based logic instead.
+ bool parentRequestsSafeArea = parent != null && GetSafeAreaView2(parent) != null;
+
+ isTopLevelPage = parent == null ||
+ (!parentRequestsSafeArea) ||
+ (parentRequestsSafeArea && hasTrackedViews);
+ }
+
// Calculate actual overlap for each edge
// Top: how much the view extends into the top safe area
// If the viewTop is < 0 that means that it's most likely
// panned off the top of the screen so we don't want to apply any top inset
- if (top > 0 && viewTop < top && viewTop >= 0)
+ if (top > 0 && !isTopLevelPage && viewTop < top && viewTop >= 0)
{
// Calculate the actual overlap amount
top = Math.Min(top - viewTop, top);
}
- else
+ else if (!isTopLevelPage && (viewHeight > 0 || hasTrackedViews))
{
- if (viewHeight > 0 || hasTrackedViews)
- top = 0;
+ // For non-top-level views that don't overlap, reset to 0
+ top = 0;
}
+ // Otherwise keep the inset value (first layout or top-level page)
// Bottom: how much the view extends into the bottom safe area
- if (bottom > 0 && viewBottom > (screenHeight - bottom))
+ if (bottom > 0 && !isTopLevelPage && viewBottom > (screenHeight - bottom))
{
// Calculate the actual overlap amount
var bottomEdge = screenHeight - bottom;
bottom = Math.Min(viewBottom - bottomEdge, bottom);
}
- else
+ else if (!isTopLevelPage && (viewHeight > 0 || hasTrackedViews))
{
- // if the view height is zero because it hasn't done the first pass
- // and we don't have any tracked views yet then we will apply the bottom inset
- if (viewHeight > 0 || hasTrackedViews)
- bottom = 0;
+ // For non-top-level views that don't overlap, reset to 0
+ bottom = 0;
}
+ // Otherwise keep the inset value (first layout or top-level page)
// Left: how much the view extends into the left safe area
- if (left > 0 && viewLeft < left)
+ if (left > 0 && !isTopLevelPage && viewLeft < left)
{
// Calculate the actual overlap amount
left = Math.Min(left - viewLeft, left);
}
- else
+ else if (!isTopLevelPage && (viewWidth > 0 || hasTrackedViews))
{
- if (viewWidth > 0 || hasTrackedViews)
- left = 0;
+ // For non-top-level views that don't overlap, reset to 0
+ left = 0;
}
+ // Otherwise keep the inset value (first layout or top-level page)
// Right: how much the view extends into the right safe area
- if (right > 0 && viewRight > (screenWidth - right))
+ if (right > 0 && !isTopLevelPage && viewRight > (screenWidth - right))
{
// Calculate the actual overlap amount
var rightEdge = screenWidth - right;
right = Math.Min(viewRight - rightEdge, right);
}
- else
+ else if (!isTopLevelPage && (viewWidth > 0 || hasTrackedViews))
{
- if (viewWidth > 0 || hasTrackedViews)
- right = 0;
+ // For non-top-level views that don't overlap, reset to 0
+ right = 0;
}
+ // Otherwise keep the inset value (first layout or top-level page)
}
// Build new window insets with unconsumed values
@@ -261,6 +291,22 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor
return newWindowInsets;
}
+ internal static bool IsInsideSafeAreaIgnoredContainer(IView view)
+ {
+ // Walk up the parent hierarchy to check if this view is inside a container
+ // that implements ISafeAreaIgnoredContainer (ListView, TableView, ViewCell)
+ var parent = view.Parent;
+ while (parent != null)
+ {
+ if (parent is ISafeAreaIgnoredContainer)
+ return true;
+
+ parent = (parent as IView)?.Parent;
+ }
+
+ return false;
+ }
+
internal static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double originalSafeArea, int edge, bool isKeyboardShowing, SafeAreaPadding keyBoardInsets)
{
// Edge-to-edge content - no safe area padding