Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Xamarin

Automatic Scrolling to Edited Text Box in Xamarin Android

5.00/5 (3 votes)
7 May 2017CPOL3 min read 9.2K  
Fixing hiding text box controls by software keyboard on Xamarin Android

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 a workaround.

Introduction

In the 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 the text boxes were hidden by software keyboard that were supposed 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

The first thing to do is to detect focus on Entry (single line editor) and Editor (multi line editor). The 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, they are probably used for other purposes. It is a good idea then to limit all functionalities that are supposed to be introduced in renderers to as few 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 supposed 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;
}

The 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 does 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 than one time for every control focus (because detecting software keyboard shows 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 (which do not have parent control). This is real Y coordinate of focused control. This value, with saved element height, represents position of bottom of focused control on screen. With that information, we can compare it to the available screen size (Application.Current.MainPage.Bounds.Height), which at this point should be approximately half of the screen (with the other half taken by the 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, the 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 the first half of the solution that is supposed 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 the 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 the whole application where it belongs. :)

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)