Taming the keyboard in native app development with Xamarin Forms

 

Wow, it’s been over 2 years since I last blogged. I haven’t done much exciting techy things in my spare time for a while, I did build a deck, maybe I’ll blog how to build a deck, but I want to start blogging more about native app development using Xamarin Forms.

I am loving Xamarin Forms. I have been playing with this technology for a bit under a year now, on and off. I started as a noob, and have now become fairly proficient in it. So I’m hoping to share some unique tips and tricks I’ve found along the way. I’ve already done 3 talks on it, 2 at Meetups in Melbourne and 1 at work, now it’s time to start blogging too :)

My first blog post is about the soft keyboard! A huge benefit of native mobile app development versus web mobile app development, is that you can get access to the soft keyboard. So you can resize your view when the keyboard pops up or down, including detecting the height of any accessory views, or auto correction attached to the keyboard, etc. This allows you to build your view with elegance, user experience, and design in mind.

For instance, Facebook Messenger’s interface, where you get some added action buttons that are always anchored above the keyboard, no matter if the keyboard is up or down, with accessory views on or not, or with auto correction displayed or not.

Facebook Messenger on iOS

Facebook Messenger on iOS

You can build those added actions as a Grid of Buttons, but this blog post isn’t about that, it’s about resizing the View.

So in iOS, the keyboard always overlaps the View. You have to find a way to resize the ContentPage height.

In Android, prior to Xamarin Forms material design update, this was a non issue for me as Android always resized the View whenever the keyboard popped up. However, when I implemented Xamarin Forms material design, that behaviour totally changed. There is an easy fix for this in Android 4.x, however not so for Android 5 and 6. I’m still waiting to hear back from the Xamarin Forms team, but I got it working in the meantime.

So let’s get into the code.

2 methods you can choose for iOS.

You can implement this Custom Renderer from Anthony Mills on StackOverflow here http://stackoverflow.com/a/31172519/78578

This method works, however once your View gets more complicated, with various input’s that vary in showing or not showing the accessory view or auto completion, it can totally break! As it did for me. If you have a simple View this should work fine.

Method 2 is my own method which I spent quite a bit of time on trying to get my head around how I would do it. What I did was create a Control called ‘BoxViewKeyboardHeight’.

In my XAML, I put this BoxViewKeyboardHeight at the bottom of my page, with a height of zero like so:

<local:BoxViewKeyboardHeight HeightRequest="0" />

I then created the control in my PCL:

using Xamarin.Forms;

namespace MyNamespace.Controls
{
    public class BoxViewKeyboardHeight : BoxView
    {
        
    }
}

And this is the custom renderer in the iOS project:

using System;
using UIKit;

using MyNamespace.Controls;
using MyNamespace.iOS.CustomRenderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly:ExportRenderer( typeof(BoxViewKeyboardHeight), typeof(BoxViewKeyboardHeightRenderer))]
namespace MyNamespace.iOS.CustomRenderers
{
    public class BoxViewKeyboardHeightRenderer : BoxRenderer
    {
        protected override void OnElementChanged (ElementChangedEventArgs<BoxView> e)
        {
            base.OnElementChanged (e);

            if (Element != null)
            {
                Element.HeightRequest = 0;
            }

            UIKeyboard.Notifications.ObserveWillShow ((sender, args) => {

                if (Element != null)
                {
                    Element.HeightRequest = args.FrameEnd.Height;
                }

            });

            UIKeyboard.Notifications.ObserveWillHide ((sender, args) => {
                
                if (Element != null)
                {
                    Element.HeightRequest = 0;
                }

            });
        }
    }
}

And voila! It basically creates a BoxView at the bottom of the page, and the height gets resized depending on the height of the keyboard, squeezing and resizing your other controls if they have variable heights. This simple method is very effective and actually very stable. It works great! If you see any issues with this code, then please let me know.

Let’s turn to Android now. As I mentioned, before the Xamarin Forms material design update, the View just automatically resized, I didn’t have to do anything extra (well I didn’t for the project I was working on).

However, when I implemented material design, the behaviour totally broke. This is easy to fix in Android 4.x, thanks to Jason Smith for replying to my forum thread. All you do is call Window.SetSoftInputMode (SoftInput.AdjustResize); after calling base.OnCreate in your FormsAppCompatActivity subclass.

In Android 5+ though, this doesn't work because it is disabled by the fullscreen flag, according to Jason Smith again. I believe he's looking to add a config option for it. In the meantime, with a bit more searching the internet, I managed to get it working in Android 5+ too.

