diff --git a/samples/ControlCatalog/Pages/TextBoxPage.xaml b/samples/ControlCatalog/Pages/TextBoxPage.xaml index 2030c6e744..1ac447ea69 100644 --- a/samples/ControlCatalog/Pages/TextBoxPage.xaml +++ b/samples/ControlCatalog/Pages/TextBoxPage.xaml @@ -13,7 +13,7 @@ - + ("numericWatermark") + .TextInputOptionsQuery += (s, a) => + { + a.ContentType = Avalonia.Input.TextInput.TextInputContentType.Number; + }; } } } diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs new file mode 100644 index 0000000000..7e49cb5dfa --- /dev/null +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -0,0 +1,96 @@ +using System; +using Android.Content; +using Android.Runtime; +using Android.Views; +using Android.Views.InputMethods; +using Avalonia.Input; +using Avalonia.Input.TextInput; + +namespace Avalonia.Android +{ + class AndroidInputMethod : ITextInputMethodImpl + where TView: View, IInitEditorInfo + { + private readonly TView _host; + private readonly InputMethodManager _imm; + private IInputElement _inputElement; + + public AndroidInputMethod(TView host) + { + if (host.OnCheckIsTextEditor() == false) + throw new InvalidOperationException("Host should return true from OnCheckIsTextEditor()"); + + _host = host; + _imm = host.Context.GetSystemService(Context.InputMethodService).JavaCast(); + + _host.Focusable = true; + _host.FocusableInTouchMode = true; + _host.ViewTreeObserver.AddOnGlobalLayoutListener(new SoftKeyboardListner(_host)); + } + + public void Reset() + { + _imm.RestartInput(_host); + } + + public void SetActive(bool active) + { + if (active) + { + _host.RequestFocus(); + Reset(); + _imm.ShowSoftInput(_host, ShowFlags.Implicit); + } + else + _imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None); + } + + public void SetCursorRect(Rect rect) + { + } + + public void SetOptions(TextInputOptionsQueryEventArgs options) + { + if (_inputElement != null) + { + _inputElement.PointerReleased -= RestoreSoftKeyboard; + } + + _inputElement = options.Source as InputElement; + + if (_inputElement == null) + { + _imm.HideSoftInputFromWindow(_host.WindowToken, HideSoftInputFlags.None); + } + + _host.InitEditorInfo((outAttrs) => + { + outAttrs.InputType = options.ContentType switch + { + TextInputContentType.Email => global::Android.Text.InputTypes.TextVariationEmailAddress, + TextInputContentType.Number => global::Android.Text.InputTypes.ClassNumber, + TextInputContentType.Password => global::Android.Text.InputTypes.TextVariationPassword, + TextInputContentType.Phone => global::Android.Text.InputTypes.ClassPhone, + TextInputContentType.Url => global::Android.Text.InputTypes.TextVariationUri, + _ => global::Android.Text.InputTypes.ClassText + }; + + if (options.AutoCapitalization) + { + outAttrs.InitialCapsMode = global::Android.Text.CapitalizationMode.Sentences; + outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagCapSentences; + } + + if (options.Multiline) + outAttrs.InputType |= global::Android.Text.InputTypes.TextFlagMultiLine; + }); + + //_inputElement.PointerReleased += RestoreSoftKeyboard; + } + + private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e) + { + _imm.ShowSoftInput(_host, ShowFlags.Implicit); + } + } +} diff --git a/src/Android/Avalonia.Android/AvaloniaActivity.cs b/src/Android/Avalonia.Android/AvaloniaActivity.cs index b3a7585520..3c9f373a66 100644 --- a/src/Android/Avalonia.Android/AvaloniaActivity.cs +++ b/src/Android/Avalonia.Android/AvaloniaActivity.cs @@ -15,7 +15,6 @@ namespace Avalonia.Android if (_content != null) View.Content = _content; SetContentView(View); - TakeKeyEvents(true); base.OnCreate(savedInstanceState); } @@ -32,8 +31,5 @@ namespace Avalonia.Android View.Content = value; } } - - public override bool DispatchKeyEvent(KeyEvent e) => - View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e); } } diff --git a/src/Android/Avalonia.Android/IInitEditorInfo.cs b/src/Android/Avalonia.Android/IInitEditorInfo.cs new file mode 100644 index 0000000000..98fc4eafc1 --- /dev/null +++ b/src/Android/Avalonia.Android/IInitEditorInfo.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Android.Views.InputMethods; + +namespace Avalonia.Android +{ + interface IInitEditorInfo + { + void InitEditorInfo(Action init); + } +} diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs index b115917622..aabf8160f8 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs @@ -32,7 +32,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4); Address = buffer.bits; - Dpi = scaling * new Vector(96, 96); + Dpi = new Vector(96, 96) * scaling; } public void Dispose() diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs index 16c5bdae3d..34784612f1 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -13,7 +13,7 @@ namespace Avalonia.Android bool _invalidateQueued; readonly object _lock = new object(); private readonly Handler _handler; - + public InvalidationAwareSurfaceView(Context context) : base(context) { diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index fe237a1719..60b772a183 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -4,14 +4,16 @@ using Android.Content; using Android.Graphics; using Android.Runtime; using Android.Views; - +using Android.Views.InputMethods; using Avalonia.Android.OpenGL; using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Controls; +using Avalonia.Controls.Platform; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; @@ -19,19 +21,20 @@ using Avalonia.Rendering; namespace Avalonia.Android.Platform.SkiaPlatform { - class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo + class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod { private readonly IGlPlatformSurface _gl; private readonly IFramebufferPlatformSurface _framebuffer; private readonly AndroidKeyboardEventsHelper _keyboardHelper; private readonly AndroidTouchEventsHelper _touchHelper; - + private readonly ITextInputMethodImpl _textInputMethod; private ViewImpl _view; public TopLevelImpl(Context context, bool placeOnTop = false) { _view = new ViewImpl(context, this, placeOnTop); + _textInputMethod = new AndroidInputMethod(_view); _keyboardHelper = new AndroidKeyboardEventsHelper(this); _touchHelper = new AndroidTouchEventsHelper(this, () => InputRoot, GetAvaloniaPointFromEvent); @@ -45,18 +48,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); } - private bool _handleEvents; - - public bool HandleEvents - { - get { return _handleEvents; } - set - { - _handleEvents = value; - _keyboardHelper.HandleEvents = _handleEvents; - } - } - public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling; @@ -144,7 +135,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform Resized?.Invoke(size); } - class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback + class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo { private readonly TopLevelImpl _tl; private Size _oldSize; @@ -191,6 +182,27 @@ namespace Avalonia.Android.Platform.SkiaPlatform base.SurfaceChanged(holder, format, width, height); } + + public sealed override bool OnCheckIsTextEditor() + { + return true; + } + + private Action _initEditorInfo; + + public void InitEditorInfo(Action init) + { + _initEditorInfo = init; + } + + public sealed override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) + { + if (_initEditorInfo != null) + _initEditorInfo(outAttrs); + + return base.OnCreateInputConnection(outAttrs); + } + } public IPopupImpl CreatePopup() => null; @@ -209,6 +221,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform public double Scaling => RenderScaling; + public ITextInputMethodImpl TextInputMethod => _textInputMethod; + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { throw new NotImplementedException(); diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs index 426b221738..2b2a9dd2b4 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs @@ -1,12 +1,7 @@ using System; -using System.ComponentModel; -using Android.Content; -using Android.Runtime; using Android.Views; -using Android.Views.InputMethods; using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.SkiaPlatform; -using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Raw; @@ -14,14 +9,13 @@ namespace Avalonia.Android.Platform.Specific.Helpers { internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView { - private TView _view; - private IInputElement _lastFocusedElement; + private readonly TView _view; public bool HandleEvents { get; set; } public AndroidKeyboardEventsHelper(TView view) { - this._view = view; + _view = view; HandleEvents = true; } @@ -36,9 +30,20 @@ namespace Avalonia.Android.Platform.Specific.Helpers return DispatchKeyEventInternal(e, out callBase); } + string? UnicodeTextInput(KeyEvent keyEvent) + { + return keyEvent.Action == KeyEventActions.Multiple + && keyEvent.RepeatCount == 0 + && !string.IsNullOrEmpty(keyEvent?.Characters) + ? keyEvent.Characters + : null; + } + private bool? DispatchKeyEventInternal(KeyEvent e, out bool callBase) { - if (e.Action == KeyEventActions.Multiple) + var unicodeTextInput = UnicodeTextInput(e); + + if (e.Action == KeyEventActions.Multiple && unicodeTextInput == null) { callBase = true; return null; @@ -53,13 +58,14 @@ namespace Avalonia.Android.Platform.Specific.Helpers _view.Input(rawKeyEvent); - if (e.Action == KeyEventActions.Down && e.UnicodeChar >= 32) + if ((e.Action == KeyEventActions.Down && e.UnicodeChar >= 32) + || unicodeTextInput != null) { var rawTextEvent = new RawTextInputEventArgs( AndroidKeyboardDevice.Instance, Convert.ToUInt32(e.EventTime), _view.InputRoot, - Convert.ToChar(e.UnicodeChar).ToString() + unicodeTextInput ?? Convert.ToChar(e.UnicodeChar).ToString() ); _view.Input(rawTextEvent); } @@ -85,61 +91,6 @@ namespace Avalonia.Android.Platform.Specific.Helpers return rv; } - private bool NeedsKeyboard(IInputElement element) - { - //may be some other elements - return element is TextBox; - } - - private void TryShowHideKeyboard(IInputElement element, bool value) - { - var input = _view.View.Context.GetSystemService(Context.InputMethodService).JavaCast(); - - if (value) - { - //show keyboard - //may be in the future different keyboards support e.g. normal, only digits etc. - //Android.Text.InputTypes - input.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.ImplicitOnly); - } - else - { - //hide keyboard - input.HideSoftInputFromWindow(_view.View.WindowToken, HideSoftInputFlags.None); - } - } - - public void UpdateKeyboardState(IInputElement element) - { - var focusedElement = element; - bool oldValue = NeedsKeyboard(_lastFocusedElement); - bool newValue = NeedsKeyboard(focusedElement); - - if (newValue != oldValue || newValue) - { - TryShowHideKeyboard(focusedElement, newValue); - } - - _lastFocusedElement = element; - } - - public void ActivateAutoShowKeyboard() - { - var kbDevice = (KeyboardDevice.Instance as INotifyPropertyChanged); - - //just in case we've called more than once the method - kbDevice.PropertyChanged -= KeyboardDevice_PropertyChanged; - kbDevice.PropertyChanged += KeyboardDevice_PropertyChanged; - } - - private void KeyboardDevice_PropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(KeyboardDevice.FocusedElement)) - { - UpdateKeyboardState(KeyboardDevice.Instance.FocusedElement); - } - } - public void Dispose() { HandleEvents = false; diff --git a/src/Android/Avalonia.Android/SoftKeyboardListner.cs b/src/Android/Avalonia.Android/SoftKeyboardListner.cs new file mode 100644 index 0000000000..df658f6314 --- /dev/null +++ b/src/Android/Avalonia.Android/SoftKeyboardListner.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Android.Content; +using Android.OS; +using Android.Util; +using Android.Views; +using Avalonia.Input; + +namespace Avalonia.Android +{ + class SoftKeyboardListner : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener + { + private const int DefaultKeyboardHeightDP = 100; + private static readonly int EstimatedKeyboardDP = DefaultKeyboardHeightDP + (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop ? 48 : 0); + + private readonly View _host; + private bool _wasKeyboard; + + public SoftKeyboardListner(View view) + { + _host = view; + } + + public void OnGlobalLayout() + { + int estimatedKeyboardHeight = (int)TypedValue.ApplyDimension(ComplexUnitType.Dip, + EstimatedKeyboardDP, _host.Resources.DisplayMetrics); + + var rect = new global::Android.Graphics.Rect(); + _host.GetWindowVisibleDisplayFrame(rect); + + int heightDiff = _host.RootView.Height - (rect.Bottom - rect.Top); + var isKeyboard = heightDiff >= estimatedKeyboardHeight; + + if (_wasKeyboard && !isKeyboard) + KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); + + _wasKeyboard = isKeyboard; + } + } +} diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs index 121acb6351..5f33cadf2e 100644 --- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs +++ b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs @@ -16,18 +16,18 @@ namespace Avalonia.AndroidTestApplication Icon = "@drawable/icon", LaunchMode = LaunchMode.SingleInstance/*, ScreenOrientation = ScreenOrientation.Landscape*/)] - public class MainBaseActivity : Activity + public class MainBaseActivity : AvaloniaActivity { protected override void OnCreate(Bundle savedInstanceState) { - base.OnCreate(savedInstanceState); if (Avalonia.Application.Current == null) { AppBuilder.Configure() .UseAndroid() .SetupWithoutStarting(); } - SetContentView(new AvaloniaView(this) { Content = App.CreateSimpleWindow() }); + base.OnCreate(savedInstanceState); + Content = App.CreateSimpleWindow(); } } @@ -72,13 +72,33 @@ namespace Avalonia.AndroidTestApplication Height = 40, Background = Brushes.LightGreen, Foreground = Brushes.Black - } + }, + CreateTextBox(Input.TextInput.TextInputContentType.Normal), + CreateTextBox(Input.TextInput.TextInputContentType.Password), + CreateTextBox(Input.TextInput.TextInputContentType.Email), + CreateTextBox(Input.TextInput.TextInputContentType.Url), + CreateTextBox(Input.TextInput.TextInputContentType.Phone), + CreateTextBox(Input.TextInput.TextInputContentType.Number), } } }; return window; } + + private static TextBox CreateTextBox(Input.TextInput.TextInputContentType contentType) + { + var textBox = new TextBox() + { + Margin = new Thickness(20, 10), + Watermark = contentType.ToString(), + BorderThickness = new Thickness(3), + FontSize = 20 + }; + textBox.TextInputOptionsQuery += (s, e) => { e.ContentType = contentType; }; + + return textBox; + } } }