Browse Source

Merge pull request #5735 from ili/android-keyboard

Android keyboard support
pull/5787/head
Max Katz 6 years ago
committed by GitHub
parent
commit
34c3f05ced
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  2. 6
      samples/ControlCatalog/Pages/TextBoxPage.xaml.cs
  3. 96
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  4. 4
      src/Android/Avalonia.Android/AvaloniaActivity.cs
  5. 12
      src/Android/Avalonia.Android/IInitEditorInfo.cs
  6. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/AndroidFramebuffer.cs
  7. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  8. 46
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  9. 83
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
  10. 42
      src/Android/Avalonia.Android/SoftKeyboardListner.cs
  11. 28
      src/Android/Avalonia.AndroidTestApplication/MainActivity.cs

2
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -13,7 +13,7 @@
<StackPanel Orientation="Vertical" Spacing="8"> <StackPanel Orientation="Vertical" Spacing="8">
<TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" /> <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="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" <TextBox Width="200"
Watermark="Floating Watermark" Watermark="Floating Watermark"
UseFloatingWatermark="True" UseFloatingWatermark="True"

6
samples/ControlCatalog/Pages/TextBoxPage.xaml.cs

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

96
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);
}
}
}

4
src/Android/Avalonia.Android/AvaloniaActivity.cs

@ -15,7 +15,6 @@ namespace Avalonia.Android
if (_content != null) if (_content != null)
View.Content = _content; View.Content = _content;
SetContentView(View); SetContentView(View);
TakeKeyEvents(true);
base.OnCreate(savedInstanceState); base.OnCreate(savedInstanceState);
} }
@ -32,8 +31,5 @@ namespace Avalonia.Android
View.Content = value; View.Content = value;
} }
} }
public override bool DispatchKeyEvent(KeyEvent e) =>
View.DispatchKeyEvent(e) ? true : base.DispatchKeyEvent(e);
} }
} }

12
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);
}
}

2
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); RowBytes = buffer.stride * (Format == PixelFormat.Rgb565 ? 2 : 4);
Address = buffer.bits; Address = buffer.bits;
Dpi = scaling * new Vector(96, 96); Dpi = new Vector(96, 96) * scaling;
} }
public void Dispose() public void Dispose()

2
src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs

