Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Xamarin

Detecting Software Keyboard Events in Xamarin Android

4.82/5 (8 votes)
26 Feb 2017CPOL5 min read 29.2K   439  
Reusable service for detecting if software keyboard is visible or not on Xamarin Android

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:

XML
<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.

C#
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 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.

C#
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.

C#
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.

C#
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.

C#
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:

C#
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.

C#
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.

C#
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:

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)