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 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 a 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;
}
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 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 (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 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, the application main page would be only moved up but nothing would be rendered under it, beside the white space. To remedy this effect, the rectangle for main page is higher and application renders any controls that might be below focused one.
This is 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 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 working solution is below:
CodeProject