DEV Community

Cover image for How to create a custom keyboard with Xamarin Forms (Android)
Fabricio Bertani
Fabricio Bertani

Posted on

4 1

How to create a custom keyboard with Xamarin Forms (Android)

Some time ago a client requested a special keyboard for his application, which had to have certain conditions that the regular Android keyboard didn’t meet.

Researching

The first option that came to mind was to add a disabled Entry with a GestureRecognizer that displays a control with an animation to emulate the appearance of the keyboard, but quickly discard the idea as it wasn’t reusable.
The best option was taking the native path, so I spent quite time researching, but I only found solutions that leads to create a keyboard as a service. I knew that our client wouldn’t like the idea of having to download a separate keyboard to only use it in the application and I needed a solution that should work with Xamarin Forms!
As I had to think about its implementation in Xamarin Forms I decided that the best option was to try with a Custom Renderer of the Entry control since it uses EditText as base for the native control and try to apply all those solutions that I had read previously in it.

Let’s get down to work!

Note: in order to correctly implement the custom keyboard we are going to need that the Xamarin Forms version to be 3.6.0.135200-pre1 or higher because we need the OnFocusChangeRequest method which is only available from this version.

First we’re going to create the custom control, which will have the next bindable property:

  • EnterCommand: typeof ICommand, to bind Enter key press action.

This is how our custom control will be:

namespace CustomKeyboard
{
public class EntryWithCustomKeyboard : Entry
{
public static readonly BindableProperty EnterCommandProperty = BindableProperty.Create(
nameof(EnterCommand),
typeof(ICommand),
typeof(EntryWithCustomKeyboard),
default(ICommand),
BindingMode.OneWay
);
public ICommand EnterCommand
{
get => (ICommand)GetValue(EnterCommandProperty);
set => SetValue(EnterCommandProperty, value);
}
}
}

Now we’re moving to our Android project and keep working there (later we will return to our Xamarin Forms project).
Before we go on, ensure to have all the required Android packages:

alt-text

Next we’re going to edit our Android MainActivity to avoid that native keyboard to show up, for that we will use the SoftInputMode.StateAlwaysHidden attribute

namespace CustomKeyboard.Droid
{
[Activity(
Label = "CustomKeyboard",
Icon = "@mipmap/icon",
Theme = "@style/MainTheme",
MainLauncher = true,
ScreenOrientation = ScreenOrientation.Portrait,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,
WindowSoftInputMode = SoftInput.StateAlwaysHidden,
LaunchMode = LaunchMode.SingleTask)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
Window.SetSoftInputMode(SoftInput.StateAlwaysHidden);
}
}
}
view raw MainActivity.cs hosted with ❤ by GitHub

What are we going to do next is start to define our custom keyboard.
Inside the Resource/layout folder we are going to create an Android layout named CustomKeyboard typeof InputMethodService.Keyboard

<?xml version="1.0" encoding="utf-8"?>
<android.inputmethodservice.Keyboard
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:keyPreviewLayout="@null"
android:keyBackground="@drawable/keyboard_background"
android:textColor="@android:color/white"
android:background="@android:color/white" />

Firstly we set the alignParentBottom property to true because we want our keyboard to be visible from the bottom side of the screen.
Secondly we set the keyPreviewLayout property to null because on this sample we don’t want a response layout when some key is pressed.
As you can see in the keyBackground property refers to a drawable called keyboard_background, which doesn’t exist yet, so we are going to create it inside the Drawable folder as an xml file, there we are going to define a state selector for the two states our keys can have: normal (without pressing) and pressed.

<?xml version="1.0" encoding="UTF-8" ?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_focused="false"
android:state_selected="false"
android:state_pressed="false"
android:drawable="@drawable/normal" />
<item
android:state_pressed="true"
android:drawable="@drawable/pressed" />
</selector>

As you can see, we are going to have to create another two xml inside the Drawable folder, in which we are going to define the look and feel of our keyboard to match (or not) with our application theme.

<?xml version="1.0" encoding="UTF-8" ?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:left="2dp" android:right="2dp">
<shape android:shape="rectangle">
<solid android:color="#FFFFFF" />
</shape>
</item>
<item android:bottom="2dp">
<shape android:shape="rectangle">
<solid android:color="#FF6A00" />
</shape>
</item>
</layer-list>
view raw normal.xml hosted with ❤ by GitHub
<?xml version="1.0" encoding="UTF-8" ?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FF8C00" />
</shape>
view raw pressed.xml hosted with ❤ by GitHub

Now within the values folder we will create an xml named ids which we will need later.

<?xml version="1.0" encoding="UTF-8" ?>
<resources>
<item
name="customKeyboard"
type="id" />
</resources>
view raw ids.xml hosted with ❤ by GitHub

