Xamarin Forms - Hardware Keyboard

Clickety-clack ⌨

In this age of touch screens and voice assistants it can be easy to forget the humble hardware keyboard. You might think that worrying about supporting such an archaic input device, with actual physical key mechanisms, sounds incredulous! However, there are an ever increasing number of people living the iPad lifestyle and adding some shortcuts to justify their Smart Keyboard purchase will surely be well received.

I’ve chosen to approach this by sub classing ContentPage and creating custom renderers. For this demo we’ll support alphanumeric input (letters & numbers), as well as, the common keyboard shortcuts for cut, copy & paste.

Setup

First up we’ll create our custom KeyboardPage which has a Name property and two virtual methods, OnKeyUp & OnKeyCommand, that we’ll override to handle input.

Next we create two instances of our page with different names that we’ll place within tabbed navigation. Each page will have an instance of a view model that contains a collection of all the input received, which we’ll bind to a list view. The goal is when we navigate between the two tabs the unique name of the page should be displayed along with the key pressed. This demonstrates that the pages are correctly taking focus and processing the input.

public enum KeyCommand
{
    Cut,
    Copy,
    Paste
}

public class KeyboardPage : ContentPage
{
    public string Name { get; set; }

    public virtual void OnKeyUp(string text, string description) { return; }
    public virtual void OnKeyCommand(KeyCommand command) { return; }
}

Android Renderer

We’ll override OnKeyUp in our custom renderer on Android, which returns a bool; true if handled, false otherwise. To capture keyboard presses we also have to ensure our Android native control is focusable and in focus.

public class KeyboardPageRenderer : PageRenderer
{
    private KeyboardPage _page => Element as KeyboardPage;

    public KeyboardPageRenderer(Context context) : base(context)
    {
        Focusable = true;
        FocusableInTouchMode = true;
    }

    protected override void OnElementChanged(
        ElementChangedEventArgs<Page> e)
    {
        base.OnElementChanged(e);

        if (Visibility == ViewStates.Visible)
            RequestFocus();

        _page.Appearing += (sender, args) =>
        {
            RequestFocus();
        };
    }

    public override bool OnKeyUp(
        [GeneratedEnum] Keycode keyCode,
        KeyEvent e)
    {
        var handled = false;

        if (...)
        {
            // Handle command (cut, copy & paste)
            handled = true;
        }
        else if (...)
        {
            // Handle alphanumeric characters
            handled = true;
        }

        return handled || base.OnKeyUp(keyCode, e);
    }
}

iOS Renderer

We setup our KeyCommand selector and then register the UIKeyCommands we want to support. As a nice little bonus, if you set the discoverability title when constructing UIKeyCommand it will show up (along with the shortcut) when holding down the command key (>= iOS 9).

public class KeyboardPageRenderer : PageRenderer
{
    private const string KeySelector = "KeyCommand:";

    private KeyboardPage _page => Element as KeyboardPage;

    private readonly IList<UIKeyCommand> _keyCommands =
        new List<UIKeyCommand>();

    public override bool CanBecomeFirstResponder
    {
        get => true;
    }

    protected override void OnElementChanged(
        VisualElementChangedEventArgs e)
    {
        base.OnElementChanged(e);

        if (_keyCommands.Count == 0)
        {
            var selector = new ObjCRuntime.Selector(KeySelector);
            ...
            // Add UIKeyCommands
            _keyCommands.Add(
                UIKeyCommand.Create(
                    new NSString("a"),
                    0,
                    selector));
            // Repeat for alphanumeric characters
            ...
            // Set the Discoverability title to be
            // viewable (>= iOS 9) when holding down ⌘
            _keyCommands.Add(
                UIKeyCommand.Create(
                    new NSString("x"),
                    UIKeyModifierFlags.Command,
                    selector,
                    new NSString("Cut")));
            // Repeat for copy & paste
            ...

            foreach (var kc in _keyCommands)
            {
                AddKeyCommand(kc);
            }
        }
    }

    [Export(KeySelector)]
    private void KeyCommand(UIKeyCommand keyCmd)
    {
        if (keyCmd == null)
            return;

        if (_keyCommands.Contains(keyCmd))
        {
            // Handle KeyCommand
        }
    }
}

Holding down Command (⌘) reveals any keyboard shortcuts supported by an app.

iPad Discoverability

Demo

As each page receives keyboard input it is bound in the list view. When we switch between pages you can see the focus changes and the input is handled appropriately (either by Page 1 or Page 2).

Sample app showing keyboard input capture

There is scope for creating an abstraction to handle key input from either platform, but this should hopefully give you enough to get those fingers tapping out some ideas. The entire working sample can be found on my GitHub here.

comments powered by Disqus