Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
141 changes: 141 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue32871.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#if ANDROID
using Android.Views;
using AView = Android.Views.View;
#endif

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 32871, "[Android] Bottom insets issues when keyboard is shown", PlatformAffected.Android)]
public partial class Issue32871 : ContentPage
{
public Issue32871()
{
SafeAreaEdges = SafeAreaEdges.None;
BackgroundColor = Colors.Green;

var paddingLabel = new Label
{
Text = "waiting",
AutomationId = "PaddingLabel",
TextColor = Colors.White,
FontSize = 12
};

var entry = new Entry
{
Placeholder = "Tap here to show keyboard",
AutomationId = "TestEntry",
VerticalOptions = LayoutOptions.Start,
HorizontalOptions = LayoutOptions.Fill,
HeightRequest = 56
};

var grid = new Grid
{
AutomationId = "MainGrid",
SafeAreaEdges = SafeAreaEdges.Default,
BackgroundColor = Colors.Red,
RowDefinitions =
{
new RowDefinition(80),
new RowDefinition(GridLength.Auto),
new RowDefinition(GridLength.Star),
new RowDefinition(GridLength.Auto)
}
};

var label = new Label
{
Text = "Issue 32871",
AutomationId = "HeaderLabel",
HorizontalTextAlignment = Microsoft.Maui.TextAlignment.Center,
VerticalOptions = LayoutOptions.Start,
TextColor = Colors.White
};

var bottomButton = new Button
{
Text = "Bottom Button",
AutomationId = "BottomButton",
BackgroundColor = Colors.Blue,
TextColor = Colors.White
};

Grid.SetRow(label, 0);
Grid.SetRow(paddingLabel, 1);
Grid.SetRow(entry, 2);
Grid.SetRow(bottomButton, 3);

grid.Children.Add(label);
grid.Children.Add(paddingLabel);
grid.Children.Add(entry);
grid.Children.Add(bottomButton);

Content = grid;

SetupPlatform(grid, paddingLabel);
}

partial void SetupPlatform(Grid grid, Label paddingLabel);

protected override void OnDisappearing()
{
base.OnDisappearing();
CleanupPlatform();
}

partial void CleanupPlatform();
}

