maui

Issue with FlexLayout and other Layouts not correctly measuring the amount of space available


I am getting an issue where .net MAUI LayoutManagers are not correctly measuring content and using restraints. I have a FlexLayout inside a VerticalStackLayout inside a Frame. The FlexLayout is used for displaying three hyperlinks and two labels used as spacers and wrapping them if they don't fit the width. The issue I get is that the FlexLayout does not always resize it's height to fit the content.

FlexLayout style.

<Style TargetType="FlexLayout" x:Key="my_FlexLinkHolder">
    <Setter Property="Direction" Value="Row"/>
    <Setter Property="Wrap" Value="Wrap"/>
    <Setter Property="AlignItems" Value="Start"/>
    <Setter Property="JustifyContent" Value="Start"/>
    <Setter Property="AlignContent" Value="Start"/>
</Style>

Picture of issue

If you notice in the picture, one label is being draw over by the table. For whatever reason FlexLayout is not properly expanding height.

I tried switching from a FlexLayout to a custom layout (HorizontalWrapLayout from microsoft). I get the same problem.

HorizontalWrapLayout

public class HorizontalWrapLayout : HorizontalStackLayout
{
    public HorizontalWrapLayout()
    {
    }

    protected override ILayoutManager CreateLayoutManager()
    {
        return new HorizontalWrapLayoutManager(this);
    }
}

HorizontalWrapLayoutManager

public class HorizontalWrapLayoutManager : HorizontalStackLayoutManager
{
    HorizontalWrapLayout _layout;

    public HorizontalWrapLayoutManager(HorizontalWrapLayout horizontalWrapLayout) : base(horizontalWrapLayout)
    {
        _layout = horizontalWrapLayout;
    }

    public override Size Measure(double widthConstraint, double heightConstraint)
    {
        var padding = _layout.Padding;

        widthConstraint -= padding.HorizontalThickness;

        double currentRowWidth = 0;
        double currentRowHeight = 0;
        double totalWidth = 0;
        double totalHeight = 0;

        for (int n = 0; n < _layout.Count; n++)
        {
            var child = _layout[n];
            if (child.Visibility == Visibility.Collapsed)
            {
                continue;
            }

            var measure = child.Measure(double.PositiveInfinity, heightConstraint);

            // Will adding this IView put us past the edge?
            if (currentRowWidth + measure.Width > widthConstraint)
            {
                // Keep track of the width so far
                totalWidth = Math.Max(totalWidth, currentRowWidth);
                totalHeight += currentRowHeight;

                // Account for spacing
                totalHeight += _layout.Spacing;

                // Start over at 0
                currentRowWidth = 0;
                currentRowHeight = measure.Height;
            }
            currentRowWidth += measure.Width;
            currentRowHeight = Math.Max(currentRowHeight, measure.Height);

            if (n < _layout.Count - 1)
            {
                currentRowWidth += _layout.Spacing;
            }
        }

        // Account for the last row
        totalWidth = Math.Max(totalWidth, currentRowWidth);
        totalHeight += currentRowHeight;

        // Account for padding
        totalWidth += padding.HorizontalThickness;
        totalHeight += padding.VerticalThickness;

        // Ensure that the total size of the layout fits within its constraints
        var finalWidth = ResolveConstraints(widthConstraint, Stack.Width, totalWidth, Stack.MinimumWidth, Stack.MaximumWidth);
        var finalHeight = ResolveConstraints(heightConstraint, Stack.Height, totalHeight, Stack.MinimumHeight, Stack.MaximumHeight);

        return new Size(finalWidth, finalHeight);
    }

    public override Size ArrangeChildren(Rect bounds)
    {
        var padding = Stack.Padding;
        double top = padding.Top + bounds.Top;
        double left = padding.Left + bounds.Left;

        double currentRowTop = top;
        double currentX = left;
        double currentRowHeight = 0;

        double maxStackWidth = currentX;

        for (int n = 0; n < _layout.Count; n++)
        {
            var child = _layout[n];
            if (child.Visibility == Visibility.Collapsed)
            {
                continue;
            }

            if (currentX + child.DesiredSize.Width > bounds.Right)
            {
                // Keep track of our maximum width so far
                maxStackWidth = Math.Max(maxStackWidth, currentX);

                // Move down to the next row
                currentX = left;
                currentRowTop += currentRowHeight + _layout.Spacing;
                currentRowHeight = 0;
            }

            var destination = new Rect(currentX, currentRowTop, child.DesiredSize.Width, child.DesiredSize.Height);
            child.Arrange(destination);

            currentX += destination.Width + _layout.Spacing;
            currentRowHeight = Math.Max(currentRowHeight, destination.Height);
        }

        var actual = new Size(maxStackWidth, currentRowTop + currentRowHeight);

        // Adjust the size if the layout is set to fill its container
        return actual.AdjustForFill(bounds, Stack);
    }
}

