How do you right-align a control in a RelativeLayout?

OwenPOwenP USMember ✭✭

I'm working on a custom control derived from ContentView. It incorporates a Label that becomes visible in response to user input and as such can alter the size of the control. It's going to live in a RelativeLayout, and I wanted its right edge to be aligned with the right edge of some other controls in my application. I'm finding this difficult because it seems as if RelativeLayout makes its constraint calls before it's asked its child elements to be sized, so if you want to be "parent.Width - myself.Width", the control begins in the wrong position because at the time the RelativeLayout is asking, Width is -1.

Here's a very simple example of a UI that wants a right-aligned label:

` public class MainPage : ContentPage {
public MainPage() {
var layout = new RelativeLayout();

        var label = new Label() {
            Text = "I want to be right-aligned."
        };
        layout.Children.Add(label,
            Constraint.RelativeToParent((rl) => rl.Width - label.Width),
            Constraint.Constant(10));

        var button = new Button() {
            Text = "Invalidate"
        };
        button.Clicked += (object sender, EventArgs e) => layout.ForceLayout();
        layout.Children.Add(button,
            Constraint.Constant(10),
            Constraint.Constant(10));

        Content = layout;
    }
}`

I expect this to start with the label properly aligned, but it does not align the label correctly until another layout pass is forced. By overriding methods like OnSizeRequest() in my custom control, I've determined this is because the calls to OnSizeRequest() don't happen until after the calls to the RelativeLayout's constraint lambdas. So, when the page is laid out, the label's Width is -1. When ForceLayout() is called later, the Label has had a chance to perform its layout logic and has properly set the Width property, so it gets laid out correctly.

Am I interpreting how to use a RelativeLayout wrong, or is this a mistake in its logic?

Posts

  • OwenPOwenP USMember ✭✭

    I found an answer, I think this is a bug. It took some ILSpy work to figure out what was going on after some stack traces in debug statements led me to ask, "Why is my control getting measured twice per layout pass, but only after I've asked for its width?"

    When a RelativeLayout's child is invalidated, RelativeLayout will calculate its constraints before the control has had its size set. If the control is referenced in RelativeToView or RelativeToParent constraints, it will still have its old bounds. Width/height constraints will appear to use the appropriate sizes if they are null, because they ask the control for its most recent computed width/height instead of using the potentially stale HeightRequest/WidthRequest. So if you need the control's size in any constraints that execute a lambda, you MUST call the control's GetSizeRequest() method to get the most recent size.

    This has some bad effects if GetSizeRequest() is expensive; in the worst-case it's called 4 times per layout. It'd be nice if there were a way for it to be called once and accessed, but with the current API it's not possible. Maybe a RelativeToSelf() is called for. I don't know. Here's a version of the earlier example that behaves as expected:

    `public class MainPage : ContentPage {
        public MainPage() {
            var layout = new RelativeLayout();
    
            var label = new Label() {
                Text = "I want to be right-aligned."
            };
            Func<RelativeLayout, double> getLabelWidth = (parent) => label.GetSizeRequest(layout.Width, layout.Height).Request.Width;
            layout.Children.Add(label,
                Constraint.RelativeToParent((rl) => rl.Width - getLabelWidth(rl)),
                Constraint.Constant(10));
    
            var button = new Button() {
                Text = "Invalidate"
            };
            button.Clicked += (object sender, EventArgs e) => layout.ForceLayout();
            layout.Children.Add(button,
                Constraint.Constant(10),
                Constraint.Constant(10));
    
            Content = layout;
        }
    }`
    
Sign In or Register to comment.