#if ANDROID
public partial class Issue32871
{
SoftInput _previousSoftInputMode;

partial void SetupPlatform(Grid grid, Label paddingLabel)
{
var window = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity?.Window;
if (window?.Attributes is WindowManagerLayoutParams attr)
{
_previousSoftInputMode = attr.SoftInputMode;
}
window?.SetSoftInputMode(SoftInput.AdjustUnspecified | SoftInput.StateHidden);

grid.HandlerChanged += (s, e) =>
{
if (grid.Handler?.PlatformView is AView nativeView)
{
paddingLabel.Text = $"NativePadding: B={nativeView.PaddingBottom}";
nativeView.AddOnLayoutChangeListener(new LayoutListener(nativeView, paddingLabel));
}
};
Comment on lines +104 to +111

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

grid.HandlerChanged subscribes with a lambda and AddOnLayoutChangeListener(...) creates a listener instance that is never removed or disposed. If the handler changes (or the page is reopened), this can register multiple listeners and leak Java-side objects. Store the listener (and the handler-changed delegate) as fields, unsubscribe in CleanupPlatform, and call RemoveOnLayoutChangeListener + Dispose() on the listener.

Copilot uses AI. Check for mistakes.
}

partial void CleanupPlatform()
{
var window = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity?.Window;
window?.SetSoftInputMode(_previousSoftInputMode);
}

class LayoutListener : Java.Lang.Object, AView.IOnLayoutChangeListener
{
readonly WeakReference<AView> _view;
readonly WeakReference<Label> _label;

public LayoutListener(AView view, Label label)
{
_view = new WeakReference<AView>(view);
_label = new WeakReference<Label>(label);
}

public void OnLayoutChange(AView v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom)
{
if (_view.TryGetTarget(out var view) && _label.TryGetTarget(out var label))
{
label.Text = $"NativePadding: B={view.PaddingBottom}";
}
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#if ANDROID // Android-only: fix and native padding assertion rely on Android platform code
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue32871 : _IssuesUITest
{
public override string Issue => "[Android] Bottom insets issues when keyboard is shown";

public Issue32871(TestDevice device) : base(device)
{
}

[Test]
[Category(UITestCategories.SafeAreaEdges)]
public void BottomPaddingShouldBePreservedWhileKeyboardIsShowing()
{
App.WaitForElement("MainGrid");
App.WaitForTextToBePresentInElement("PaddingLabel", "NativePadding");

var initialPaddingText = App.FindElement("PaddingLabel").GetText() ?? "";
var initialBottomPadding = ExtractBottomPadding(initialPaddingText);

if (initialBottomPadding <= 0)
{
Assert.Ignore("Device has no navigation bar bottom inset — cannot validate this regression.");
return;
}
Comment on lines +23 to +30

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

ExtractBottomPadding returns -1 when parsing fails, but the test treats any <= 0 value as “no navigation bar inset” and ignores. This can mask real failures (e.g., if the label never updates or the text format changes). Consider asserting that parsing succeeded (e.g., >= 0) and only Assert.Ignore when the parsed bottom padding is exactly 0.

Copilot uses AI. Check for mistakes.

App.Tap("TestEntry");

Assert.That(App.WaitForKeyboardToShow(), Is.True,
"Keyboard must be visible to validate the fix.");

var paddingWhileKeyboard = App.FindElement("PaddingLabel").GetText() ?? "";
var bottomPaddingDuringKeyboard = ExtractBottomPadding(paddingWhileKeyboard);

Assert.That(bottomPaddingDuringKeyboard, Is.EqualTo(initialBottomPadding),
$"Bottom padding should be preserved while keyboard is showing. " +
$"Initial: {initialBottomPadding}px, During keyboard: {bottomPaddingDuringKeyboard}px.");
Comment on lines +34 to +42

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

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

The test shows the keyboard but never dismisses it. Leaving the soft keyboard open can interfere with subsequent UI tests running in the same app session (occluding elements, affecting taps/screenshots). Dismiss the keyboard at the end of the test (and optionally wait for it to hide) to return the app to a stable state.

Suggested change
Assert.That(App.WaitForKeyboardToShow(), Is.True,
"Keyboard must be visible to validate the fix.");
var paddingWhileKeyboard = App.FindElement("PaddingLabel").GetText() ?? "";
var bottomPaddingDuringKeyboard = ExtractBottomPadding(paddingWhileKeyboard);
Assert.That(bottomPaddingDuringKeyboard, Is.EqualTo(initialBottomPadding),
$"Bottom padding should be preserved while keyboard is showing. " +
$"Initial: {initialBottomPadding}px, During keyboard: {bottomPaddingDuringKeyboard}px.");
try
{
Assert.That(App.WaitForKeyboardToShow(), Is.True,
"Keyboard must be visible to validate the fix.");
var paddingWhileKeyboard = App.FindElement("PaddingLabel").GetText() ?? "";
var bottomPaddingDuringKeyboard = ExtractBottomPadding(paddingWhileKeyboard);
Assert.That(bottomPaddingDuringKeyboard, Is.EqualTo(initialBottomPadding),
$"Bottom padding should be preserved while keyboard is showing. " +
$"Initial: {initialBottomPadding}px, During keyboard: {bottomPaddingDuringKeyboard}px.");
}
finally
{
App.DismissKeyboard();
}

Copilot uses AI. Check for mistakes.
}

static double ExtractBottomPadding(string paddingText)
{
var prefix = "B=";
var idx = paddingText.IndexOf(prefix, StringComparison.Ordinal);
if (idx >= 0)
{
var valueStr = paddingText.Substring(idx + prefix.Length).Trim();
if (double.TryParse(valueStr, out var value))
return value;
}
return -1;
}
}
#endif
4 changes: 0 additions & 4 deletions src/Core/src/Platform/Android/SafeAreaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,6 @@ internal static double GetSafeAreaForEdge(SafeAreaRegions safeAreaRegion, double
// 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;
}
}

Expand Down
Loading