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