@ -13,7 +13,7 @@ namespace Avalonia.Android
bool _invalidateQueued; bool _invalidateQueued;
readonly object _lock = new object(); readonly object _lock = new object();
private readonly Handler _handler; private readonly Handler _handler;
public InvalidationAwareSurfaceView(Context context) : base(context) public InvalidationAwareSurfaceView(Context context) : base(context)
{ {

46
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -4,14 +4,16 @@ using Android.Content;
using Android.Graphics; using Android.Graphics;
using Android.Runtime; using Android.Runtime;
using Android.Views; using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.OpenGL; using Avalonia.Android.OpenGL;
using Avalonia.Android.Platform.Specific; using Avalonia.Android.Platform.Specific;
using Avalonia.Android.Platform.Specific.Helpers; using Avalonia.Android.Platform.Specific.Helpers;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Platform.Surfaces; using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces; using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform; using Avalonia.Platform;
@ -19,19 +21,20 @@ using Avalonia.Rendering;
namespace Avalonia.Android.Platform.SkiaPlatform namespace Avalonia.Android.Platform.SkiaPlatform
{ {
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo, ITopLevelImplWithTextInputMethod
{ {
private readonly IGlPlatformSurface _gl; private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer; private readonly IFramebufferPlatformSurface _framebuffer;
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper; private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidTouchEventsHelper<TopLevelImpl> _touchHelper; private readonly AndroidTouchEventsHelper<TopLevelImpl> _touchHelper;
private readonly ITextInputMethodImpl _textInputMethod;
private ViewImpl _view; private ViewImpl _view;
public TopLevelImpl(Context context, bool placeOnTop = false) public TopLevelImpl(Context context, bool placeOnTop = false)
{ {
_view = new ViewImpl(context, this, placeOnTop); _view = new ViewImpl(context, this, placeOnTop);
_textInputMethod = new AndroidInputMethod<ViewImpl>(_view);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this); _keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
_touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(this, () => InputRoot, _touchHelper = new AndroidTouchEventsHelper<TopLevelImpl>(this, () => InputRoot,
GetAvaloniaPointFromEvent); GetAvaloniaPointFromEvent);
@ -45,18 +48,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); _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) => public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling; new Point(e.GetX(pointerIndex), e.GetY(pointerIndex)) / RenderScaling;
@ -144,7 +135,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
Resized?.Invoke(size); Resized?.Invoke(size);
} }
class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback class ViewImpl : InvalidationAwareSurfaceView, ISurfaceHolderCallback, IInitEditorInfo
{ {
private readonly TopLevelImpl _tl; private readonly TopLevelImpl _tl;
private Size _oldSize; private Size _oldSize;
@ -191,6 +182,27 @@ namespace Avalonia.Android.Platform.SkiaPlatform
base.SurfaceChanged(holder, format, width, height); 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; public IPopupImpl CreatePopup() => null;
@ -209,6 +221,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public double Scaling => RenderScaling; public double Scaling => RenderScaling;
public ITextInputMethodImpl TextInputMethod => _textInputMethod;
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{ {
throw new NotImplementedException(); throw new NotImplementedException();

83
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs

@ -1,12 +1,7 @@
using System; using System;
using System.ComponentModel;
using Android.Content;
using Android.Runtime;
using Android.Views; using Android.Views;
using Android.Views.InputMethods;
using Avalonia.Android.Platform.Input; using Avalonia.Android.Platform.Input;
using Avalonia.Android.Platform.SkiaPlatform; using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
@ -14,14 +9,13 @@ namespace Avalonia.Android.Platform.Specific.Helpers
{ {
internal class AndroidKeyboardEventsHelper<TView> : IDisposable where TView : TopLevelImpl, IAndroidView internal class AndroidKeyboardEventsHelper<TView> : IDisposable where TView : TopLevelImpl, IAndroidView
{ {
private TView _view; private readonly TView _view;
private IInputElement _lastFocusedElement;
public bool HandleEvents { get; set; } public bool HandleEvents { get; set; }
public AndroidKeyboardEventsHelper(TView view) public AndroidKeyboardEventsHelper(TView view)
{ {
this._view = view; _view = view;
HandleEvents = true; HandleEvents = true;
} }
@ -36,9 +30,20 @@ namespace Avalonia.Android.Platform.Specific.Helpers
return DispatchKeyEventInternal(e, out callBase); 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) 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; callBase = true;
return null; return null;
@ -53,13 +58,14 @@ namespace Avalonia.Android.Platform.Specific.Helpers
_view.Input(rawKeyEvent); _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( var rawTextEvent = new RawTextInputEventArgs(
AndroidKeyboardDevice.Instance, AndroidKeyboardDevice.Instance,
Convert.ToUInt32(e.EventTime), Convert.ToUInt32(e.EventTime),
_view.InputRoot, _view.InputRoot,
Convert.ToChar(e.UnicodeChar).ToString() unicodeTextInput ?? Convert.ToChar(e.UnicodeChar).ToString()
); );
_view.Input(rawTextEvent); _view.Input(rawTextEvent);
} }
@ -85,61 +91,6 @@ namespace Avalonia.Android.Platform.Specific.Helpers
return rv; 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() public void Dispose()
{ {
HandleEvents = false; HandleEvents = false;

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

28
src/Android/Avalonia.AndroidTestApplication/MainActivity.cs

@ -16,18 +16,18 @@ namespace Avalonia.AndroidTestApplication
Icon = "@drawable/icon", Icon = "@drawable/icon",
LaunchMode = LaunchMode.SingleInstance/*, LaunchMode = LaunchMode.SingleInstance/*,
ScreenOrientation = ScreenOrientation.Landscape*/)] ScreenOrientation = ScreenOrientation.Landscape*/)]
public class MainBaseActivity : Activity public class MainBaseActivity : AvaloniaActivity
{ {
protected override void OnCreate(Bundle savedInstanceState) protected override void OnCreate(Bundle savedInstanceState)
{ {
base.OnCreate(savedInstanceState);
if (Avalonia.Application.Current == null) if (Avalonia.Application.Current == null)
{ {
AppBuilder.Configure<App>() AppBuilder.Configure<App>()
.UseAndroid() .UseAndroid()
.SetupWithoutStarting(); .SetupWithoutStarting();
} }
SetContentView(new AvaloniaView(this) { Content = App.CreateSimpleWindow() }); base.OnCreate(savedInstanceState);
Content = App.CreateSimpleWindow();
} }
} }
@ -72,13 +72,33 @@ namespace Avalonia.AndroidTestApplication
Height = 40, Height = 40,
Background = Brushes.LightGreen, Background = Brushes.LightGreen,
Foreground = Brushes.Black 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; 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;
}
} }
} }

Loading…
Cancel
Save