dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.28k stars 1.76k forks source link

[iOS] CollectionView's height does not include measured height of Header and Footer to size of Content #23435

Open Domik234 opened 5 months ago

Domik234 commented 5 months ago

Description

CollectionView without Header and Footer is calculated correctly.

When Header or Footer is added - measurement of these views is not included to size of CollectionView.

VerticalOptions: Fill + Grid.Row = Star ![IMG_0188](https://github.com/dotnet/maui/assets/9479585/627b8991-1f26-41ea-aa6a-defe9a190ec6)
VerticalOptions: End - Scrolled to Top + Grid.Row = Star ![IMG_0189](https://github.com/dotnet/maui/assets/9479585/e91e58ef-2a93-4d25-89ef-bc75fe09dac5)
VerticalOptions: End - Scrolled to Bottom + Grid.Row = Star ![IMG_0190](https://github.com/dotnet/maui/assets/9479585/764bf29e-4221-4c0b-8a73-879a75be0de8)

When you set RowDefinitions to auto, auto - it's broken the same way (for VerticalOptions: Fill) as when you set VerticalOptions to Start, Center or End + having Header or Footer.

VerticalOptions: Fill + Grid.Row = Auto ![IMG_0191](https://github.com/dotnet/maui/assets/9479585/6e50288b-4689-49dd-9a2e-5b40095421df)

Prepared repo has buttons to switch situations as needed for enabling Header/Footer, switching VerticalOptions for CV and adding new items and removing last item.

Red = Control Bar Yellow = CV Header Orange = CV Footer

Bonus Situation:

  1. There is a grid with RowDefinitions = "auto," and ColumnDefinitions = ""
  2. Row 0 (auto) is occupied by ContentView with HeightRequest = 48
  3. Row 1 (*) is occupied by CollectionView.
  4. When you set CV's VerticalOptions to Start / Center / End -> it works until there is enough item to fill it's own calculated height. When you add one more item -> it forces Grid to shrink auto row (row 0) and make more space for CollectionView.
CollectionView with VO set not to Fill breaks Grid's measurement when items use CV's available space ![IMG_0192](https://github.com/dotnet/maui/assets/9479585/793bd780-ff2e-4ee7-a134-7a6e8f49879f)

Not tested: Horizontal CV

Steps to Reproduce

Run the repo.

  1. Download repo? 1a. Use the button "H: Off" to show header. Size of CollectionView will not change but header will be included in Content. 1b. Use the button "F: Off" to show footer. Size of CollectionView will not change but footer will be included in Content. 1c. Use the buttons "H: Off" and "F: Off" to show header and footer. Size of CollectionView will not change but header and footer will be included in Content.

Link to public reproduction project repository

https://github.com/Domik234/Issue-MauiCollectionViewVerticalOptionsEndBugs

Version with bug

8.0.61 SR6.1

Is this a regression from previous behavior?

Yes, this used to work in Xamarin.Forms

Last version that worked well

Unknown/Other

Affected platforms

iOS

Affected platform versions

No response

Did you find any workaround?

-

Relevant log output

-
github-actions[bot] commented 5 months ago

Hi I'm an AI powered bot that finds similar issues based off the issue title.

Please view the issues below to see if they solve your problem, and if the issue describes your problem please consider closing this one and thumbs upping the other issue to help us prioritize it. Thank you!

Open similar issues:

Closed similar issues:

Note: You can give me feedback by thumbs upping or thumbs downing this comment.

ninachen03 commented 4 months ago

This issue has been verified using Visual Studio 17.11.0 Preview 2.1(8.0.61 & 8.0.60). Can repro on iOS platform.

Domik234 commented 4 months ago

First problem: Header's or Footer's height is not computed to the size of CollectionView's Content.

CollectionViewHandler.GetDesiredSize (or it's parent's GetDesiredSize) forgets to add insets from Controller.CollectionView.AdjustedContentInset used for Header and Footer to the height of CollectionView!

    public class XCollectionViewHandler : CollectionViewHandler
    {
        public XCollectionViewHandler() : base(/* vlastní mapper? */) { }

#if IOS
        public override Size GetDesiredSize(double widthConstraint, double heightConstraint)
        {
            var size = base.GetDesiredSize(widthConstraint, heightConstraint);

            var insets = Controller.CollectionView.AdjustedContentInset.Top + Controller.CollectionView.AdjustedContentInset.Bottom;
            //Console.WriteLine("GetDesiredSize is " + size.ToString() + " and edited is " + newSize.ToString());
            return new Size(size.Width, Math.Min(size.Height + insets, heightConstraint));
        }
#endif
    }

Second problem is that when I set VerticalOptions to something else than Fill - it is calculated also wrong and it can go to space out of it's bounds thanks to:

  1. ArrangeOverride of CollectionView
  2. LayoutExtensions.ComputeFrame for iOS Platform calculations (Currently Investigated)
Domik234 commented 4 months ago

Second problem: CollectionView doesn't tolerate it's bounds

CollectionView is able to ignore layouted bounds. Problem is somewhere in the method of LayoutExtensions.ComputeFrame(IView, Rect) called from VisualElement.ArrangeOverride. It looks like it doesn't care about really available space and it sets whatever it wants but more propably there will be error in the method LayoutExtensions.AlignVertical(IView, Rect, Thickness) with equation or simply forgotten check if it is still in bounds.

//This is broken
protected virtual Size ArrangeOverride(Rect bounds)
{
    Frame = this.ComputeFrame(bounds);
    Handler?.PlatformArrange(Frame);
    return Frame.Size;
}

When the parameter bounds's Y = 48 -> ComputeFrame is able to set the the Frame Y value like 17. So the view's Frame is able to begin in another view!

This can be fixed easily by this:

public class XCollectionView : CollectionView
{
    protected override Size ArrangeOverride(Rect bounds)
    {
        //base.ArrangeOverride(bounds); - Base doesn't work. Let's take it's content out and edit it.

        //1. Save the computedFrame that app wants to use.
        var computedFrame = this.ComputeFrame(bounds);

        //2. Set Y to be the maximum value of original bounds' Y and computerFrame's Y - that means it cannot be higher than it is allowed.
        //3. Set the Height of Frame to the minimum value of original bounds's Y + Height and computerFrame's Height - reverse process of the maximum for Y
        //Steps 2 and 3 limit the Frame to be exactly in the available bounds for the layout that calculated it.
        Frame = new Rect(computedFrame.Left, Math.Max(bounds.Top, computedFrame.Top), computedFrame.Width, Math.Min(bounds.Height, computedFrame.Height));
        Handler?.PlatformArrange(Frame);
        return Frame.Size;
    }
}

I've already tested this for all cases that I use with all combinations of VerticalOptions settings. In this case I've completely ignored horizontal orientation possibility but by the principle I may be solved the same logic.

Ideally solving this directly in the ComputeFrame could solve most of this problems because this won't be the only case where this happens and breaks UI drawing but that will need more testing.

Domik234 commented 4 months ago

Whole solution for fixed calculations for Vertical orientation with size of Header and Footer contained working on 8.0.71

using Microsoft.Maui.Controls.Handlers.Items;
using Microsoft.Maui.Layouts;

namespace MauiApp1.Components
{
    public class XCollectionView : CollectionView
    {
        protected override Size ArrangeOverride(Rect bounds)
        {
            var computedFrame = this.ComputeFrame(bounds);
            Frame = new Rect(computedFrame.Left, Math.Max(bounds.Top, computedFrame.Top), computedFrame.Width, Math.Min(bounds.Height, computedFrame.Height));
            Handler?.PlatformArrange(Frame);
            return Frame.Size;
        }
    }

    public class XCollectionViewHandler : CollectionViewHandler
    {
        public XCollectionViewHandler() : base() { }

#if IOS
        public override Size GetDesiredSize(double widthConstraint, double heightConstraint)
        {
            var size = base.GetDesiredSize(widthConstraint, heightConstraint);

            var insets = Controller.CollectionView.AdjustedContentInset.Top + Controller.CollectionView.AdjustedContentInset.Bottom;
            return new Size(size.Width, Math.Min(size.Height + insets, heightConstraint));
        }
#endif
    }
}

Don't forget to register the handler!

    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                })
                //Don't forget to add this!!! - Start
                .ConfigureMauiHandlers(handlers =>
                {
                    handlers.AddHandler<XCollectionView, XCollectionViewHandler>();
                });
                //Don't forget to add this!!! - End

#if DEBUG
            builder.Logging.AddDebug();
#endif

            return builder.Build();
        }
    }
Domik234 commented 1 week ago

Relevant? https://github.com/dotnet/maui/issues/25773