Important note: this should work well with newer version of Xamarin (2.3.3). If you cannot update for some reason this article will explain workaround.

Introduction

In previous article I explained how to detect show and hide events of software keyboard in Xamarin Android. I did it in my project because of the fact that some of text boxes was hidden by software keyboard that suppose to edit contents of those text boxes. In this article I will explain how to work around this issue in some older versions of Xamarin.

Solution

First thing to do is to detect focus on Entry (single line editor) and Editor (multi line editor). Best place to do that is in the renderers. If you do not have those in your project you can create new ones. If you already have renderers probably they are probably used for other purposes. It is a good idea then to limit all functionalities that are suppose to be introduced in renderers to as little lines as possible, to limit code dependency inside of renderers. Because of that I decided to add scrolling functionality to control with single line by call to static method of static class. Below are renderers for 2 editors controls.

public class ScrollEntryRenderer : EntryRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
    {
        base.OnElementChanged(e);
        EditorsScrollingHelper.AttachToControl(Control, this);
    }
}
public class ScrollEditorRenderer : EditorRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs<Editor> e)
    {
        base.OnElementChanged(e);
        EditorsScrollingHelper.AttachToControl(Control, this);
    }
}

AttachToControl method in EditorsScrollingHelper is suppose to add scrolling functionality when there is a focus event on control. EditorsScrollingHelper is doing all the heavy lifting of this mechanism.

public static void AttachToControl(TextView control, ScrollEditorRenderer renderer)
{
    control.FocusChange += (s, e) =>
    {
        FocusChange(e, renderer.Element);
    };
}

public static void AttachToControl(TextView control, ScrollEntryRenderer renderer)
{
    control.FocusChange += (s, e) =>
    {
        FocusChange(e, renderer.Element);
    };
}

FocusChange method of EditorsScrollingHelper class saves all necessary data for actual scrolling to focused control.

private static void FocusChange(AndroidView.FocusChangeEventArgs e, XamarinView element)
{
    if (e.HasFocus)
    {
        _focusedElement = element;
        _elementHeight = element.Bounds.Height;
    }
    else _focusedElement = null;
}

Need to save reference to focused control is self-explanatory. Element height will be explained below.

Actual scrolling is done inside handler of ISoftwareKeyboardService.Show event. Handler is added inside of static constructor of EditorsScrollingHelper class.

static EditorsScrollingHelper()
{
    TinyIoCContainer.Current.Resolve<ISoftwareKeyboardService>().Show += OnKeyboardShow;
}

OnKeyboardShow handler do scrolling if there is _focusedElement saved before inside FocusChange method.

private static void OnKeyboardShow(object sender, SoftwareKeyboardEventArgs args)
{
    if (_focusedElement != null)
    {
        ScrollIfNotVisible(_focusedElement);
    }
}

If condition triggers scrolling logic only then when there is focused control - because _focusedElement field is not null. It is done this way because Show event triggers more then one time for every control focus (because detecting software keyboard show event by GlobalLayoutListener is not bulletproof). Saved element height is used inside a Show event to calculate if scroll to focused control is necessary.

public static void ScrollIfNotVisible(XamarinView element)
{
    double translationY = 0;
    var parent = element;
    while (parent != null)
    {
        translationY -= parent.Y;
        parent = parent.Parent as XamarinView;
    }
    var height = Application.Current.MainPage.Bounds.Height;
    var elementHeight = _elementHeight;
    translationY -= elementHeight;
    if (-translationY > height)
    {
        if (Math.Abs(Application.Current.MainPage.TranslationY - translationY) > 0.99)
        {
            Application.Current.MainPage.SetTranslation(
                translationY + height / 2 - elementHeight / 2);
        }
    }
}

Scrolling is done by summing all of the elements heights from the focused element, going up through visual tree to the top - window root (witch do not have parent control). This is real Y coordinate of focused control. This value, with saved element height, represent position of bottom of focused control on screen. With that information, we can compare it to available screen size (Application.Current.MainPage.Bounds.Height), which at this point should be approximately half of the screen (with other half taken by software keyboard). If translationY value is greater then screen height available for application, which means that bottom of the control is invisible, then translation of main page is applied. Translation is done by extension method.

public static void SetTranslation(this VisualElement element, double y)
{
    element.TranslationY = y;
    var rectangle = new Rectangle
    {
        Left = element.X,
        Top = element.Y,
        Width = element.Width,
        Height = element.Height + (y < 0 ? -y : 0)
    };
    element.Layout(rectangle);
}

Translation works by moving control up by y pixels and setting new higher rectangle for this control. Without it application main page would be only moved up but nothing would be rendered under it, beside the white space. To remedy this effect rectangle for main page is higher and application renders any controls that might be below focused one.

This is first half of solution that is suppose to scroll app to focused control. Other half is to scroll back when control is unfocused. This is done in SoftwareKeyboardService by executing extra code on software keyboard hide event.

public void InvokeKeyboardHide(SoftwareKeyboardEventArgs args)
{
    OnHide();
    var handler = Hide;
    handler?.Invoke(this, args);
}

Translation of main page back to original location is done in OnHide method.

private void OnHide()
{
    if (Application.Current.MainPage != null)
    {
        Application.Current.MainPage.SetTranslation(0);
    }
}

Setting translation to zero puts back whole application where it belongs. :)

That is it! Gif from application with working solution is below.