diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986.cs index 262d38aa3a62..f734efe41cca 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986.cs @@ -13,6 +13,72 @@ public Issue28986(TestDevice device) : base(device) { } + [Test] + [Category(UITestCategories.SafeAreaEdges)] + public void SoftInputDoesNotApplyBottomPaddingWhenKeyboardHidden() + { + // This test validates the fix for issue #31870 + // When SafeAreaEdges.Bottom is set to SoftInput and the keyboard is NOT showing, + // there should be NO bottom padding from the navigation bar. + App.WaitForElement("ContentGrid"); + + // First, set to None to get the baseline position (no safe area padding) + App.Tap("GridResetNoneButton"); + var noneRect = App.WaitForElement("MainGrid").GetRect(); + + // Now set bottom edge to SoftInput (keyboard is still hidden) + App.Tap("GridSetBottomSoftInputButton"); + + // Wait for layout to update and get the rect after setting SoftInput + var softInputRect = App.WaitForElement("MainGrid").GetRect(); + + // Verify that the bottom position is the same as None (no padding applied) + // The height should be the same because SoftInput should not add padding when keyboard is hidden + Assert.That(softInputRect.Height, Is.EqualTo(noneRect.Height).Within(5), + "MainGrid height should be the same with SoftInput as with None when keyboard is hidden (no bottom padding)"); + + // Also verify the Y position hasn't shifted + Assert.That(softInputRect.Y, Is.EqualTo(noneRect.Y).Within(5), + "MainGrid Y position should be the same with SoftInput as with None when keyboard is hidden"); + } + + [Test] + [Category(UITestCategories.SafeAreaEdges)] + public void AllRegionStillAppliesBottomPaddingWhenKeyboardHidden() + { + // This test validates that the fix for #31870 doesn't break SafeAreaRegions.All + // When SafeAreaEdges is set to All, it should respect safe area behavior consistently. + // The specific assertion depends on whether the device has a navigation bar. + App.WaitForElement("ContentGrid"); + + // First, set to None to get the baseline position (no safe area padding) + App.Tap("GridResetNoneButton"); + var noneRect = App.WaitForElement("MainGrid").GetRect(); + + // Set bottom edge to SoftInput to test the specific behavior we fixed + App.Tap("GridSetBottomSoftInputButton"); + App.WaitForElement("MainGrid"); + var softInputRect = App.WaitForElement("MainGrid").GetRect(); + + // Now set to All (should apply all safe area insets) + App.Tap("GridResetAllButton"); + + // Get the rect after setting All + var allRect = App.WaitForElement("MainGrid").GetRect(); + + // The key validation: All should have same or less height than None (never more) + // And All should behave differently than SoftInput when keyboard is hidden + // Note: Using LessThanOrEqualTo instead of LessThan because some test devices (e.g., emulators + // without navigation bars) have no bottom safe area padding, resulting in equal heights. + // This test validates behavior consistency rather than assuming specific padding values. + Assert.That(allRect.Height, Is.LessThanOrEqualTo(noneRect.Height), + "MainGrid height with All should be less than or equal to None (All respects safe area)"); + + // SoftInput should match None when keyboard is hidden (no bottom padding) + Assert.That(softInputRect.Height, Is.EqualTo(noneRect.Height).Within(5), + "MainGrid height with SoftInput should match None when keyboard is hidden"); + } + [Test] [Category(UITestCategories.SafeAreaEdges)] public void SafeAreaMainGridBasicFunctionality() @@ -97,7 +163,6 @@ public void SafeAreaMainGridSequentialButtonTesting() Assert.That(finalAllPosition.Y, Is.EqualTo(allPosition.Y), "Final All position should match initial All position"); } -#if TEST_FAILS_ON_ANDROID [Test] [Category(UITestCategories.SafeAreaEdges)] public void SafeAreaPerEdgeValidation() @@ -128,6 +193,5 @@ public void SafeAreaPerEdgeValidation() Assert.That(containerPositionWithoutSoftInput.Height, Is.EqualTo(containerPosition.Height), "ContentGrid height should return to original when Soft Input is dismissed with Container edges"); }); } - #endif } #endif \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ContentPage.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ContentPage.cs index 98f83a954045..d32412becec1 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ContentPage.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue28986_ContentPage.cs @@ -98,7 +98,6 @@ public void SafeAreaMainGridSequentialButtonTesting() } - #if TEST_FAILS_ON_ANDROID [Test] [Category(UITestCategories.SafeAreaEdges)] public void SafeAreaPerEdgeValidation() @@ -129,6 +128,5 @@ public void SafeAreaPerEdgeValidation() Assert.That(containerPositionWithoutSoftInput.Height, Is.EqualTo(containerPosition.Height), "ContentGrid height should return to original when Soft Input is dismissed with Container edges"); }); } - #endif } #endif diff --git a/src/Core/src/Platform/Android/MauiWindowInsetListener.cs b/src/Core/src/Platform/Android/MauiWindowInsetListener.cs index 3e1d2509d4c5..9763ec85ed32 100644 --- a/src/Core/src/Platform/Android/MauiWindowInsetListener.cs +++ b/src/Core/src/Platform/Android/MauiWindowInsetListener.cs @@ -304,6 +304,10 @@ public void TrackView(AView view) public bool HasTrackedView => _trackedViews.Count > 0; + public bool IsViewTracked(AView view) + { + return _trackedViews.Contains(view); + } public void ResetView(AView view) { if (view is IHandleWindowInsets customHandler) diff --git a/src/Core/src/Platform/Android/SafeAreaExtensions.cs b/src/Core/src/Platform/Android/SafeAreaExtensions.cs index d6bb7d9db0f1..faf4cbbe06dc 100644 --- a/src/Core/src/Platform/Android/SafeAreaExtensions.cs +++ b/src/Core/src/Platform/Android/SafeAreaExtensions.cs @@ -62,6 +62,26 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor var right = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(2, layout), baseSafeArea.Right, 2, isKeyboardShowing, keyboardInsets); var bottom = GetSafeAreaForEdge(GetSafeAreaRegionForEdge(3, layout), baseSafeArea.Bottom, 3, isKeyboardShowing, keyboardInsets); + var globalWindowInsetsListener = MauiWindowInsetListener.FindListenerForView(view); + bool hasTrackedViews = globalWindowInsetsListener?.HasTrackedView == true; + + // If this view has no safe area padding to apply, pass insets through to children + // instead of consuming them. This allows child views with SafeAreaEdges set + // to properly handle the insets even when the parent has SafeAreaEdges.None + // However, if this view was previously tracked (had padding before), we need to + // continue processing to reset the padding to 0 + if (left == 0 && right == 0 && top == 0 && bottom == 0) + { + // Only pass through if this view hasn't been tracked yet + // If it was tracked, we need to reset its padding + if (globalWindowInsetsListener?.IsViewTracked(view) != true) + { + // Don't consume insets - pass them through for potential child views to handle + return windowInsets; + } + } + + if (isKeyboardShowing && context.GetActivity()?.Window is Window window && window?.Attributes is WindowManagerLayoutParams attr) @@ -77,9 +97,6 @@ internal static SafeAreaRegions GetSafeAreaRegionForEdge(int edge, ICrossPlatfor } } - var globalWindowInsetsListener = MauiWindowInsetListener.FindListenerForView(view); - bool hasTrackedViews = globalWindowInsetsListener?.HasTrackedView == true; - // Check intersection with view bounds to determine which edges actually need padding // If we don't have any tracked views yet we will find the first view to pad // in order to limit duplicate measures @@ -253,15 +270,25 @@ internal static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double } // Handle SoftInput specifically - only apply keyboard insets for bottom edge when keyboard is showing - if (isKeyboardShowing && edge == 3) - { - if (SafeAreaEdges.IsSoftInput(safeAreaRegion)) - return keyBoardInsets.Bottom; - - // if they keyboard is showing then we will just return 0 for the bottom inset - // because that part of the view is covered by the keyboard so we don't want to pad the view - return 0; - } + if (edge == 3) + { + if (SafeAreaEdges.IsOnlySoftInput(safeAreaRegion)) + { + // SoftInput only applies padding when keyboard is showing + return isKeyboardShowing ? keyBoardInsets.Bottom : 0; + } + + if (isKeyboardShowing) + { + // Return keyboard insets for any region that includes SoftInput + if (SafeAreaEdges.IsSoftInput(safeAreaRegion)) + return keyBoardInsets.Bottom; + + // if the keyboard is showing then we will just return 0 for the bottom inset + // because that part of the view is covered by the keyboard so we don't want to pad the view + return 0; + } + } // All other regions respect safe area in some form // This includes: diff --git a/src/Core/src/Platform/iOS/MauiView.cs b/src/Core/src/Platform/iOS/MauiView.cs index dc04aa9a545d..226f94664e1f 100644 --- a/src/Core/src/Platform/iOS/MauiView.cs +++ b/src/Core/src/Platform/iOS/MauiView.cs @@ -143,16 +143,29 @@ SafeAreaRegions GetSafeAreaRegionForEdge(int edge) return SafeAreaRegions.None; } - static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double originalSafeArea) + // Note: This method was changed from static to instance to access _isKeyboardShowing field + // which is needed to determine if SoftInput padding should be applied + double GetSafeAreaForEdge(double originalSafeArea, int edge) { + var safeAreaRegion = GetSafeAreaRegionForEdge(edge); + // Edge-to-edge content - no safe area padding if (safeAreaRegion == SafeAreaRegions.None) return 0; + // Handle SoftInput specifically - only apply padding when keyboard is actually showing + if (edge == 3 && SafeAreaEdges.IsOnlySoftInput(safeAreaRegion)) + { + // SoftInput only applies padding when keyboard is showing + // When keyboard is hidden, return 0 to avoid showing home indicator padding + if (!_isKeyboardShowing) + return 0; + } + // All other regions respect safe area in some form // This includes: // - Default: Platform default behavior - // - All: Obey all safe area insets + // - 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 @@ -336,10 +349,10 @@ SafeAreaPadding GetAdjustedSafeAreaInsets() if (View is ISafeAreaView2) { // Apply safe area selectively per edge based on SafeAreaRegions - 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(baseSafeArea.Left, 0); + var top = GetSafeAreaForEdge(baseSafeArea.Top, 1); + var right = GetSafeAreaForEdge(baseSafeArea.Right, 2); + var bottom = GetSafeAreaForEdge(baseSafeArea.Bottom, 3); return new SafeAreaPadding(left, right, top, bottom); } diff --git a/src/Core/src/Primitives/SafeAreaEdges.cs b/src/Core/src/Primitives/SafeAreaEdges.cs index 0f231e249ee7..ed14a8f1b031 100644 --- a/src/Core/src/Primitives/SafeAreaEdges.cs +++ b/src/Core/src/Primitives/SafeAreaEdges.cs @@ -78,13 +78,19 @@ internal static bool IsSoftInput(SafeAreaRegions region) return (region & SafeAreaRegions.SoftInput) == SafeAreaRegions.SoftInput; } + internal static bool IsOnlySoftInput(SafeAreaRegions region) + { + // Check if the region is ONLY SoftInput, not combined with other flags or All + return region == SafeAreaRegions.SoftInput; + } + internal static bool IsContainer(SafeAreaRegions region) { if (region == SafeAreaRegions.Default) return false; if (region == SafeAreaRegions.All) return true; - return (region & SafeAreaRegions.Container) == SafeAreaRegions.Container; + return (region & SafeAreaRegions.Container) == SafeAreaRegions.Container; } ///