Introduction
It is quite strange that you can't easily detect if software keyboard is visible or not on Android. Because of that, it is also a complicated thing to do in Xamarin.
One time, when I was working on Xamarin application, I had a problem with software keyboard overlapping text box it was supposed to edit. If there would be some kind of system event that could be used to detect keyboard popup, this would be an easy fix. But there is not. After some Googling, I found this blog post, which pointed me in the right direction. Because I wanted reusable service for Dependency Injection inside of view models, I decided to do that a little differently.
Biggest drawback of this service is that it is a kind of hack. It does not directly bind to Android software keyboard. Instead of that, it reads the changes of global layout, which happens whenever software keyboard popups (because screen space available for application is about half of screen then). When layout changes, we can safely check if keyboard is actually visible, which is (at least that) easy to test.
Solution
Let's create a simple Xamarin Android application, with a single view like below:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="KeyboardService.View.MainPageView">
<Grid HorizontalOptions="FillAndExpand" Padding="0" ColumnSpacing="0"
x:Name="Grid" VerticalOptions="FillAndExpand" RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackLayout Grid.Row="0"
Grid.Column="0" Grid.ColumnSpan="2"
HorizontalOptions="FillAndExpand">
<Label Text="Keyboard service app"
FontSize="40" HorizontalOptions="Center"
HorizontalTextAlignment="Center" />
</StackLayout>
<Grid Grid.Row="1"
Grid.Column="1" HorizontalOptions="Fill"
VerticalOptions="FillAndExpand"
BackgroundColor="Gray" x:Name="Content">
<Grid.RowDefinitions>
<RowDefinition Height="600" />
<RowDefinition/>
</Grid.RowDefinitions>
<StackLayout Grid.Row="0" VerticalOptions="End"
HorizontalOptions="FillAndExpand">
<Label Text="{Binding Event}" FontSize="40" />
</StackLayout>
<Entry Grid.Row="1" Grid.Column="0" FontSize="40"
BackgroundColor="Gray" Text="Entry" />
</Grid>
</Grid>
</ContentPage>
It is just a simple view with single text box and single label. This is just enough to test keyboard events. Entry control raises keyboard and label should change text when keyboard service event will be invoked (on keyboard show or hide).
To implement KeyboardService
, we need to implement ViewTreeObserver.IOnGlobalLayoutListener
first. It is an Android interface so it requires Java.Lang.Object
type, which can be inherited by any class. It does not necessarily have to be Activity
as I explained in this post and how it is done in the blog post from the introduction.
internal class GlobalLayoutListener : Object, ViewTreeObserver.IOnGlobalLayoutListener
{
private static InputMethodManager _inputManager;
private static void ObtainInputManager()
{
_inputManager = (InputMethodManager)TinyIoCContainer.Current.Resolve<Activity>()
.GetSystemService(Context.InputMethodService);
}
public void OnGlobalLayout()
{
if (_inputManager.Handle == IntPtr.Zero)
{
ObtainInputManager();
}
}
}
To ease obtaining of references wherever they are needed, we can use TinyIoC library inside the project for Dependency Injection. This way, we can easily use MainActivity
class as Activity
inside GlobalLayoutListener
, to get Android InputMethodManager
object, which can be used to test if software keyboard is visible or not.
Sometimes, reference to InputMethodManager
inside _inputManager
value is no longer valid. It mainly happens whenever OS sends application into background or otherwise it is no longer visible, hence the if (_inputManager.Handle == IntPtr.Zero)
condition. Whenever handle to Java object is invalid, new reference to InputMethodManager
is created.
Testing if keyboard is visible, can be done by checking value of IsAcceptingText
property. If it is true
- software keyboard accepts text input and it is visible.
if (_inputManager.IsAcceptingText)
{
}
else
{
}
To implement software keyboards events and invoke them from the above code, we need to create service interface first with those events.
public interface ISoftwareKeyboardService
{
event SoftwareKeyboardEventHandler Hide;
event SoftwareKeyboardEventHandler Show;
}
We need only two events for hiding and showing of software keyboard. Events delegate and its event arguments type are very simple classes.
public delegate void SoftwareKeyboardEventHandler(object sender, SoftwareKeyboardEventArgs args);
public class SoftwareKeyboardEventArgs : EventArgs
{
public SoftwareKeyboardEventArgs(bool isVisible)
{
IsVisible = isVisible;
}
public bool IsVisible { get; private set; }
}
Just a simple IsVisible
property to check if keyboard is visible or not.
Generally, it is a good idea to write as much code in shared project as possible, so we should split implementation of service interface into two classes: SoftwareKeyboardServiceBase
inside shared project (or PCL) and SoftwareKeyboardService
in Android platform project. We can't do both in the same type because GlobalLayoutListener
is an Android platform class and cannot be used inside PCL. Also if PCL class with service interface is placed inside PCL, it possibly can be implemented on different platforms (if necessary, of course). First, we should take care of platform independent class.
public abstract class SoftwareKeyboardServiceBase : ISoftwareKeyboardService
{
public virtual event SoftwareKeyboardEventHandler Hide;
public virtual event SoftwareKeyboardEventHandler Show;
public void InvokeKeyboardHide(SoftwareKeyboardEventArgs args)
{
var handler = Hide;
handler?.Invoke(this, args);
}
public void InvokeKeyboardShow(SoftwareKeyboardEventArgs args)
{
var handler = Show;
handler?.Invoke(this, args);
}
}
It is just an implementation of ISoftwareKeyboardService
interface, plus methods for invoking keyboard events from outside of service itself. They can be easily used from GlobalLayoutListener
. To do that, we have to create an instance of listener first and register it inside Android activity. It can be done at application start, but if application would never use those events and/or service at all, it would be waste of memory. So it is a better idea to create a listener and register it, only if service and its events are used. Since events accessors can be customized in C#, we can do that in those. Platform implementation of SoftwareKeyboardService
will then look like this:
public class SoftwareKeyboardService : SoftwareKeyboardServiceBase
{
private readonly MainActivity _activity;
private GlobalLayoutListener _globalLayoutListener;
public SoftwareKeyboardService(Activity activity)
{
_activity = activity;
}
public override event SoftwareKeyboardEventHandler Hide
{
add
{
base.Hide += value;
CheckListener();
}
remove { base.Hide -= value; }
}
public override event SoftwareKeyboardEventHandler Show
{
add
{
base.Show += value;
CheckListener();
}
remove { base.Show -= value; }
}
private void CheckListener()
{
if (_globalLayoutListener == null)
{
_globalLayoutListener = new GlobalLayoutListener(this);
_activity.Window.DecorView.ViewTreeObserver.AddOnGlobalLayoutListener(_globalLayoutListener);
}
}
}
In add accessors of both Hide
and Show
events, CheckListener
method is executed. It takes care of creating instance of GlobalLayoutListener
and registering it in ViewTreeObserver
obtained from activity, first time event handler is added to one of service events. This is nice.
Activity
will be resolved from IoC, if service will be resolved from IoC too. If not, it has to be injected in some other way. Of course, it is also possible to tap into ViewTreeObserver
in some other way and since this object is available from any Android control, it is totally possible, but getting it from the main Android window is really convenient.
As you can see, there is new custom constructor of GlobalLayoutListener
type with service instance as a parameter.
public GlobalLayoutListener(SoftwareKeyboardService softwareKeyboardService)
{
_softwareKeyboardService = softwareKeyboardService;
ObtainInputManager();
}
Saving service instance allows us to use it whenever global layout changes to invoke appropriate event inside OnGlobalLayout
method.
if (_inputManager.IsAcceptingText)
{
_softwareKeyboardService.InvokeKeyboardShow(new SoftwareKeyboardEventArgs(true));
}
else
{
_softwareKeyboardService.InvokeKeyboardHide(new SoftwareKeyboardEventArgs(false));
}
Last thing is, to create view model for our main page with keyboard service instance. The most important parts of this class are below:
public MainPageViewModel()
{
var keyboardService = TinyIoCContainer.Current.Resolve<ISoftwareKeyboardService>();
keyboardService.Hide += _keyboardService_Hide;
keyboardService.Show += _keyboardService_Show;
}
private void _keyboardService_Show(object sender, SoftwareKeyboardEventArgs args)
{
Event = "Show event handler invoked";
}
private void _keyboardService_Hide(object sender, SoftwareKeyboardEventArgs args)
{
Event = "Hide event handler invoked";
}
public string Event
{
get { return _event; }
set
{
_event = value;
OnPropertyChanged();
}
}
Keyboard service instance is resolved from IoC during view model constructor. Then events handlers are attached to events and in the background GlobalLayoutListener
is created and added to ViewTreeObserver
. Whenever keyboard shows or hides, value of label should change.
This is enough to make our service work. After starting our simple application, we can check if this works.
As you can see, label value changes on software popup, which means the service works fine.
Summary
This is all that is needed to make reusable Xamarin Android software keyboard service.
The code from the article is available for download here or from GitHub.