Next, inside the Resources folder we will create a new folder called xml. Within it we will create an xml in which we will define the keys of our special keyboard.
In our case our keyboard will be called special_keyboard, it will be typeof Keyboard in which we will define the horizontal and vertical size of our keys, the horizontalGap and verticalGap properties refer to the spacing and dimension type %p (in case you’ve never seen it) is a kind of percentage relative to the parent view.
Each row of keys will be included within sections delimited by the <Row></Row> tags.
Our first row will be occupied with a separating line to mark the limit of our keyboard. The Row tag will have a height of 4dp and we will indicate that it is at the top of our keyboard through the rowEdgeFlags property. Then we will add a line as a Key within the Row tags that it would take the full width of the keyboard. Our separator is just another xml that we will create inside the Drawable folder:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<stroke android:width="1dp" />
<size android:height="2dp" />
</shape>

Each Key will have two strictly necessary properties: codes and keyLabel code will be a number that tells the OS what letter or symbol the key corresponds to. At this point I want to clarify something: I found dozens of samples about creating customs keyboards for Android and they have used many different codes to refer to a certain symbol or key. The best list of codes that has been fulfilled most of all that I have tried is the following: Android Keycodes. You can also see the official Android docs or even the Xamarin Android but none of them has worked with accuracy.
keyLabel is the string that is going to be shown in our key, it is very important to put this property even if we don’t want to show any text in our key (in that case it would be keyLabel="").
For style decision, at the beginning and at the end of each row, I add a Key with code equal to 0 (so you don’t have any action), the width also of 0 and a spacing of 2%p, these keys will also carry the keyEdgeFlags property with the left or right values as appropriate.
Here our full keyboard:

<?xml version="1.0" encoding="UTF-8" ?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
android:keyWidth="8%p"
android:keyHeight="50dp"
android:horizontalGap="1%p"
android:verticalGap="1%p">
<Row android:keyHeight="4dp" android:rowEdgeFlags="top" android:verticalGap="1%p">
<Key android:codes="0" android:keyWidth="100%p" android:keyIcon="@drawable/kb_separator_line" />
</Row>
<Row>
<Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="left" />
<Key android:codes="29" android:keyLabel="A" android:keyWidth="18%p" />
<Key android:codes="30" android:keyLabel="B" android:keyWidth="18%p" />
<Key android:codes="31" android:keyLabel="C" android:keyWidth="18%p" />
<Key android:codes="32" android:keyLabel="D" android:keyWidth="18%p" />
<Key android:codes="33" android:keyLabel="E" android:keyWidth="18%p" />
<Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="right" />
</Row>
<Row>
<Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="left" />
<Key android:codes="8" android:keyLabel="1" android:keyWidth="18%p" />
<Key android:codes="9" android:keyLabel="2" android:keyWidth="18%p" />
<Key android:codes="10" android:keyLabel="3" android:keyWidth="18%p" />
<Key android:codes="11" android:keyLabel="4" android:keyWidth="18%p" />
<Key android:codes="12" android:keyLabel="5" android:keyWidth="18%p" />
<Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="right" />
</Row>
<Row>
<Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="left" />
<Key android:codes="13" android:keyLabel="6" android:keyWidth="18%p" />
<Key android:codes="14" android:keyLabel="7" android:keyWidth="18%p" />
<Key android:codes="15" android:keyLabel="8" android:keyWidth="18%p" />
<Key android:codes="16" android:keyLabel="9" android:keyWidth="18%p" />
<Key android:codes="7" android:keyLabel="0" android:keyWidth="18%p" />
<Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="right" />
</Row>
<Row>
<Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="left" />
<Key android:codes="67" android:keyLabel="DELETE" android:keyWidth="37%p" />
<Key android:codes="66" android:keyLabel="ENTER" android:keyWidth="56%p" />
<Key android:codes="0" android:keyWidth="0dp" android:horizontalGap="2%p" android:keyEdgeFlags="right" />
</Row>
</Keyboard>

Finally before starting to work in our renderer, we will create another folder within Resources, called anim with an xml that we are going to call slide_in_bottom this will be the animation with which our keyboard will appear on the screen.

<?xml version="1.0" encoding="UTF-8" ?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromYDelta="150%p" android:toYDelta="0" android:duration="200"/>
<alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="200" />
</set>

Now we will create a new folder on our Android project, named Renderers and there we will create our renderer which we will call EntryWithCustomKeyboardRenderer which will extend from EntryRenderer and implement the interface IOnKeyboardActionListener. Also within our custom renderer we will create a private class called NullListener which is going to extend from Java.Lang.Object and implement the interface IOnKeyboardActionListener which we are going to use in our renderer to avoid null exceptions.

