Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The width of CollectionView's footer and header is not correct #17885

Open
maonaoda opened this issue Oct 7, 2023 · 10 comments
Open

The width of CollectionView's footer and header is not correct #17885

maonaoda opened this issue Oct 7, 2023 · 10 comments
Labels
area-controls-collectionview CollectionView, CarouselView, IndicatorView delighter-sc platform/iOS 🍎 s/triaged Issue has been reviewed s/verified Verified / Reproducible Issue ready for Engineering Triage t/bug Something isn't working
Milestone

Comments

@maonaoda
Copy link
Contributor

maonaoda commented Oct 7, 2023

Description

The ViewWillLayoutSubviews() method will be executed multiple times and CollectionView.Frame.Width is finally set to the correct value.
But UpdateHeaderFooterPosition() will not be executed again at the finally execution,
so header and footer`s width will set to the wrong CollectionView.Frame.Width.

  	// This update is only relevant if you have a footer view because it's used to place the footer view
  	// based on the ContentSize so we just update the positions if the ContentSize has changed

As the comments in the code say, ViewWillLayoutSubviews() only recalculates the footer`s position, but actually changes the width of the header and footer, so it is necessary to fix it.

https://github.com/dotnet/maui/blob/ff50be2f541a7eef49243c4d23c4615db20fc581/src/Controls/src/Core/Handlers/Items/iOS/StructuredItemsViewController.cs#L78C48-L78C48

		public override void ViewWillLayoutSubviews()
		{
			base.ViewWillLayoutSubviews();

			// This update is only relevant if you have a footer view because it's used to place the footer view
			// based on the ContentSize so we just update the positions if the ContentSize has changed
			if (_footerUIView != null)
			{
				var emptyView = CollectionView.ViewWithTag(EmptyTag);

				if (IsHorizontal)
				{
					if (_footerUIView.Frame.X != ItemsViewLayout.CollectionViewContentSize.Width ||
						_footerUIView.Frame.X < emptyView?.Frame.X)
						UpdateHeaderFooterPosition();
				}
				else
				{
					if (_footerUIView.Frame.Y != ItemsViewLayout.CollectionViewContentSize.Height ||
						_footerUIView.Frame.Y < (emptyView?.Frame.Y + emptyView?.Frame.Height))
						UpdateHeaderFooterPosition();
				}
			}
		}

https://github.com/dotnet/maui/blob/ff50be2f541a7eef49243c4d23c4615db20fc581/src/Controls/src/Core/Handlers/Items/iOS/StructuredItemsViewController.cs#L149C3-L149C36

void UpdateHeaderFooterPosition(){
...
				if (_headerUIView != null && _headerUIView.Frame.Y != headerHeight)
				{
					_headerUIView.Frame = new CoreGraphics.CGRect(0, -headerHeight, CollectionView.Frame.Width, headerHeight);
				}
...
				if (_footerUIView != null && (_footerUIView.Frame.Y != height || emptyHeight > 0))
				{
					_footerUIView.Frame = new CoreGraphics.CGRect(0, height + emptyHeight, CollectionView.Frame.Width, footerHeight);
				}
...
}

Steps to Reproduce

No response

Link to public reproduction project repository

No response

Version with bug

8.0.0-rc.1.9171

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

Unknown/Other

Affected platforms

iOS

Affected platform versions

No response

Did you find any workaround?

Maybe there is another better way.

		public override void ViewWillLayoutSubviews()
		{
			base.ViewWillLayoutSubviews();

			// This update is only relevant if you have a footer view because it's used to place the footer view
			// based on the ContentSize so we just update the positions if the ContentSize has changed
			if (_footerUIView != null)
			{
				var emptyView = CollectionView.ViewWithTag(EmptyTag);

				if (IsHorizontal)
				{
					if (_footerUIView.Frame.X != ItemsViewLayout.CollectionViewContentSize.Width ||
						_footerUIView.Frame.X < emptyView?.Frame.X)
						UpdateHeaderFooterPosition();
				}
				else
				{
					if (_footerUIView.Frame.Y != ItemsViewLayout.CollectionViewContentSize.Height ||
						_footerUIView.Frame.Y < (emptyView?.Frame.Y + emptyView?.Frame.Height))
						UpdateHeaderFooterPosition(true);
				}
			}
		}

void UpdateHeaderFooterPosition(bool justPosition = false)
{
...
				if (_headerUIView != null && _headerUIView.Frame.Y != headerHeight)
				{
					_headerUIView.Frame = new CoreGraphics.CGRect(0, -headerHeight, justPosition ? _headerUIView.Frame.Width : CollectionView.Frame.Width, headerHeight);
				}
...
				if (_footerUIView != null && (_footerUIView.Frame.Y != height || emptyHeight > 0))
				{
					_footerUIView.Frame = new CoreGraphics.CGRect(0, height + emptyHeight, justPosition ? _footerUIView .Frame.Width : CollectionView.Frame.Width, footerHeight);
				}
...
}

