From 2ad9cb93f47f46fcb0584370ab8c297b20912718 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Wed, 25 Jan 2017 11:54:54 -0700 Subject: [PATCH] Add localized listener for Android numeric input --- .../CustomRenderers.cs | 30 +++ .../Bugzilla42000.cs | 65 +++++++ .../Xamarin.Forms.Controls.Issues.Shared.projitems | 1 + .../Cells/EntryCellRenderer.cs | 18 +- .../Renderers/EditorRenderer.cs | 18 +- .../Renderers/EntryRenderer.cs | 19 +- .../Renderers/KeyboardExtensions.cs | 2 +- .../Renderers/LocalizedDigitsKeyListener.cs | 211 +++++++++++++++++++++ .../Xamarin.Forms.Platform.Android.csproj | 5 +- 9 files changed, 362 insertions(+), 7 deletions(-) create mode 100644 Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42000.cs create mode 100644 Xamarin.Forms.Platform.Android/Renderers/LocalizedDigitsKeyListener.cs diff --git a/Xamarin.Forms.ControlGallery.Android/CustomRenderers.cs b/Xamarin.Forms.ControlGallery.Android/CustomRenderers.cs index 86876a3..4c6c1a8 100644 --- a/Xamarin.Forms.ControlGallery.Android/CustomRenderers.cs +++ b/Xamarin.Forms.ControlGallery.Android/CustomRenderers.cs @@ -19,12 +19,18 @@ using AButton = Android.Widget.Button; using AView = Android.Views.View; using Android.OS; using System.Reflection; +using Android.Text; +using Android.Text.Method; using Xamarin.Forms.Controls.Issues; [assembly: ExportRenderer(typeof(Bugzilla31395.CustomContentView), typeof(CustomContentRenderer))] [assembly: ExportRenderer(typeof(NativeListView), typeof(NativeListViewRenderer))] [assembly: ExportRenderer(typeof(NativeListView2), typeof(NativeAndroidListViewRenderer))] [assembly: ExportRenderer(typeof(NativeCell), typeof(NativeAndroidCellRenderer))] + +[assembly: ExportRenderer(typeof(Bugzilla42000._42000NumericEntryNoDecimal), typeof(EntryRendererNoDecimal))] +[assembly: ExportRenderer(typeof(Bugzilla42000._42000NumericEntryNoNegative), typeof(EntryRendererNoNegative))] + #if PRE_APPLICATION_CLASS #elif FORMS_APPLICATION_ACTIVITY #else @@ -485,5 +491,29 @@ namespace Xamarin.Forms.ControlGallery.Android base.OnElementChanged(e); } } + + // Custom renderers for Bugzilla42000 demonstration purposes + + public class EntryRendererNoNegative : EntryRenderer + { + protected override NumberKeyListener GetDigitsKeyListener(InputTypes inputTypes) + { + // Disable the NumberFlagSigned bit + inputTypes &= ~InputTypes.NumberFlagSigned; + + return base.GetDigitsKeyListener(inputTypes); + } + } + + public class EntryRendererNoDecimal : EntryRenderer + { + protected override NumberKeyListener GetDigitsKeyListener(InputTypes inputTypes) + { + // Disable the NumberFlagDecimal bit + inputTypes &= ~InputTypes.NumberFlagDecimal; + + return base.GetDigitsKeyListener(inputTypes); + } + } } diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42000.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42000.cs new file mode 100644 index 0000000..77436bc --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla42000.cs @@ -0,0 +1,65 @@ +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; + +namespace Xamarin.Forms.Controls.Issues +{ + [Preserve(AllMembers = true)] + [Issue(IssueTracker.Bugzilla, 42000, "Unable to use comma (\", \") as decimal point", PlatformAffected.Android)] + public class Bugzilla42000 : ContentPage + { + public Bugzilla42000() + { + var instructions = new Label + { + Text = + @"Change your system language settings and verify that you can type the correct decimal separator into the Entry and Editor controls below. +If your language is set to English (United States), you should be able to type '2.5', but not '2.5.3' or '2,5'. +If your language is set to Deutsch (Deutschland), you should be able to type '2,5', but not '2,5,3' or '2.5'. +" + }; + + var entrylabel = new Label { Text = "Entry:" }; + var entry = new Entry { Keyboard = Keyboard.Numeric }; + + var editorlabel = new Label { Text = "Editor:" }; + var editor = new Editor { Keyboard = Keyboard.Numeric }; + + var customRendererInstructions = new Label + { + Margin = new Thickness(0, 40, 0, 0), + Text = @"The two entries below demonstrate disabling decimal separators and negative numbers, respectively. +In the first one, neither '.' nor ',' should be typeable. +In the second, the '-' should not be typeable." + }; + + var entryNoDecimal = new _42000NumericEntryNoDecimal { Keyboard = Keyboard.Numeric }; + var entryNoNegative = new _42000NumericEntryNoNegative { Keyboard = Keyboard.Numeric }; + + Content = new StackLayout + { + VerticalOptions = LayoutOptions.Center, + Children = + { + instructions, + entrylabel, + entry, + editorlabel, + editor, + customRendererInstructions, + entryNoDecimal, + entryNoNegative + } + }; + } + + [Preserve(AllMembers = true)] + public class _42000NumericEntryNoDecimal : Entry + { + } + + [Preserve(AllMembers = true)] + public class _42000NumericEntryNoNegative : Entry + { + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index e98c36c..e511963 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -132,6 +132,7 @@ + Bugzilla42069_Page.xaml diff --git a/Xamarin.Forms.Platform.Android/Cells/EntryCellRenderer.cs b/Xamarin.Forms.Platform.Android/Cells/EntryCellRenderer.cs index e46422a..bfe8777 100644 --- a/Xamarin.Forms.Platform.Android/Cells/EntryCellRenderer.cs +++ b/Xamarin.Forms.Platform.Android/Cells/EntryCellRenderer.cs @@ -1,5 +1,7 @@ using System.ComponentModel; using Android.Content; +using Android.Text; +using Android.Text.Method; using Android.Views; namespace Xamarin.Forms.Platform.Android @@ -56,6 +58,14 @@ namespace Xamarin.Forms.Platform.Android UpdateHeight(); } + protected NumberKeyListener GetDigitsKeyListener(InputTypes inputTypes) + { + // Override this in a custom renderer to use a different NumberKeyListener + // or to filter out input types you don't want to allow + // (e.g., inputTypes &= ~InputTypes.NumberFlagSigned to disallow the sign) + return LocalizedDigitsKeyListener.Create(inputTypes); + } + void OnEditingCompleted() { var entryCell = (IEntryCellController)Cell; @@ -88,7 +98,13 @@ namespace Xamarin.Forms.Platform.Android void UpdateKeyboard() { var entryCell = (EntryCell)Cell; - _view.EditText.InputType = entryCell.Keyboard.ToInputType(); + var keyboard = entryCell.Keyboard; + _view.EditText.InputType = keyboard.ToInputType(); + + if (keyboard == Keyboard.Numeric) + { + _view.EditText.KeyListener = GetDigitsKeyListener(_view.EditText.InputType); + } } void UpdateLabel() diff --git a/Xamarin.Forms.Platform.Android/Renderers/EditorRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/EditorRenderer.cs index c9b596b..dba55ea 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/EditorRenderer.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/EditorRenderer.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using Android.Content.Res; using Android.Text; +using Android.Text.Method; using Android.Util; using Android.Views; using Java.Lang; @@ -86,6 +87,14 @@ namespace Xamarin.Forms.Platform.Android base.OnElementPropertyChanged(sender, e); } + protected NumberKeyListener GetDigitsKeyListener(InputTypes inputTypes) + { + // Override this in a custom renderer to use a different NumberKeyListener + // or to filter out input types you don't want to allow + // (e.g., inputTypes &= ~InputTypes.NumberFlagSigned to disallow the sign) + return LocalizedDigitsKeyListener.Create(inputTypes); + } + internal override void OnNativeFocusChanged(bool hasFocus) { if (Element.IsFocused && !hasFocus) // Editor has requested an unfocus, fire completed event @@ -102,7 +111,14 @@ namespace Xamarin.Forms.Platform.Android { Editor model = Element; EditorEditText edit = Control; - edit.InputType = model.Keyboard.ToInputType() | InputTypes.TextFlagMultiLine; + var keyboard = model.Keyboard; + + edit.InputType = keyboard.ToInputType() | InputTypes.TextFlagMultiLine; + + if (keyboard == Keyboard.Numeric) + { + edit.KeyListener = GetDigitsKeyListener(edit.InputType); + } } void UpdateText() diff --git a/Xamarin.Forms.Platform.Android/Renderers/EntryRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/EntryRenderer.cs index c1c24a8..7d53370 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/EntryRenderer.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/EntryRenderer.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using Android.Content.Res; using Android.Text; +using Android.Text.Method; using Android.Util; using Android.Views; using Android.Views.InputMethods; @@ -116,6 +117,14 @@ namespace Xamarin.Forms.Platform.Android base.OnElementPropertyChanged(sender, e); } + protected virtual NumberKeyListener GetDigitsKeyListener(InputTypes inputTypes) + { + // Override this in a custom renderer to use a different NumberKeyListener + // or to filter out input types you don't want to allow + // (e.g., inputTypes &= ~InputTypes.NumberFlagSigned to disallow the sign) + return LocalizedDigitsKeyListener.Create(inputTypes); + } + void UpdateAlignment() { Control.Gravity = Element.HorizontalTextAlignment.ToHorizontalGravityFlags(); @@ -156,7 +165,15 @@ namespace Xamarin.Forms.Platform.Android void UpdateInputType() { Entry model = Element; - _textView.InputType = model.Keyboard.ToInputType(); + var keyboard = model.Keyboard; + + _textView.InputType = keyboard.ToInputType(); + + if (keyboard == Keyboard.Numeric) + { + _textView.KeyListener = GetDigitsKeyListener(_textView.InputType); + } + if (model.IsPassword && ((_textView.InputType & InputTypes.ClassText) == InputTypes.ClassText)) _textView.InputType = _textView.InputType | InputTypes.TextVariationPassword; if (model.IsPassword && ((_textView.InputType & InputTypes.ClassNumber) == InputTypes.ClassNumber)) diff --git a/Xamarin.Forms.Platform.Android/Renderers/KeyboardExtensions.cs b/Xamarin.Forms.Platform.Android/Renderers/KeyboardExtensions.cs index a2e6017..6941422 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/KeyboardExtensions.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/KeyboardExtensions.cs @@ -20,7 +20,7 @@ namespace Xamarin.Forms.Platform.Android else if (self == Keyboard.Email) result = InputTypes.ClassText | InputTypes.TextVariationEmailAddress; else if (self == Keyboard.Numeric) - result = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal; + result = InputTypes.ClassNumber | InputTypes.NumberFlagDecimal | InputTypes.NumberFlagSigned; else if (self == Keyboard.Telephone) result = InputTypes.ClassPhone; else if (self == Keyboard.Text) diff --git a/Xamarin.Forms.Platform.Android/Renderers/LocalizedDigitsKeyListener.cs b/Xamarin.Forms.Platform.Android/Renderers/LocalizedDigitsKeyListener.cs new file mode 100644 index 0000000..009cffb --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/LocalizedDigitsKeyListener.cs @@ -0,0 +1,211 @@ +using System.Collections.Generic; +using Android.Text; +using Android.Text.Method; +using Java.Lang; +using Java.Text; + +namespace Xamarin.Forms.Platform.Android +{ + internal class LocalizedDigitsKeyListener : NumberKeyListener + { + readonly char _decimalSeparator; + + // I'm not aware of a situation/locale where this would need to be something different, + // but we'll make it easy to localize the sign in the future just in case + const char SignCharacter = '-'; + + static Dictionary s_unsignedCache; + static Dictionary s_signedCache; + + static char GetDecimalSeparator() + { + var format = NumberFormat.Instance as DecimalFormat; + if (format == null) + { + return '.'; + } + + DecimalFormatSymbols sym = format.DecimalFormatSymbols; + return sym.DecimalSeparator; + } + + public static NumberKeyListener Create(InputTypes inputTypes) + { + if ((inputTypes & InputTypes.NumberFlagDecimal) == 0) + { + // If decimal isn't allowed, we can just use the Android version + return DigitsKeyListener.GetInstance(inputTypes.HasFlag(InputTypes.NumberFlagSigned), false); + } + + // Figure out what the decimal separator is for the current locale + char decimalSeparator = GetDecimalSeparator(); + + if (decimalSeparator == '.') + { + // If it's '.', then we can just use the default Android version + return DigitsKeyListener.GetInstance(inputTypes.HasFlag(InputTypes.NumberFlagSigned), true); + } + + // If decimals are enabled and the locale's decimal separator is not '.' + // (which is hard-coded in the Android DigitKeyListener), then use + // our custom one with a configurable decimal separator + return GetInstance(inputTypes, decimalSeparator); + } + + public static LocalizedDigitsKeyListener GetInstance(InputTypes inputTypes, char decimalSeparator) + { + if ((inputTypes & InputTypes.NumberFlagSigned) != 0) + { + return GetInstance(inputTypes, decimalSeparator, ref s_signedCache); + } + + return GetInstance(inputTypes, decimalSeparator, ref s_unsignedCache); + } + + static LocalizedDigitsKeyListener GetInstance(InputTypes inputTypes, char decimalSeparator, ref Dictionary cache) + { + if (cache == null) + { + cache = new Dictionary(1); + } + + if (!cache.ContainsKey(decimalSeparator)) + { + cache.Add(decimalSeparator, new LocalizedDigitsKeyListener(inputTypes, decimalSeparator)); + } + + return cache[decimalSeparator]; + } + + protected LocalizedDigitsKeyListener(InputTypes inputTypes, char decimalSeparator) + { + _decimalSeparator = decimalSeparator; + InputType = inputTypes; + } + + public override InputTypes InputType { get; } + + char[] _acceptedChars; + + protected override char[] GetAcceptedChars() + { + if ((InputType & InputTypes.NumberFlagSigned) == 0) + { + return _acceptedChars ?? + (_acceptedChars = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', _decimalSeparator }); + } + + return _acceptedChars ?? + (_acceptedChars = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', SignCharacter, _decimalSeparator }); + } + + static bool IsSignChar(char c) + { + return c == SignCharacter; + } + + bool IsDecimalPointChar(char c) + { + return c == _decimalSeparator; + } + + public override ICharSequence FilterFormatted(ICharSequence source, int start, int end, ISpanned dest, int dstart, + int dend) + { + // Borrowed heavily from the Android source + ICharSequence filterFormatted = base.FilterFormatted(source, start, end, dest, dstart, dend); + + if (filterFormatted != null) + { + source = filterFormatted; + start = 0; + end = filterFormatted.Length(); + } + + int sign = -1; + int dec = -1; + int dlen = dest.Length(); + + // Find out if the existing text has a sign or decimal point characters. + for (var i = 0; i < dstart; i++) + { + char c = dest.CharAt(i); + if (IsSignChar(c)) + { + sign = i; + } + else if (IsDecimalPointChar(c)) + { + dec = i; + } + } + + for (int i = dend; i < dlen; i++) + { + char c = dest.CharAt(i); + if (IsSignChar(c)) + { + return new String(""); // Nothing can be inserted in front of a sign character. + } + + if (IsDecimalPointChar(c)) + { + dec = i; + } + } + + // If it does, we must strip them out from the source. + // In addition, a sign character must be the very first character, + // and nothing can be inserted before an existing sign character. + // Go in reverse order so the offsets are stable. + SpannableStringBuilder stripped = null; + for (int i = end - 1; i >= start; i--) + { + char c = source.CharAt(i); + var strip = false; + + if (IsSignChar(c)) + { + if (i != start || dstart != 0) + { + strip = true; + } + else if (sign >= 0) + { + strip = true; + } + else + { + sign = i; + } + } + else if (IsDecimalPointChar(c)) + { + if (dec >= 0) + { + strip = true; + } + else + { + dec = i; + } + } + + if (strip) + { + if (end == start + 1) + { + return new String(""); // Only one character, and it was stripped. + } + if (stripped == null) + { + stripped = new SpannableStringBuilder(source, start, end); + } + stripped.Delete(i - start, i + 1 - start); + } + } + + return stripped ?? filterFormatted; + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj index b622ecb..89cf67e 100644 --- a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj +++ b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj @@ -172,6 +172,7 @@ + @@ -266,9 +267,7 @@ - - - + -- 2.7.4