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