Relevant log output

No response

@maonaoda maonaoda added the t/bug Something isn't working label Oct 7, 2023
@maonaoda
Copy link
Contributor Author

maonaoda commented Oct 7, 2023

https://github.com/maonaoda/Bug17885

expected:
image

actual:
image

@jsuarezruiz jsuarezruiz added area-controls-collectionview CollectionView, CarouselView, IndicatorView platform/iOS 🍎 labels Oct 9, 2023
@jsuarezruiz jsuarezruiz added this to the Backlog milestone Oct 9, 2023
@ghost
Copy link

ghost commented Oct 9, 2023

We've added this issue to our backlog, and we will work to address it as time and resources allow. If you have any additional information or questions about this issue, please leave a comment. For additional info about issue management, please read our Triage Process.

@XamlTest XamlTest added s/verified Verified / Reproducible Issue ready for Engineering Triage s/triaged Issue has been reviewed labels Oct 12, 2023
@XamlTest
Copy link

XamlTest commented Oct 12, 2023

Verified this on Visual Studio Enterprise 17.8.0 Preview 3.0(8.0.0-rc.2.9373). Repro on iOS 16.4, not repro on Android 13.0-API33 with provided Project:
Bug17885.zip

@bradencohen
Copy link
Contributor

bradencohen commented Oct 17, 2023

This bug also impacts any CollectionView header that uses CSS as well (on iOS).

The header always stays the initial size, and the content displays out of the bounds, causing the elements to overlap the CollectionView items. This bug causes problems with both width/height.

I created a simple behavior that seems to fix the issue for me:

public class CollectionViewResizeHeaderBehavior : Behavior<CollectionView>
{
    protected override void OnAttachedTo( CollectionView bindable )
    {

        /*
         * BC 10/17/2023
         * This is some iOS specific code that is needed to force the header to resize when the inner content is resized.
         * This is a bug in .NET MAUI, and we can track the progress here: https://github.com/dotnet/maui/issues/17885
         */
#if IOS
        if ( bindable.Header is VisualElement element )
        {
            bindable.Dispatcher.Dispatch( () =>
            {
                element.InvalidateMeasureNonVirtual( ( Microsoft.Maui.Controls.Internals.InvalidationTrigger.Undefined ) );
            } );
        }
#endif

        base.OnAttachedTo( bindable );
    }
}        

In my case, I add the behavior to every CollectionView thru the handler:

  /// <summary>
  /// Configures the Collection View handler to apply a behavior that fixes a bug in .NET MAUI where the
  /// header of a collection view does not resize when the inner content.
  /// </summary>
  private static void ConfigureCollectionViewHeaderFix()
  {
      CollectionViewHandler.Mapper.AppendToMapping( "CollectionViewResizeHeaderBehavior", ( handler, view ) =>
      {
          if ( view is CollectionView collectionView )
          {
              collectionView.Behaviors.Add( new CollectionViewResizeHeaderBehavior() );
          }
      } );
  }

@maonaoda
Copy link
Contributor Author

maonaoda commented Oct 18, 2023

In fact, both Header and Footer have added MeasureInvalidated.
But when Header or Footers contents child elements size changes, this MeasureInvalidated will be never triggered at all.

I currently just add a property to manually trigger the MapHeaderTemplate or MapFooterTemplate when Header (Footer) needs
size change.

		internal void UpdateSubview(object view, DataTemplate viewTemplate, nint viewTag, ref UIView uiView, ref VisualElement formsElement)
		{
			uiView?.RemoveFromSuperview();

			if (formsElement != null)
			{
				ItemsView.RemoveLogicalChild(formsElement);
				formsElement.MeasureInvalidated -= OnFormsElementMeasureInvalidated;
			}

			UpdateView(view, viewTemplate, ref uiView, ref formsElement);

			if (uiView != null)
			{
				uiView.Tag = viewTag;
				CollectionView.AddSubview(uiView);
			}

			if (formsElement != null)
				ItemsView.AddLogicalChild(formsElement);

			if (formsElement != null)
			{
				RemeasureLayout(formsElement);
				formsElement.MeasureInvalidated += OnFormsElementMeasureInvalidated;
			}
			else if (uiView != null)
			{
				uiView.SizeToFit();
			}
		}

@maonaoda
Copy link
Contributor Author

