I'm designing a UI in Xamarin.Forms to collect feedback from users about our application. There is an Editor control at the bottom of this page. On an iPhone 4S, and in many landscape orientations, the keyboard completely obscures this editor control. On Android, this is not a big deal because the OS automatically scrolls (though the sizing behavior is a little weird.) On iOS, the only things resembling solutions are very wonky.
In native iOS, the solution is simple: wrap your views in a UIScrollView, then when the keyboard appears add that much space to the content size and scroll appropriately. Xamarin doesn't expose anything to control the scroll position in ScrollView, and ContentSize is private, so that's out. A few posts (here and here) seem to indicate ScrollView is at least part of the solution. It does appear Xamarin has some automatic scrolling behavior, but it's... curious.
My layout is fairly simple:
- At the top, a fixed navigation bar that I do not want to scroll out of view.
- Beneath that, a 180px tall image that represents a screenshot of the application.
- Beneath that, a label with information such as the timestamp. (2-3 lines of text).
- Beneath that, the editor, filling the remaining available space.
I've included code for a layout I've tried at the bottom of my post. I created a StackLayout that contains the image, the label, and the editor. I put that inside a ScrollView. Then, I create a RelativeLayout and place the navigation bar at the top-left with the ScrollView beneath it.
What I want to happen when the Editor is tapped is for the keyboard to be displayed and, if it obscures the Editor, for the layout to be nudged upwards to make the Editor visible. What happens instead is it seems like Xamarin scrolls the layout upwards by the keyboard height plus some margin that looks suspiciously like the keyboard utility bar height. This shoves the Editor upwards so high it's obscured by the navigation bar.
I've tried a lot of different tweaks and I'm at a loss. I can't control enough of the ScrollView to get the behavior I need. I've seen suggestions that use a BoxView resized when the Editor gains focus, but to make it work really well I'd still have to hook into the iOS notifications to get the appropriate size and have a fairly intimate knowledge of where my Editor's bounds are. It feels wrong.
Does anyone else have a solution to this on Xamarin.Forms? Even if I have to dip into native, I'd like an answer.
(Here's an example layout that demonstrates the problem, there's a little bit of weird structure because I was debugging. The funky colors are also a relic of layout debugging.)
using System;
using Xamarin.Forms;
namespace TestScroll
{
public class MainPage : ContentPage {
public MainPage() {
InitializeComponent();
}
private ScrollView _scroller;
protected void InitializeComponent() {
var mainLayout = new RelativeLayout();
var navbar = new Label() {
BackgroundColor = Color.Blue,
TextColor = Color.White,
Text = "I am the Nav Bar",
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.StartAndExpand
};
var subLayout = new ScrollView() {
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand
};
_scroller = subLayout;
var subStack = new StackLayout();
subStack.Spacing = 0;
subLayout.Content = subStack;
var image = new BoxView() {
Color = Color.Green,
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.Fill,
HeightRequest = 300
};
subStack.Children.Add(image);
var infoLabel = new Label() {
BackgroundColor = Color.Blue,
TextColor = Color.Black,
Text = "Timestamp!\r\nOther stuff!",
VerticalOptions = LayoutOptions.Start
};
subStack.Children.Add(infoLabel);
var editor = new Editor() {
VerticalOptions = LayoutOptions.FillAndExpand
};
subStack.Children.Add(editor);
mainLayout.Children.Add(navbar,
Constraint.Constant(0),
Constraint.Constant(20),
Constraint.RelativeToParent((parent) => parent.Width),
Constraint.Constant(70));
mainLayout.Children.Add(subLayout,
Constraint.Constant(0),
Constraint.RelativeToView(navbar, (parent, view) => navbar.Bounds.Bottom),
Constraint.RelativeToParent((parent) => parent.Width),
Constraint.RelativeToView(navbar, TestConstraint));
Content = mainLayout;
}
private double TestConstraint(RelativeLayout parent, View view) {
double result = parent.Height - view.Bounds.Height;
Console.WriteLine ("Lower stack height : {0}", result);
Console.WriteLine ("Scroll content size: {0}", _scroller.ContentSize);
return result;
}
}
}