[assembly: ExportRenderer(typeof(EntryWithCustomKeyboard), typeof(EntryWithCustomKeyboardRenderer))]
namespace CustomKeyboard.Droid.Renderers
{
public class EntryWithCustomKeyboardRenderer : EntryRenderer, IOnKeyboardActionListener
{
private Context context;
private EntryWithCustomKeyboard entryWithCustomKeyboard;
private Android.InputMethodServices.KeyboardView mKeyboardView;
private Android.InputMethodServices.Keyboard mKeyboard;
private InputTypes inputTypeToUse;
private bool keyPressed;
public EntryWithCustomKeyboardRenderer(Context context) : base(context)
{
this.context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
var newCustomEntryKeyboard = e.NewElement as EntryWithCustomKeyboard;
var oldCustomEntryKeyboard = e.OldElement as EntryWithCustomKeyboard;
if (newCustomEntryKeyboard == null && oldCustomEntryKeyboard == null)
return;
if (e.NewElement != null)
{
this.entryWithCustomKeyboard = newCustomEntryKeyboard;
this.CreateCustomKeyboard();
this.inputTypeToUse = this.entryWithCustomKeyboard.Keyboard.ToInputType() | InputTypes.TextFlagNoSuggestions;
// Here we set the EditText event handlers
this.EditText.FocusChange += Control_FocusChange;
this.EditText.TextChanged += EditText_TextChanged;
this.EditText.Click += EditText_Click;
this.EditText.Touch += EditText_Touch;
}
// Dispose control
if (e.OldElement != null)
{
this.EditText.FocusChange -= Control_FocusChange;
this.EditText.TextChanged -= EditText_TextChanged;
this.EditText.Click -= EditText_Click;
this.EditText.Touch -= EditText_Touch;
}
}
protected override void OnFocusChangeRequested(object sender, VisualElement.FocusRequestArgs e)
{
e.Result = true;
if (e.Focus)
this.Control.RequestFocus();
else
this.Control.ClearFocus();
}
// Event handlers
private void Control_FocusChange(object sender, FocusChangeEventArgs e)
{
// Workaround to avoid null reference exceptions in runtime
if (this.EditText.Text == null)
this.EditText.Text = string.Empty;
if (e.HasFocus)
{
this.mKeyboardView.OnKeyboardActionListener = this;
if (this.Element.Keyboard == Keyboard.Text)
this.CreateCustomKeyboard();
this.ShowKeyboardWithAnimation();
}
else
{
// When the control looses focus, we set an empty listener to avoid crashes
this.mKeyboardView.OnKeyboardActionListener = new NullListener();
this.HideKeyboardView();
}
}
private void EditText_TextChanged(object sender, Android.Text.TextChangedEventArgs e)
{
// Ensure no key is pressed to clear focus
if (this.EditText.Text.Length != 0 && !this.keyPressed)
{
this.EditText.ClearFocus();
return;
}
}
private void EditText_Click(object sender, System.EventArgs e)
{
ShowKeyboardWithAnimation();
}
private void EditText_Touch(object sender, TouchEventArgs e)
{
this.EditText.InputType = InputTypes.Null;
this.EditText.OnTouchEvent(e.Event);
this.EditText.InputType = this.inputTypeToUse;
e.Handled = true;
}
// Keyboard related section
// Method to create our custom keyboard view
private void CreateCustomKeyboard()
{
var activity = (Activity)this.context;
var rootView = activity.Window.DecorView.FindViewById(Android.Resource.Id.Content);
var activityRootView = (ViewGroup)((ViewGroup)rootView).GetChildAt(0);
this.mKeyboardView = activityRootView.FindViewById<Android.InputMethodServices.KeyboardView>(Resource.Id.customKeyboard);
// If the previous line fails, it means the keyboard needs to be created and added
if (this.mKeyboardView == null)
{
this.mKeyboardView = (Android.InputMethodServices.KeyboardView)activity.LayoutInflater.Inflate(Resource.Layout.CustomKeyboard, null);
this.mKeyboardView.Id = Resource.Id.customKeyboard;
this.mKeyboardView.Focusable = true;
this.mKeyboardView.FocusableInTouchMode = true;
this.mKeyboardView.Release += (sender, e) => { };
var layoutParams = new Android.Widget.RelativeLayout.LayoutParams(LayoutParams.MatchParent, LayoutParams.WrapContent);
layoutParams.AddRule(LayoutRules.AlignParentBottom);
activityRootView.AddView(this.mKeyboardView, layoutParams);
}
this.HideKeyboardView();
this.mKeyboard = new Android.InputMethodServices.Keyboard(this.context, Resource.Xml.special_keyboard);
this.SetCurrentKeyboard();
}
private void SetCurrentKeyboard()
{
this.mKeyboardView.Keyboard = this.mKeyboard;
}
// Method to show our custom keyboard
private void ShowKeyboardWithAnimation()
{
// First we must ensure that keyboard is hidden to
// prevent showing it multiple times
if (this.mKeyboardView.Visibility == ViewStates.Gone)
{
// Ensure native keyboard is hidden
var imm = (InputMethodManager)this.context.GetSystemService(Context.InputMethodService);
imm.HideSoftInputFromWindow(this.EditText.WindowToken, 0);
this.EditText.InputType = InputTypes.Null;
var animation = AnimationUtils.LoadAnimation(this.context, Resource.Animation.slide_in_bottom);
this.mKeyboardView.Animation = animation;
this.mKeyboardView.Enabled = true;
// Show custom keyboard with animation
this.mKeyboardView.Visibility = ViewStates.Visible;
}
}
// Method to hide our custom keyboard
private void HideKeyboardView()
{
this.mKeyboardView.Visibility = ViewStates.Gone;
this.mKeyboardView.Enabled = false;
this.EditText.InputType = InputTypes.Null;
}
// Implementing IOnKeyboardActionListener interface
public void OnKey([GeneratedEnum] Keycode primaryCode, [GeneratedEnum] Keycode[] keyCodes)
{
if (!this.EditText.IsFocused)
return;
// Ensure key is pressed to avoid removing focus
this.keyPressed = true;
// Create event for key press
long eventTime = JavaSystem.CurrentTimeMillis();
var ev = new KeyEvent(eventTime, eventTime, KeyEventActions.Down, primaryCode, 0, 0, 0, 0,
KeyEventFlags.SoftKeyboard | KeyEventFlags.KeepTouchMode);
// Ensure native keyboard is hidden
var imm = (InputMethodManager)this.context.GetSystemService(Context.InputMethodService);
imm.HideSoftInputFromWindow(this.EditText.WindowToken, HideSoftInputFlags.None);
this.EditText.InputType = this.inputTypeToUse;
switch(ev.KeyCode)
{
case Keycode.Enter:
// Sometimes EditText takes long to update the HasFocus status
if (this.EditText.HasFocus)
{
// Close the keyboard, remove focus and launch command asociated action
this.HideKeyboardView();
this.ClearFocus();
this.entryWithCustomKeyboard.EnterCommand?.Execute(null);
}
break;
}
// Set the cursor at the end of the text
this.EditText.SetSelection(this.EditText.Text.Length);
if (this.EditText.HasFocus)
{
this.DispatchKeyEvent(ev);
this.keyPressed = false;
}
}
public void OnPress([GeneratedEnum] Keycode primaryCode)
{
}
public void OnRelease([GeneratedEnum] Keycode primaryCode)
{
}
public void OnText(ICharSequence text)
{
}
public void SwipeDown()
{
}
public void SwipeLeft()
{
}
public void SwipeRight()
{
}
public void SwipeUp()
{
}
private class NullListener : Java.Lang.Object, IOnKeyboardActionListener
{
public void OnKey([GeneratedEnum] Keycode primaryCode, [GeneratedEnum] Keycode[] keyCodes)
{
}
public void OnPress([GeneratedEnum] Keycode primaryCode)
{
}
public void OnRelease([GeneratedEnum] Keycode primaryCode)
{
}
public void OnText(ICharSequence text)
{
}
public void SwipeDown()
{
}
public void SwipeLeft()
{
}
public void SwipeRight()
{
}
public void SwipeUp()
{
}
}
}
}