Not only the width, but also the height is completely fixed, which prevents me from dynamically displaying the view`s height based on the Binding.

@TobyDevelopment
Copy link

TobyDevelopment commented Feb 22, 2024

@maonaoda I am experiencing the same issue. From my testing the CollectionView Header dimensions do not update if elements are shown / hidden or the device orientation is changed. Also, if you constrain the width of the CollectionView the header does not constrain to that same value. Calling InvalidateMeasureNonVirtual(Microsoft.Maui.Controls.Internals.InvalidationTrigger.Undefined) after showing / hiding elements (and in OnSizeAllocated override for device orientation changes) does fix but is a hack.

@maonaoda
Copy link
Contributor Author

maonaoda commented Feb 26, 2024

It needs a suitable time to use InvalidateMeasureNonVirtual(Microsoft.Maui.Controls.Internals.InvalidationTrigger.Undefined)
In the end I solved it like this

#if IOS
    // temporary best workaround for https://github.com/dotnet/maui/issues/17885
    public partial class CollectionViewHeaderFooterLayoutHandler : LayoutHandler
    {
        protected override HeaderFooterLayoutView CreatePlatformView()
        {
            return new HeaderFooterLayoutView()
            {
                CrossPlatformLayout = VirtualView
            };
        }
    }


    public class HeaderFooterLayoutView : LayoutView
    {
        public override void LayoutSubviews()
        {
            base.LayoutSubviews();

            var bounds = AdjustForSafeArea(Bounds).ToRectangle();

            var widthConstraint = bounds.Width;
            var heightConstraint = bounds.Height;

            var size = CrossPlatformLayout.CrossPlatformMeasure(widthConstraint, double.PositiveInfinity);
            if (CrossPlatformLayout is VisualElement element
                && Math.Abs(heightConstraint - size.Height) > 0.1)
            {
                element.InvalidateMeasureNonVirtual(Microsoft.Maui.Controls.Internals.InvalidationTrigger.Undefined);
            }
        }
    }
#endif
    public class CollectionViewHeaderFooterGrid : Grid { }

    public class CollectionViewHeaderFooterStackLayout : StackLayout { }

Set CollectionViewHeaderFooterLayoutHandler to CollectionViewHeaderFooterGrid and CollectionViewHeaderFooterStackLayout ,then use CollectionViewHeaderFooterGrid and CollectionViewHeaderFooterStackLayout under CollectionView.Header or CollectionView.Footer

※Note that there may be a better way. But currently I can only use this because maui has no plans to repair it at all.

@acaliaro
Copy link

@samhouts do you have some news about this?

@acaliaro
Copy link

It needs a suitable time to use InvalidateMeasureNonVirtual(Microsoft.Maui.Controls.Internals.InvalidationTrigger.Undefined) In the end I solved it like this

#if IOS
    // temporary best workaround for https://github.com/dotnet/maui/issues/17885
    public partial class CollectionViewHeaderFooterLayoutHandler : LayoutHandler
    {
        protected override HeaderFooterLayoutView CreatePlatformView()
        {
            return new HeaderFooterLayoutView()
            {
                CrossPlatformLayout = VirtualView
            };
        }
    }


    public class HeaderFooterLayoutView : LayoutView
    {
        public override void LayoutSubviews()
        {
            base.LayoutSubviews();

            var bounds = AdjustForSafeArea(Bounds).ToRectangle();

            var widthConstraint = bounds.Width;
            var heightConstraint = bounds.Height;

            var size = CrossPlatformLayout.CrossPlatformMeasure(widthConstraint, double.PositiveInfinity);
            if (CrossPlatformLayout is VisualElement element
                && Math.Abs(heightConstraint - size.Height) > 0.1)
            {
                element.InvalidateMeasureNonVirtual(Microsoft.Maui.Controls.Internals.InvalidationTrigger.Undefined);
            }
        }
    }
#endif
    public class CollectionViewHeaderFooterGrid : Grid { }

    public class CollectionViewHeaderFooterStackLayout : StackLayout { }

Set CollectionViewHeaderFooterLayoutHandler to CollectionViewHeaderFooterGrid and CollectionViewHeaderFooterStackLayout ,then use CollectionViewHeaderFooterGrid and CollectionViewHeaderFooterStackLayout under CollectionView.Header or CollectionView.Footer

※Note that there may be a better way. But currently I can only use this because maui has no plans to repair it at all.

@maonaoda why you check heightConstraint - size.Height when the problem is on the width?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-controls-collectionview CollectionView, CarouselView, IndicatorView delighter-sc platform/iOS 🍎 s/triaged Issue has been reviewed s/verified Verified / Reproducible Issue ready for Engineering Triage t/bug Something isn't working
Projects
None yet
Development

No branches or pull requests

7 participants