diff options
author | E.Z. Hart <hartez@gmail.com> | 2017-01-25 11:54:54 -0700 |
---|---|---|
committer | Rui Marinho <me@ruimarinho.net> | 2017-02-17 11:11:46 +0000 |
commit | 2ad9cb93f47f46fcb0584370ab8c297b20912718 (patch) | |
tree | 96868b61a3a9da1fa98ce4bb0d74e7d0c61e76c8 | |
parent | a1c7f9909a16d3253d9afd5cb2e1a839c6fb5a8c (diff) | |
download | xamarin-forms-2ad9cb93f47f46fcb0584370ab8c297b20912718.tar.gz xamarin-forms-2ad9cb93f47f46fcb0584370ab8c297b20912718.tar.bz2 xamarin-forms-2ad9cb93f47f46fcb0584370ab8c297b20912718.zip |
Add localized listener for Android numeric input
9 files changed, 362 insertions, 7 deletions
diff --git a/Xamarin.Forms.ControlGallery.Android/CustomRenderers.cs b/Xamarin.Forms.ControlGallery.Android/CustomRenderers.cs index 86876a38..4c6c1a89 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 00000000..77436bcd --- /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 e98c36c8..e5119636 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 @@ <Compile Include="$(MSBuildThisFileDirectory)Bugzilla41424.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Bugzilla41600.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Bugzilla41619.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)Bugzilla42000.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Bugzilla42069.cs" /> <Compile Include="$(MSBuildThisFileDirectory)Bugzilla42069_Page.xaml.cs"> <DependentUpon>Bugzilla42069_Page.xaml</DependentUpon> diff --git a/Xamarin.Forms.Platform.Android/Cells/EntryCellRenderer.cs b/Xamarin.Forms.Platform.Android/Cells/EntryCellRenderer.cs index e46422a0..bfe8777f 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 c9b596bc..dba55ea8 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 c1c24a83..7d533708 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 a2e60179..6941422d 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 00000000..009cffbb --- /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<char, LocalizedDigitsKeyListener> s_unsignedCache; + static Dictionary<char, LocalizedDigitsKeyListener> 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<char, LocalizedDigitsKeyListener> cache) + { + if (cache == null) + { + cache = new Dictionary<char, LocalizedDigitsKeyListener>(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 b622ecb4..89cf67e0 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 @@ <Compile Include="Renderers\KeyboardExtensions.cs" /> <Compile Include="AppCompat\PickerRenderer.cs" /> <Compile Include="AppCompat\ViewRenderer.cs" /> + <Compile Include="Renderers\LocalizedDigitsKeyListener.cs" /> <Compile Include="Renderers\MasterDetailContainer.cs" /> <Compile Include="Renderers\PageContainer.cs" /> <Compile Include="Renderers\ScrollViewContainer.cs" /> @@ -266,9 +267,7 @@ <ItemGroup> <None Include="packages.config" /> </ItemGroup> - <ItemGroup> - <Folder Include="Extensions\" /> - </ItemGroup> + <ItemGroup /> <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" /> <Import Project="..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets" Condition="Exists('..\packages\Xamarin.Android.Support.Vector.Drawable.23.3.0\build\Xamarin.Android.Support.Vector.Drawable.targets')" /> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> |