diff --git a/src/Controls/tests/TestCases.HostApp/Elements/KeyboardScrollingEntriesPage.xaml b/src/Controls/tests/TestCases.HostApp/Elements/KeyboardScrollingEntriesPage.xaml index 18e82d95659a..689a4212e775 100644 --- a/src/Controls/tests/TestCases.HostApp/Elements/KeyboardScrollingEntriesPage.xaml +++ b/src/Controls/tests/TestCases.HostApp/Elements/KeyboardScrollingEntriesPage.xaml @@ -4,28 +4,82 @@ AutomationId="KeyboardScrollingEntriesPage" x:Class="Maui.Controls.Sample.KeyboardScrollingEntriesPage" xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"> - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Controls/tests/TestCases.HostApp/Elements/KeyboardScrollingGridPage.xaml b/src/Controls/tests/TestCases.HostApp/Elements/KeyboardScrollingGridPage.xaml index c62e0538368e..996c86de8adc 100644 --- a/src/Controls/tests/TestCases.HostApp/Elements/KeyboardScrollingGridPage.xaml +++ b/src/Controls/tests/TestCases.HostApp/Elements/KeyboardScrollingGridPage.xaml @@ -3,15 +3,22 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" AutomationId="KeyboardScrollingGridPage" x:Class="Maui.Controls.Sample.KeyboardScrollingGridPage"> - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs b/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs index 62cdbed39668..4ebe250f61c0 100644 --- a/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs +++ b/src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs @@ -305,6 +305,14 @@ static string RemoveEverythingExceptForNumbersAndCommas(string input) // all the fields are updated before calling AdjustPostition() internal static async Task AdjustPositionDebounce() { + // If View is inside a MauiView that implements ISafeAreaView2 + // and has SafeAreaEdges.SoftInput set, do not perform auto-scrolling + // since SafeAreaEdges.SoftInput will handle the adjustments + if (View is not null && MauiView.IsSoftInputHandledByParent(View)) + { + return; + } + if (IsKeyboardShowing) { // Universal 30ms delay for all input controls to ensure proper timing coordination diff --git a/src/Core/src/Platform/iOS/MauiView.cs b/src/Core/src/Platform/iOS/MauiView.cs index f14248c3c82e..b81437ceb57b 100644 --- a/src/Core/src/Platform/iOS/MauiView.cs +++ b/src/Core/src/Platform/iOS/MauiView.cs @@ -60,7 +60,7 @@ public abstract class MauiView : UIView, ICrossPlatformLayoutBacking, IVisualTre // True if the view is an ISafeAreaView, does not ignore safe area, and is not inside a UIScrollView; // otherwise, false. Null means not yet determined. bool? _scrollViewDescendant; - + // Keyboard tracking CGRect _keyboardFrame = CGRect.Empty; bool _isKeyboardShowing; @@ -126,13 +126,13 @@ SafeAreaRegions GetSafeAreaRegionForEdge(int edge) { return safeAreaPage.GetSafeAreaRegionsForEdge(edge); } - + // Fallback to legacy ISafeAreaView behavior if (View is ISafeAreaView sav) { return sav.IgnoreSafeArea ? SafeAreaRegions.None : SafeAreaRegions.Container; } - + return SafeAreaRegions.None; } @@ -141,7 +141,7 @@ static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double original // 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 @@ -167,7 +167,7 @@ protected CGRect AdjustForSafeArea(CGRect bounds) { KeyboardAutoManagerScroll.ShouldScrollAgain = true; } - + ValidateSafeArea(); return _safeArea.InsetRect(bounds); } @@ -215,7 +215,7 @@ void UnsubscribeFromKeyboardNotifications() NSNotificationCenter.DefaultCenter.RemoveObserver(showObserver); _keyboardWillShowObserver = null; } - + if (_keyboardWillHideObserver?.TryGetTarget(out var hideObserver) == true) { NSNotificationCenter.DefaultCenter.RemoveObserver(hideObserver); @@ -267,7 +267,7 @@ void OnKeyboardWillHide(NSNotification notification) } return null; } - + SafeAreaPadding GetAdjustedSafeAreaInsets() { var baseSafeArea = SafeAreaInsets.ToSafeAreaInsets(); @@ -292,26 +292,33 @@ SafeAreaPadding GetAdjustedSafeAreaInsets() { // Get the keyboard frame and calculate its intersection with the current window var window = this.Window; - + if (window != null && !_keyboardFrame.IsEmpty) { var windowFrame = window.Frame; var keyboardIntersection = CGRect.Intersect(_keyboardFrame, windowFrame); - + // If keyboard is visible and intersects with window if (!keyboardIntersection.IsEmpty) { - // Calculate keyboard height in the window's coordinate system - var keyboardHeight = keyboardIntersection.Height; - + var bottomEdgeRegion = safeAreaPage.GetSafeAreaRegionsForEdge(3); // 3 = bottom edge + // For SafeAreaRegions.SoftInput: Always pad so content doesn't go under the keyboard - // Bottom edge is most commonly affected by keyboard - var bottomEdgeRegion = safeAreaPage.GetSafeAreaRegionsForEdge(3); // 3 = bottom edge - if (SafeAreaEdges.IsSoftInput(bottomEdgeRegion)) + if (SafeAreaEdges.IsSoftInput(bottomEdgeRegion) && !IsSoftInputHandledByParent(this)) { // Use the larger of the current bottom safe area or the keyboard height - var adjustedBottom = Math.Max(baseSafeArea.Bottom, keyboardHeight); + // Get the input control's bottom Y in window coordinates + var inputBottomY = 0.0; + if (Window is not null) + { + var viewFrameInWindow = this.Superview?.ConvertRectToView(this.Frame, Window) ?? this.Frame; + inputBottomY = viewFrameInWindow.Y + viewFrameInWindow.Height; + } + var keyboardTopY = _keyboardFrame.Y; + var overlap = inputBottomY > keyboardTopY ? (inputBottomY - keyboardTopY) : 0.0; + + var adjustedBottom = (overlap > 0) ? overlap : baseSafeArea.Bottom; baseSafeArea = new SafeAreaPadding(baseSafeArea.Left, baseSafeArea.Right, baseSafeArea.Top, adjustedBottom); } } @@ -339,6 +346,21 @@ SafeAreaPadding GetAdjustedSafeAreaInsets() return baseSafeArea; } + /// + /// Checks if any parent view in the hierarchy is a MauiView that implements ISafeAreaView2 + /// and has SafeAreaEdges.SoftInput set for the bottom edge. This is used to determine if + /// keyboard overlap/padding is already being handled by an ancestor, so the current view + /// should not apply additional adjustments. + /// Returns true if a parent is handling soft input, false otherwise. + /// + internal static bool IsSoftInputHandledByParent(UIView view) + { + return view.FindParent(x => x is MauiView mv + && mv.View is ISafeAreaView2 safeAreaView2 + && SafeAreaEdges.IsSoftInput(safeAreaView2.GetSafeAreaRegionsForEdge(3))) is not null; + } + + /// /// Checks if the current measure information is still valid for the given constraints. /// This optimization avoids redundant measure operations when constraints haven't changed.