diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 1a68c4d732..9da9627a6f 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -158,4 +158,4 @@ - + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index 1a53217842..363330be86 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -14,7 +14,8 @@ + MinimumPrefixLength="1" + /> - + - View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e); + //public override bool DispatchKeyEvent(KeyEvent e) => + // View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e); } } diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 8de3657283..f78f383041 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/src/Android/Avalonia.Android/AvaloniaView.cs @@ -2,6 +2,8 @@ using System; using Android.Content; using Android.Runtime; using Android.Views; +using Android.Views.Accessibility; +using Android.Views.InputMethods; using Android.Widget; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; @@ -23,6 +25,9 @@ namespace Avalonia.Android AddView(_view.View); _root = new EmbeddableControlRoot(_view); _root.Prepare(); + + Focusable = true; + FocusableInTouchMode = true; } public object Content @@ -89,5 +94,26 @@ namespace Avalonia.Android public WindowState WindowState { get; set; } public IDisposable ShowDialog() => null; } + + public override void OnInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) + { + base.OnInitializeAccessibilityNodeInfo(info); + } + + public override AccessibilityNodeInfo CreateAccessibilityNodeInfo() + { + return base.CreateAccessibilityNodeInfo(); + } + + public override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) + { + outAttrs.InputType = global::Android.Text.InputTypes.ClassNumber; + return base.OnCreateInputConnection(outAttrs); + } + + public override bool OnCheckIsTextEditor() + { + return true; + } } } 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..a1c2b2d9fb 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs @@ -2,23 +2,31 @@ using System; using Android.Content; using Android.Graphics; using Android.OS; +using Android.Runtime; using Android.Util; using Android.Views; +using Android.Views.Accessibility; +using Android.Views.InputMethods; +using Avalonia.Android.Platform.Specific; +using Avalonia.Input; using Avalonia.Platform; namespace Avalonia.Android { - public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformHandle + public abstract class InvalidationAwareSurfaceView : SurfaceView, ISurfaceHolderCallback, IPlatformHandle, IAndroidSoftInput { bool _invalidateQueued; + private ISoftInputElement _softInputElement; readonly object _lock = new object(); private readonly Handler _handler; - public InvalidationAwareSurfaceView(Context context) : base(context) { Holder.AddCallback(this); _handler = new Handler(context.MainLooper); + + Focusable = true; + FocusableInTouchMode = true; } public override void Invalidate() @@ -83,5 +91,60 @@ namespace Avalonia.Android } protected abstract void Draw(); public string HandleDescriptor => "SurfaceView"; + + public override IInputConnection OnCreateInputConnection(EditorInfo outAttrs) + { + outAttrs.InputType = _softInputElement.InputType switch + { + InputType.Numeric => global::Android.Text.InputTypes.ClassNumber, + InputType.Phone => global::Android.Text.InputTypes.ClassPhone, + _ => global::Android.Text.InputTypes.Null + }; + + return base.OnCreateInputConnection(outAttrs); + } + + + public override bool CheckInputConnectionProxy(View view) + { + return base.CheckInputConnectionProxy(view); + } + + public override void OnInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) + { + base.OnInitializeAccessibilityNodeInfo(info); + } + + public override bool OnCheckIsTextEditor() + { + return true; + } + + public void ShowSoftInput(ISoftInputElement softInputElement) + { + var input = Context.GetSystemService(Context.InputMethodService).JavaCast(); + var previousSoftInput = _softInputElement; + _softInputElement = softInputElement; + + if (_softInputElement.InputType == InputType.None) + HideSoftInput(); + else + { + RequestFocus(); + + if (!ReferenceEquals(_softInputElement, previousSoftInput)) + { + input.RestartInput(this); + } + + input.ToggleSoftInput(ShowFlags.Forced, HideSoftInputFlags.NotAlways); + } + } + + public void HideSoftInput() + { + var input = Context.GetSystemService(Context.InputMethodService).JavaCast(); + input.HideSoftInputFromWindow(WindowToken, HideSoftInputFlags.None); + } } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index fe237a1719..219dd4cf67 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -19,7 +19,7 @@ using Avalonia.Rendering; namespace Avalonia.Android.Platform.SkiaPlatform { - class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo + class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, IAndroidSoftInput { private readonly IGlPlatformSurface _gl; private readonly IFramebufferPlatformSurface _framebuffer; @@ -43,18 +43,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); + + _keyboardHelper.ActivateAutoShowKeyboard(); } - private bool _handleEvents; public bool HandleEvents { - get { return _handleEvents; } - set - { - _handleEvents = value; - _keyboardHelper.HandleEvents = _handleEvents; - } + get { return _keyboardHelper.HandleEvents; } + set { _keyboardHelper.HandleEvents = value; } } public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => @@ -213,5 +210,15 @@ namespace Avalonia.Android.Platform.SkiaPlatform { throw new NotImplementedException(); } + + void IAndroidSoftInput.ShowSoftInput(ISoftInputElement softInputElement) + { + _view.ShowSoftInput(softInputElement); + } + + void IAndroidSoftInput.HideSoftInput() + { + _view.HideSoftInput(); + } } } diff --git a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs index 426b221738..264c9f4cb5 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs @@ -1,9 +1,11 @@ using System; using System.ComponentModel; +using Android.App; using Android.Content; using Android.Runtime; using Android.Views; using Android.Views.InputMethods; +using Android.Widget; using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Controls; @@ -12,7 +14,7 @@ using Avalonia.Input.Raw; namespace Avalonia.Android.Platform.Specific.Helpers { - internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView + internal class AndroidKeyboardEventsHelper : IDisposable where TView : TopLevelImpl, IAndroidView, IAndroidSoftInput { private TView _view; private IInputElement _lastFocusedElement; @@ -36,9 +38,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 +66,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); } @@ -88,41 +102,49 @@ namespace Avalonia.Android.Platform.Specific.Helpers private bool NeedsKeyboard(IInputElement element) { //may be some other elements - return element is TextBox; + return element is ISoftInputElement; } - private void TryShowHideKeyboard(IInputElement element, bool value) + private void TryShowHideKeyboard(ISoftInputElement 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); + _view.ShowSoftInput(element); } else { - //hide keyboard - input.HideSoftInputFromWindow(_view.View.WindowToken, HideSoftInputFlags.None); + _view.HideSoftInput(); } } public void UpdateKeyboardState(IInputElement element) { - var focusedElement = element; - bool oldValue = NeedsKeyboard(_lastFocusedElement); - bool newValue = NeedsKeyboard(focusedElement); + var focusedElement = element as ISoftInputElement; + var lastElement = _lastFocusedElement as ISoftInputElement; + + bool oldValue = lastElement?.InputType > InputType.None; + bool newValue = focusedElement?.InputType > InputType.None; if (newValue != oldValue || newValue) { + if (_lastFocusedElement != null) + _lastFocusedElement.PointerReleased -= RestoreSoftKeyboard; + TryShowHideKeyboard(focusedElement, newValue); + + if (newValue && focusedElement != null) + element.PointerReleased += RestoreSoftKeyboard; } _lastFocusedElement = element; } + private void RestoreSoftKeyboard(object sender, PointerReleasedEventArgs e) + { + if (_lastFocusedElement is ISoftInputElement softInputElement && softInputElement.InputType != InputType.None) + TryShowHideKeyboard(softInputElement, true); + } + public void ActivateAutoShowKeyboard() { var kbDevice = (KeyboardDevice.Instance as INotifyPropertyChanged); diff --git a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs index c72de8e197..db65a1335b 100644 --- a/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs +++ b/src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs @@ -1,9 +1,19 @@ using Android.Views; +using Avalonia.Input; namespace Avalonia.Android.Platform.Specific { public interface IAndroidView { View View { get; } + + + } + + public interface IAndroidSoftInput + { + void ShowSoftInput(ISoftInputElement softInputElement); + + void HideSoftInput(); } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1d75f08a41..06729fa990 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -18,7 +18,7 @@ using Avalonia.Controls.Metadata; namespace Avalonia.Controls { [PseudoClasses(":empty")] - public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost + public class TextBox : TemplatedControl, UndoRedoHelper.IUndoRedoHost, ISoftInputElement { public static KeyGesture CutGesture { get; } = AvaloniaLocator.Current .GetService()?.Cut.FirstOrDefault(); @@ -130,6 +130,11 @@ namespace Avalonia.Controls nameof(CanPaste), o => o.CanPaste); + public static readonly DirectProperty InputTypeProperty = + AvaloniaProperty.RegisterDirect( + nameof(InputType), + o => o.InputType); + struct UndoRedoState : IEquatable { public string Text { get; } @@ -1243,5 +1248,7 @@ namespace Avalonia.Controls ClearSelection(); } } + + public InputType InputType { get; set; } = InputType.Text; } } diff --git a/src/Avalonia.Input/ISoftInputElement.cs b/src/Avalonia.Input/ISoftInputElement.cs new file mode 100644 index 0000000000..ed5e4e59e3 --- /dev/null +++ b/src/Avalonia.Input/ISoftInputElement.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Input +{ + public interface ISoftInputElement + { + InputType InputType { get; } + } +} diff --git a/src/Avalonia.Input/InputType.cs b/src/Avalonia.Input/InputType.cs new file mode 100644 index 0000000000..893f9dcc39 --- /dev/null +++ b/src/Avalonia.Input/InputType.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Input +{ + /// + /// Input type enumeration + /// + public enum InputType + { + /// + /// Do not use input + /// + None, + + /// + /// User full text input + /// + Text, + + /// + /// Use numeric text input + /// + Numeric, + + /// + /// Use phone input + /// + Phone + } +}