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 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 supposed to edit. If there would be some kind of system event that could be used to detect keyboard popup, this would be 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 that a little differently.

Biggest drawback of this service is that it is kind of hack. It do 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 simple Xamarin Android application, with 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 Android interface so it requires Java.Lang.Object type, which can be inherited by any class. It do not necessarily have to be Activity as I explained in this post and how it is done in blog post from 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();
        }
        //Keyboard service events
    }
}

To ease obtaining of references wherever they 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)
{
    //Invoke Show event
}
else
{
    //Invoke Hide event
}

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 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 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 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 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 better idea, to create 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 some other way. Of course, it is also possible to tap into ViewTreeObserver some other way and since, this object is available from any Android control, it is totally possible, but getting it from 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 allow 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. 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 service works fine.

Summary

This is all what is needed to make reusable Xamarin Android software keyboard service.

Code from article is available for download here: KeyboardService-master.zip (43.90 kb) or from GitHub.