Finally, we return to our Xamarin Forms project and implement our special keyboard.

<?xml version="1.0" encoding="utf-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:CustomKeyboard"
x:Class="CustomKeyboard.MainPage">
<ScrollView>
<StackLayout
Orientation="Vertical">
<local:EntryWithCustomKeyboard
x:Name="entry1"
HorizontalOptions="FillAndExpand"
Margin="0, 0, 0, 20"
Keyboard="Text"
TextColor="Black"
Placeholder="Custom Keyboard entry..." />
<local:EntryWithCustomKeyboard
x:Name="entry2"
HorizontalOptions="FillAndExpand"
Keyboard="Text"
TextColor="Black"
Placeholder="Custom Keyboard entry 2..." />
</StackLayout>
</ScrollView>
</ContentPage>
view raw MainPage.xaml hosted with ❤ by GitHub

And in our code behind we implement the EnterCommand that we created previously, for the action that we want to happen when we press the Enter key of our custom keyboard.

namespace CustomKeyboard
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
// Here we implement the action of the Enter button on our custom keyboard
this.entry1.EnterCommand = new Command(() => this.entry2.Focus());
this.entry2.EnterCommand = new Command(() => this.entry1.Focus());
}
}
}
view raw MainPage.cs hosted with ❤ by GitHub

Here’s the final result:

alt-text

You can see the complete sample repository on GitHub

GitHub logo FabriBertani / CustomKeyboardXamarinForms

How to create a custom keyboard with Xamrain Forms (Android)




In the next post we will see how to make a complex custom keyboard!

Thanks for reading 😁

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.