Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<Grid RowDefinitions="*,*">
<Grid Grid.Row="0">
<VerticalStackLayout>
<Entry Placeholder="Entry1" AutomationId="Entry1" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry2" AutomationId="Entry2" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry3" AutomationId="Entry3" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry4" AutomationId="Entry4" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry5" AutomationId="Entry5" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry6" AutomationId="Entry6" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry7" AutomationId="Entry7" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry8" AutomationId="Entry8" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry9" AutomationId="Entry9" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry10" AutomationId="Entry10" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry11" AutomationId="Entry11" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry12" AutomationId="Entry12" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry13" AutomationId="Entry13" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry14" AutomationId="Entry14" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry15" AutomationId="Entry15" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry16" AutomationId="Entry16" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry17" AutomationId="Entry17" Background="lightgray" HeightRequest="50"/>
<Entry Placeholder="Entry18" AutomationId="Entry18" Background="lightgray" HeightRequest="50"/>
</VerticalStackLayout>
</Grid>
</Grid>
<Grid RowDefinitions="*,*">
<Grid Grid.Row="0">
<VerticalStackLayout>
<Entry Placeholder="Entry1"
AutomationId="Entry1"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry2"
AutomationId="Entry2"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry3"
AutomationId="Entry3"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry4"
AutomationId="Entry4"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry5"
AutomationId="Entry5"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry6"
AutomationId="Entry6"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry7"
AutomationId="Entry7"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry8"
AutomationId="Entry8"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry9"
AutomationId="Entry9"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry10"
AutomationId="Entry10"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry11"
AutomationId="Entry11"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry12"
AutomationId="Entry12"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry13"
AutomationId="Entry13"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry14"
AutomationId="Entry14"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry15"
AutomationId="Entry15"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry16"
AutomationId="Entry16"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry17"
AutomationId="Entry17"
Background="lightgray"
HeightRequest="50"/>
<Entry Placeholder="Entry18"
AutomationId="Entry18"
Background="lightgray"
HeightRequest="50"/>
</VerticalStackLayout>
</Grid>
</Grid>
</ContentView>
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
AutomationId="KeyboardScrollingGridPage"
x:Class="Maui.Controls.Sample.KeyboardScrollingGridPage">
<Grid RowDefinitions="*, Auto">
<VerticalStackLayout Grid.Row="1">
<Entry Placeholder="Entry 1" AutomationId="Entry1"/>
<Entry Placeholder="Entry 2" AutomationId="Entry2"/>
<Entry Placeholder="Entry 3" AutomationId="Entry3"/>
<Entry Placeholder="Entry 4" AutomationId="Entry4"/>
<Entry Placeholder="Entry 5" AutomationId="Entry5"/>
<Entry Placeholder="Entry 6" AutomationId="Entry6"/>
<Entry Placeholder="Entry 7" AutomationId="Entry7"/>
</VerticalStackLayout>
</Grid>
<Grid RowDefinitions="*, Auto">
<VerticalStackLayout Grid.Row="1">
<Entry Placeholder="Entry 1"
AutomationId="Entry1"/>
<Entry Placeholder="Entry 2"
AutomationId="Entry2"/>
<Entry Placeholder="Entry 3"
AutomationId="Entry3"/>
<Entry Placeholder="Entry 4"
AutomationId="Entry4"/>
<Entry Placeholder="Entry 5"
AutomationId="Entry5"/>
<Entry Placeholder="Entry 6"
AutomationId="Entry6"/>
<Entry Placeholder="Entry 7"
AutomationId="Entry7"/>
</VerticalStackLayout>
</Grid>
</ContentView>
8 changes: 8 additions & 0 deletions src/Core/src/Platform/iOS/KeyboardAutoManagerScroll.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 38 additions & 16 deletions src/Core/src/Platform/iOS/MauiView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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
Expand All @@ -167,7 +167,7 @@ protected CGRect AdjustForSafeArea(CGRect bounds)
{
KeyboardAutoManagerScroll.ShouldScrollAgain = true;
}

ValidateSafeArea();
return _safeArea.InsetRect(bounds);
}
Expand Down Expand Up @@ -215,7 +215,7 @@ void UnsubscribeFromKeyboardNotifications()
NSNotificationCenter.DefaultCenter.RemoveObserver(showObserver);
_keyboardWillShowObserver = null;
}

if (_keyboardWillHideObserver?.TryGetTarget(out var hideObserver) == true)
{
NSNotificationCenter.DefaultCenter.RemoveObserver(hideObserver);
Expand Down Expand Up @@ -267,7 +267,7 @@ void OnKeyboardWillHide(NSNotification notification)
}
return null;
}

SafeAreaPadding GetAdjustedSafeAreaInsets()
{
var baseSafeArea = SafeAreaInsets.ToSafeAreaInsets();
Expand All @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

viewFrameInWindow could be null, can include a validation here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jsuarezruiz, in this case, viewFrameInWindow can’t be null because if the Superview is null, it falls back to this.Frame, which is always a valid rectangle. So, a null check is not required here, correct?. Please let me know if I have overlooked any scenarios or if you have further thoughts.

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);
}
}
Expand Down Expand Up @@ -339,6 +346,21 @@ SafeAreaPadding GetAdjustedSafeAreaInsets()
return baseSafeArea;
}

/// <summary>
/// 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.
/// </summary>
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;
}


/// <summary>
/// Checks if the current measure information is still valid for the given constraints.
/// This optimization avoids redundant measure operations when constraints haven't changed.
Expand Down
Loading