浏览代码

Merge pull request #5735 from ili/android-keyboard

Android keyboard support
Max Katz 4 年之前
父节点
当前提交
34c3f05ced

+ 1 - 1
samples/ControlCatalog/Pages/TextBoxPage.xaml

@@ -13,7 +13,7 @@
       <StackPanel Orientation="Vertical" Spacing="8">
         <TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" />
         <TextBox Width="200" Watermark="ReadOnly" IsReadOnly="True" Text="This is read only"/>
-        <TextBox Width="200" Watermark="Watermark" />
+        <TextBox Width="200" Watermark="Numeric Watermark" x:Name="numericWatermark"/>
         <TextBox Width="200"
                  Watermark="Floating Watermark"
                  UseFloatingWatermark="True"

+ 6 - 0
samples/ControlCatalog/Pages/TextBoxPage.xaml.cs

@@ -13,6 +13,12 @@ namespace ControlCatalog.Pages
         private void InitializeComponent()
         {
             AvaloniaXamlLoader.Load(this);
+
+            this.Get<TextBox>("numericWatermark")
+                .TextInputOptionsQuery += (s, a) =>
+                {
+                    a.ContentType = Avalonia.Input.TextInput.TextInputContentType.Number;
+                };
         }
     }
 }

+ 96 - 0
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<TView> : 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<InputMethodManager>();
+
+            _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);
+        }
+    }
+}

+ 0 - 4
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);
     }
 }

+ 12 - 0
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<EditorInfo> init);
+    }
+}

+ 1 - 1
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()

+ 1 - 1
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)
         {

+ 30 - 16
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<TopLevelImpl> _keyboardHelper;
         private readonly AndroidTouchEventsHelper<TopLevelImpl> _touchHelper;
-
+        private readonly ITextInputMethodImpl _textInputMethod;
         private ViewImpl _view;
 
         public TopLevelImpl(Context context, bool placeOnTop = false)
         {
             _view = new ViewImpl(context, this, placeOnTop);
+            _textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
             _keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
             _touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(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<EditorInfo> _initEditorInfo;
+
+            public void InitEditorInfo(Action<EditorInfo> 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();

+ 17 - 66
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<TView> : 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<InputMethodManager>();
-
-            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;

+ 42 - 0
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;
+        }
+    }
+}

+ 24 - 4
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<App>()
                     .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;
+        }
     }
 }