Firstly thanks to Le-roy for this StackOverflow answer http://stackoverflow.com/a/27208118, it got me started, and kind of worked, however there was an extra bit of a gap added in between the keyboard and the view. Also my FAB was half way behind the Android soft buttons. This wasn’t good enough, so I did some more searching and experimenting.

I expected this was due to the height of the soft buttons, hence I had a gap, and hence my FAB, and View, was eating inside the soft buttons. I set out to find a way to detect the height of the soft buttons and stumbled upon chteuchteu’s StackOverflow answer here http://stackoverflow.com/a/20783949/78578. Written in Java, it was really simple to port to C#. I tried that and was getting a height, woo!

So with a bit more experimenting, I managed to update Le-roy’s class with this extra method to detect the soft button height, and I got Android 5+ keyboard View resize working.

So this is now the updated implementation class I am using:

using System;
using Android.App;
using Android.Widget;
using Android.Views;
using Android.Graphics;
using Android.OS;
using Android.Util;

namespace MyNamespace.Droid
{
    public class AndroidBug5497WorkaroundForXamarinAndroid
    {
        private readonly View mChildOfContent;
        private int usableHeightPrevious;
        private FrameLayout.LayoutParams frameLayoutParams;

        public static void assistActivity (Activity activityIWindowManager windowManager) {
            new AndroidBug5497WorkaroundForXamarinAndroid(activitywindowManager);
        }

        private AndroidBug5497WorkaroundForXamarinAndroid(Activity activityIWindowManager windowManager) {

            var softButtonsHeight = getSoftbuttonsbarHeight (windowManager);

            var content = (FrameLayoutactivity.FindViewById(Android.Resource.Id.Content);
            mChildOfContent = content.GetChildAt(0);
            var vto = mChildOfContent.ViewTreeObserver;
            vto.GlobalLayout += (sendere=> possiblyResizeChildOfContent (softButtonsHeight);
            frameLayoutParams = (FrameLayout.LayoutParamsmChildOfContent.LayoutParameters;
        }

        private void possiblyResizeChildOfContent(int softButtonsHeight) {
            var usableHeightNow = computeUsableHeight();
            if (usableHeightNow != usableHeightPrevious) {
                var usableHeightSansKeyboard = mChildOfContent.RootView.Height - softButtonsHeight;
                var heightDifference = usableHeightSansKeyboard - usableHeightNow;
                if (heightDifference > (usableHeightSansKeyboard / 4)) {
                    // keyboard probably just became visible
                    frameLayoutParams.Height = usableHeightSansKeyboard - heightDifference + (softButtonsHeight / 2);
                } else {
                    // keyboard probably just became hidden
                    frameLayoutParams.Height = usableHeightSansKeyboard;
                }
                mChildOfContent.RequestLayout();
                usableHeightPrevious = usableHeightNow;
            }
        }

        private int computeUsableHeight() {
            var r = new Rect ();
            mChildOfContent.GetWindowVisibleDisplayFrame(r);
            return (r.Bottom - r.Top);
        }

        private int getSoftbuttonsbarHeight(IWindowManager windowManager) {
            if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop) {
                var metrics = new DisplayMetrics();
                windowManager.DefaultDisplay.GetMetrics(metrics);
                int usableHeight = metrics.HeightPixels;
                windowManager.DefaultDisplay.GetRealMetrics(metrics);
                int realHeight = metrics.HeightPixels;
                if (realHeight > usableHeight)
                    return realHeight - usableHeight;
                else
                    return 0;
            }
            return 0;
        }
    }
}

And in my MainActivity.cs I have a conditional for Android 4.x and Android 5+ implementations.

// Fix the keyboard so it doesn't overlap the grid icons above keyboard etc, and makes Android 5+ work as AdjustResize in Android 4
Window.SetSoftInputMode (SoftInput.AdjustResize);
if (Build.VERSION.SdkInt >= BuildVersionCodes.Lollipop)
{
    // Bug in Android 5+, this is an adequate workaround
    AndroidBug5497WorkaroundForXamarinAndroid.assistActivity (thisWindowManager);
}

So there you go!

The benefits in elegance, user experience, and design you can accomplish by simply detecting the soft keyboard height, and change the height of the view port. Hope someone finds this useful.

Any comments or corrections, just reply here or ping me on Twitter @dimoss

 

PS: Windows Phone? Ugghhh! I haven't had time to work on Windows Phone apps, but I know this is an issue here too. If someone has or wants to build a Windows Phone implementation, please share :)