diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LayoutShouldBeCorrectOnFirstNavigation.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LayoutShouldBeCorrectOnFirstNavigation.png new file mode 100644 index 000000000000..94c9b3f38387 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/LayoutShouldBeCorrectOnFirstNavigation.png differ 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..d3a599e0a1b7 --- /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 + } + }; + } +} diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33034.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue33034.cs new file mode 100644 index 000000000000..509e349f82b1 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33034.cs @@ -0,0 +1,55 @@ +namespace Maui.Controls.Sample.Issues; + +[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 class Issue33034 : TestShell +{ + protected override void Init() + { + 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); + } +} + +public class Issue33034TabContent : ContentPage +{ + public Issue33034TabContent() + { + // Full-width label to detect safe area padding on either side + var edgeLabel = new Label + { + Text = "EDGE LABEL", + AutomationId = "EdgeLabel", + FontSize = 18, + FontAttributes = FontAttributes.Bold, + BackgroundColor = Colors.Red, + TextColor = Colors.White, + HorizontalOptions = LayoutOptions.Fill, + HorizontalTextAlignment = TextAlignment.Center + }; + + Content = new VerticalStackLayout + { + Children = { edgeLabel } + }; + } +} + diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33038.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue33038.cs new file mode 100644 index 000000000000..dce30bdc7b53 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33038.cs @@ -0,0 +1,48 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 33038, "Layout breaks on first navigation until soft keyboard appears/disappears", PlatformAffected.Android)] +public class Issue33038 : TestShell +{ + protected override void Init() + { + FlyoutBehavior = FlyoutBehavior.Disabled; + Items.Add(new ShellContent { Title = "Start", Route = "start", Content = new Issue33038_StartPage() }); + Items.Add(new ShellContent { Title = "SignIn", Route = "signin", Content = new Issue33038_SignInPage() }); + } +} + +public class Issue33038_StartPage : ContentPage +{ + public Issue33038_StartPage() + { + var goToSignInButton = new Button { Text = "Go to SignIn", AutomationId = "GoToSignInButton" }; + goToSignInButton.Clicked += async (s, e) => await Shell.Current.GoToAsync("//signin", false); + + Content = new VerticalStackLayout + { + VerticalOptions = LayoutOptions.Center, + Spacing = 20, + Children = { new Label { Text = "Start Page", AutomationId = "StartPageLabel" }, goToSignInButton } + }; + } +} + +public class Issue33038_SignInPage : ContentPage +{ + public Issue33038_SignInPage() + { + Shell.SetNavBarIsVisible(this, false); + SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container); + + Content = new VerticalStackLayout + { + Spacing = 16, + Padding = new Thickness(20), + Children = + { + new Label { Text = "Sign In Page", BackgroundColor = Colors.Yellow, AutomationId = "SignInLabel" }, + new Entry { Placeholder = "Email", AutomationId = "EmailEntry" } + } + }; + } +} \ No newline at end of file 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..ba353669dffc --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32941.cs @@ -0,0 +1,39 @@ +#if ANDROID || IOS // 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 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..3274047efd08 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33034.cs @@ -0,0 +1,32 @@ +#if ANDROID || IOS // SafeAreaEdges not supported on Catalyst and Windows + +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() + { + App.WaitForElement("EdgeLabel"); + App.SetOrientationLandscape(); + var initialRect = App.WaitForElement("EdgeLabel").GetRect(); + + App.TapTab("Second Tab"); + App.WaitForElement("EdgeLabel"); + App.TapTab("First Tab"); + var afterSwitchRect = App.WaitForElement("EdgeLabel").GetRect(); + + Assert.That(afterSwitchRect.X, Is.EqualTo(initialRect.X).Within(5)); + Assert.That(afterSwitchRect.Width, Is.EqualTo(initialRect.Width).Within(5)); + } +} +#endif diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33038.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33038.cs new file mode 100644 index 000000000000..867fec096bd9 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33038.cs @@ -0,0 +1,25 @@ +#if ANDROID || IOS // SafeAreaEdges not supported on Catalyst and Windows + +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue33038 : _IssuesUITest +{ + public Issue33038(TestDevice testDevice) : base(testDevice) { } + + public override string Issue => "Layout breaks on first navigation until soft keyboard appears/disappears"; + + [Test] + [Category(UITestCategories.SafeAreaEdges)] + public void LayoutShouldBeCorrectOnFirstNavigation() + { + App.WaitForElement("StartPageLabel"); + App.Tap("GoToSignInButton"); + App.WaitForElement("SignInLabel"); + VerifyScreenshot(); + } +} +#endif diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/LayoutShouldBeCorrectOnFirstNavigation.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/LayoutShouldBeCorrectOnFirstNavigation.png new file mode 100644 index 000000000000..7ea48c63e137 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/LayoutShouldBeCorrectOnFirstNavigation.png differ diff --git a/src/Core/src/Platform/Android/SafeAreaExtensions.cs b/src/Core/src/Platform/Android/SafeAreaExtensions.cs index faf4cbbe06dc..d888469d28fe 100644 --- a/src/Core/src/Platform/Android/SafeAreaExtensions.cs +++ b/src/Core/src/Platform/Android/SafeAreaExtensions.cs @@ -142,6 +142,22 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor var screenWidth = realMetrics.WidthPixels; var screenHeight = realMetrics.HeightPixels; + // Check if view extends beyond screen bounds - this indicates the view + // is still being positioned (e.g., during Shell fragment transitions). + // In this case, consume all insets to prevent children from processing + // invalid data, and request a re-apply after the view settles. + bool viewExtendsBeyondScreen = viewRight > screenWidth || viewBottom > screenHeight || + viewLeft < 0 || viewTop < 0; + + if (viewExtendsBeyondScreen) + { + // Request insets to be reapplied after the next layout pass + // when the view should be properly positioned. + // Don't return early - let processing continue with current insets + // to avoid visual popping, the re-apply will correct any issues. + view.Post(() => ViewCompat.RequestApplyInsets(view)); + } + // 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