I did some debugging and noticed the issue is that the widthConstraint for Measure is different than the width of bounds for ArrangeChildren.

What in happening is the Measure thinks we have enough space, so we don't need to add another line, but when ArrangeChildren is called it sees that there is actually less space and adds another line.

Any ideas how to fix this, or any workarounds?

EDIT 9/12/24: The code I am using for the DataTemplate


<ContentPage.Resources>
    <ResourceDictionary>
        <DataTemplate x:Key="FooterTemplate">
            <Grid FlexLayout.Grow="1" VerticalOptions="End">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto"/>
                </Grid.RowDefinitions>
                <Button Text="&lt; Previous" Grid.Row="0" Grid.Column="0" Command="{Binding Source={x:Reference this}, Path=BindingContext.PrevPageCommand}"/>
                <Label Text="{Binding Source={x:Reference this}, Path=BindingContext.PageLabelText}" Grid.Row="0" Grid.Column="1" HorizontalTextAlignment="Center" VerticalTextAlignment="Center"/>
                <Button Text="Next &gt;" Grid.Row="0" Grid.Column="2" Command="{Binding Source={x:Reference this}, Path=BindingContext.NextPageCommand}"/>
            </Grid>
        </DataTemplate>
        <DataTemplate x:Key="RecordTemplate">
            <controls:MyResultCardView MyDataRecord="{Binding}" IsVisible="{Binding Source={x:Reference this}, Path=BindingContext.DoneLoading}"/>
        </DataTemplate>
        <controls:CustomTemplateSelector FooterTemplate="{StaticResource FooterTemplate}" CardTemplate="{StaticResource RecordTemplate} x:Key="CustomSelector"/>
    </ResourceDictionary>
</ContentPage.Resources>

The data template selector class.

public class CustomTemplateSelector : DataTemplateSelector
{
    public DataTemplate CardTemplate { get; set; }
    public DataTemplate FooterTemplate { get; set; }

    protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
    {
        //If the type of object is my custom footer class, will use footer template, otherwise we will use data card template
        if (item is CustomFooter)
            return FooterTemplate;
        else
            return CardTemplate;
    }
}

The selector checks if the object being bound is a Footer, otherwise it will use my custom control.

Code for MyResultCardView

public class MyResultCardView : Frame
{
    public static readonly BindableProperty MyDataRecordProperty =
           BindableProperty.Create("MyDataRecord", typeof(MyDataRecord), typeof(MyResultCardView), null, defaultBindingMode: BindingMode.TwoWay, propertyChanged: HandleValuePropertyChanged);
    
    public MyDataRecord MyDataRecord
    {
        get => (MyDataRecord)GetValue(MyDataRecordProperty);
        set => SetValue(MyDataRecordProperty, value);
    }
    
    public MyResultCardView() : base()
    {
    
    }
    
    private static void HandleValuePropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        MyResultCardView _view;
        MyDataRecord _rec;
    
        _view = (MyResultCardView)bindable;
        _rec = (MyDataRecord)newValue;
        if (_view != null && _rec != null)
        {
            _rec.DumpContentToFrame(_view);
        }
    }
}

The method DumpContentToFrame takes the Frame and programmatically adds a VerticalStackLayout to it and and populates it with labels containing text from the DataClass.

It does this kinda stuff.

VerticalStackLayout stackLayout = new VerticalStackLayout();
frame.Content = stackLayout;
Label label1 = new Label(){Text = dataProperty1};
stackLayout.Add(label1);

Then when I add a flex layout with the style my_FlexLinkHolder to the 'VerticalStackLayout', the flex layout does not measure correctly. Possibly because it doesn't like being nested.

For now I found a simple work around by just putting the label links on separate lines, instead of wrapping. However, I am curious if there are some issues that happen when FlexLayout is nested inside other layouts, because I use FlexLayout frequently.


Solution

  • If you want to wrap the Label, you don't have to use FlexLayout. You can Control text truncation and wrapping by setting the LineBreakMode property. To create hyperlinks label, you may use formatted text. Code snippets in code behind,

    Label myLabel = new Label() 
    { 
        LineBreakMode=LineBreakMode.WordWrap
    };
    FormattedString  formattedString = new FormattedString();
    Span span1 = new Span() { 
        Text="Map The Location ",
        TextColor = Colors.Blue,
        TextDecorations = TextDecorations.Underline
    };
    //create more span if you want
    
    formattedString.Spans.Add(span1);
    
    myLabel.FormattedText = formattedString;
    

    And you may add the Label in VerticleStackLayout

    stackLayout.Add(myLabel);
    

    Also, I didn't see the issue when FlexLayout nested in a VerticleStackLayout. If I wrap the Label with FlexLayout and then added to the stackLayout again, also works like below.


    Screenshot

    enter image description here