Skip to content
103 changes: 94 additions & 9 deletions maui/src/NumericEntry/SfNumericEntry.Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2756,6 +2756,25 @@ void WindowEntryHandler(object? sender)
}
}

/// <summary>
/// Cleans up Android entry event handlers to prevent memory leaks.
/// </summary>
void CleanupAndroidEntryEvents()
{
#if ANDROID
if (_androidEntry != null)
{
_androidEntry.EditorAction -= AndroidEntry_EditorAction;
_androidEntry.TextChanged -= OnTextBoxTextChanged;
_androidEntry.BeforeTextChanged -= AndroidEntry_BeforeTextChanged;
_androidEntry.AfterTextChanged -= AndroidEntry_AfterTextChanged;
_androidEntry.Click -= AndroidEntry_Click;
_androidEntry.FocusChange -= AndroidEntry_FocusChange;
_androidEntry = null;
}
#endif
}

/// <summary>
/// Configures platform-specific behavior for Android Entry controls.
/// This method is called when the Entry's handler changes.
Expand All @@ -2764,16 +2783,27 @@ void WindowEntryHandler(object? sender)
void AndroidEntryHandler(object? sender)
{
#if ANDROID
if ((sender is SfEntryView textBox) && textBox.Handler != null && textBox.Handler.PlatformView is AndroidX.AppCompat.Widget.AppCompatEditText androidEntry)
if (sender is SfEntryView textBox)
{
androidEntry.EditorAction += AndroidEntry_EditorAction;
androidEntry.TextChanged += OnTextBoxTextChanged;
androidEntry.BeforeTextChanged += AndroidEntry_BeforeTextChanged;
androidEntry.AfterTextChanged += AndroidEntry_AfterTextChanged;
androidEntry.KeyListener = global::Android.Text.Method.DigitsKeyListener.GetInstance(KEYS);
androidEntry.Click += AndroidEntry_Click;
// Disable EmojiCompat to prevent the IllegalArgumentException crash on Android when start typing text.
androidEntry.EmojiCompatEnabled = false;
// Clean up existing event handlers if we have a previous Android entry
CleanupAndroidEntryEvents();

// Subscribe to new handler's platform view if available
if (textBox.Handler != null && textBox.Handler.PlatformView is AndroidX.AppCompat.Widget.AppCompatEditText androidEntry)
{
// Store reference to the Android entry for cleanup
_androidEntry = androidEntry;

androidEntry.EditorAction += AndroidEntry_EditorAction;
androidEntry.TextChanged += OnTextBoxTextChanged;
androidEntry.BeforeTextChanged += AndroidEntry_BeforeTextChanged;
androidEntry.AfterTextChanged += AndroidEntry_AfterTextChanged;
androidEntry.KeyListener = global::Android.Text.Method.DigitsKeyListener.GetInstance(KEYS);
androidEntry.Click += AndroidEntry_Click;
androidEntry.FocusChange += AndroidEntry_FocusChange;
// Disable EmojiCompat to prevent the IllegalArgumentException crash on Android when start typing text.
androidEntry.EmojiCompatEnabled = false;
}
}
#endif
}
Expand Down Expand Up @@ -2877,6 +2907,58 @@ void AndroidEntry_Click(object? sender, EventArgs e)
}
}
}

/// <summary>
/// Handles the focus change event for an Android Entry control.
/// Ensures proper value update and formatting when focus is lost.
/// This fixes the issue where clicking a button doesn't update the value
/// or dismiss the keyboard because the entry doesn't properly lose focus.
/// </summary>
/// <param name="sender">The object that triggered the event, typically an Android Entry component.</param>
/// <param name="e">The focus change event arguments.</param>
void AndroidEntry_FocusChange(object? sender, Android.Views.View.FocusChangeEventArgs e)
{
// Capture local references to avoid race conditions
// The _androidEntry and _textBox could be set to null on another thread
var androidEntry = _androidEntry;
var textBox = _textBox;

// Verify references are valid before processing
if (sender == null || androidEntry == null || textBox == null)
{
return;
}

// When focus is lost (HasFocus is false), ensure the MAUI layer is notified
// This is crucial for Android where tapping outside (e.g., on a button)
// doesn't always properly sync the native focus state with MAUI's focus state
if (!e.HasFocus)
{
// The native Android entry has lost focus
// Check if MAUI's Entry is still reporting as focused
// If so, we need to trigger the unfocus to ensure proper event handling
MainThread.BeginInvokeOnMainThread(() =>
{
try
{
// Use the local captured references to avoid race conditions
// Only proceed if the textBox is still focused
if (textBox.IsFocused)
{
// MAUI still thinks the entry is focused, but the native control has lost focus
// This can happen when clicking outside (e.g., on a button)
// Calling Unfocus() will trigger TextBoxOnLostFocus -> OnLostFocus() -> UpdateValue() -> FormatValue()
textBox.Unfocus();
}
}
catch (ObjectDisposedException)
{
// The control was disposed between capturing the reference and using it
// This is safe to ignore as the control no longer needs to be unfocused
}
});
}
}
#endif

/// <summary>
Expand All @@ -2895,6 +2977,9 @@ void UnHookEvents()
_textBox.Unfocused -= TextBoxOnLostFocus;

}

// Clean up Android-specific event handlers
CleanupAndroidEntryEvents();
}
#endregion

Expand Down
5 changes: 5 additions & 0 deletions maui/src/NumericEntry/SfNumericEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@ public partial class SfNumericEntry : SfView, ITextElement, ITouchListener, IKey
/// </summary>
bool _isFirstFocus = true;

/// <summary>
/// Represents the underlying Android AppCompatEditText used in the control.
/// </summary>
AndroidX.AppCompat.Widget.AppCompatEditText? _androidEntry;

#endif

#if !WINDOWS
Expand Down