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

Android Layout.WidthRequest/Layout.HeightRequest mismatches Layout.Measure results #25365

Open
LeadAssimilator opened this issue Oct 18, 2024 · 4 comments
Labels
area-layout StackLayout, GridLayout, ContentView, AbsoluteLayout, FlexLayout, ContentPresenter layout-grid migration-compatibility Xamarin.Forms to .NET MAUI Migration, Upgrade Assistant, Try-Convert platform/android 🤖 t/bug Something isn't working

Comments

@LeadAssimilator
Copy link

LeadAssimilator commented Oct 18, 2024

Description

Setting WidthRequest and/or HeightRequest on any Layout control like Grid or StackLayout in MAUI causes it to sometimes Measure slightly larger than the requested size on Android. Even setting MaximumWidthRequest and MaximumHeightRequest yields the same slightly oversized measure for some values. XF however always measures to the requested values.

While the oversized amount is always less than 1, that error can compound in various cases, especially with complex custom layouts containing many sub layouts which end up causing elements that were expected to fit in a given space to be forced into another location. For example, take a layout with a WidthRequest LWR that has N children that are grids with WidthRequest IWR such that LWR = K*(IWR+S)-S where S is some inter-item spacing and K is the number of elements that should fit in the given width LWR. In XF, K items will always fit, but in MAUI sometimes K will fit and sometimes only K-1 will fit due to the measure error.

Steps to Reproduce

  1. Create a new MAUI and XF application (android only)
  2. In App.cs: MainPage = new MainPage(); (aka delete the shell)
  3. Replace the content of MainPage.xaml with a Grid named "layout" and set the Grid WidthRequest = 117 and HeightRequest = 117
	<ContentPage.Content>
		<Grid
			x:Name="layout"
			WidthRequest="117"
			HeightRequest="117">
		</Grid>
	</ContentPage.Content>
  1. Override MainPage.LayoutChildren and call/log layout.Measure(width, height);
	public partial class MainPage : ContentPage
	{
		public MainPage()
		{
			InitializeComponent();
		}

		protected override void LayoutChildren(double x, double y, double width, double height)
		{
			var sizeRequest = layout.Measure(width, height);
			Console.WriteLine($"Layout measures to: {sizeRequest}");

			base.LayoutChildren(x, y, width, height);
		}
	}
  1. Create a Pixel 7 Pro on api 34 via ADM from Visual Studio with default settings
  2. Deploy and run both apps
  3. Observe the same width/height yielding different measure results:
  • MAUI: Request=117.142857142857x117.142857142857, Minimum=117.142857142857x117.142857142857
  • XF: Request=117x117, Minimum=117x117
  1. Replace MainPage.xaml content with:
	<ContentPage.Content>
		<FlexLayout
			x:Name="layout"
			WidthRequest="234"
			HeightRequest="234"
			BackgroundColor="Yellow"
			Wrap="Wrap">
			<BoxView WidthRequest="117" HeightRequest="117" BackgroundColor="Red" />
			<BoxView WidthRequest="117" HeightRequest="117" BackgroundColor="Black" />
		</FlexLayout>
	</ContentPage.Content>
  1. Observe the unexpected layout where the red and black squares are vertically stacked instead of horizontally due to the error pushing it down a row as explained in this comment:
    Image

Link to public reproduction project repository

No response

Version with bug

8.0.92 SR9.2

Is this a regression from previous behavior?

Yes, this used to work in Xamarin.Forms

Last version that worked well

No response

Affected platforms

Android

Affected platform versions

Android 34 and up on Pixel 7 Pro emulator

Did you find any workaround?

No response

Relevant log output

No response

@LeadAssimilator LeadAssimilator added the t/bug Something isn't working label Oct 18, 2024
@samhouts samhouts added platform/android 🤖 migration-compatibility Xamarin.Forms to .NET MAUI Migration, Upgrade Assistant, Try-Convert area-layout StackLayout, GridLayout, ContentView, AbsoluteLayout, FlexLayout, ContentPresenter layout-grid labels Oct 18, 2024
@PureWeen PureWeen added the s/needs-repro Attach a solution or code which reproduces the issue label Oct 18, 2024
@LeadAssimilator
Copy link
Author

The reproduction steps are the sample.

@dotnet-policy-service dotnet-policy-service bot added s/needs-attention Issue has more information and needs another look and removed s/needs-repro Attach a solution or code which reproduces the issue labels Oct 19, 2024
@LeadAssimilator
Copy link
Author

Also interesting and inconsistent behavior is the measure results of putting certain controls directly in the root of a page versus nesting them in a layout versus in a compatibility layout and when they are measured at different times when having a size request of 117 on this device.

Any layout when added anywhere always measures as 117.142857142857.
Any view when added to the root of a ContentPage always measures as 117.
Any view when added to a compatibility layout (including ContentView) always measures to 117.
Any non-compatibility-layout view (aka not ContentView) when added to a layout always measures as 117.142857142857.
Any compatibility layout (including ContentView) when added to a layout measures as 117.142857142857 during the parent layout.
Any compatibility layout (including ContentView) when added to a layout measures as 117 during its children layout.
Any compatibility layout (including ContentView) when added to another compatibility layout always measures as 117.

If controls are allowed to be larger than their Size Requests in MAUI then is a breaking change from XF that needs documented, and it means the Maximum Size Request isn't actually a maximum either since it is getting exceeded. But then this behavior change also leads to the scenario outlined in the OP where we can be relying on a certain number of controls fitting in a given area which won't all fit once the over-sized amount compounds. And that behavior is also ultimately at odds with how controls measure when contained in ContentPage/TemplatedPage or ContentView/TemplatedView which silently act like or are compatibility layouts.

So something is surely wrong here...

@PureWeen PureWeen added s/needs-repro Attach a solution or code which reproduces the issue and removed s/needs-attention Issue has more information and needs another look labels Oct 21, 2024
@LeadAssimilator
Copy link
Author

Again, the reproduction steps are the sample. If they are followed, the problem can be reproduced easily. The steps themselves are not long or complicated. It is two snippets of code. Please explain why they are insufficient and why you even ask for them if you aren't going to use them.

@dotnet-policy-service dotnet-policy-service bot added s/needs-attention Issue has more information and needs another look and removed s/needs-repro Attach a solution or code which reproduces the issue labels Oct 21, 2024
@LeadAssimilator
Copy link
Author

It appears that Layout.Measure overrides VisualElement.Measure and instead calls IView.Measure which behaves very differently from VisualElement.Measure's base implementation. This partly explains the strange behavior I noticed above where different measures in different contexts and children layouts passes were giving different results, because I was inadvertently calling different Measures without realizing it.

IView.Measure appears to do a platform measure and is prone to compounding pixel space conversion errors while VisualElement.Measure doesn't, so those errors are avoided. The fact that Layout.Measure calls IView.Measure seems like the underlying bug here as it leads to completely different and unexpected results. Why is it even doing that? If you want IView.Measure behavior, then cast and call it explicitly, no? Clearly I'm missing something here as to why there are two different implementations of Measure and why Layout wants the one (error prone) behavior and everything else (including compatibility Layout) wants the other.

In the Pixel 7 Pro emulator case, the conversion errors are because the device's native resolution is 1440px (width) with a density of 3.5. When WidthRequest or even MaximumWidthRequest is 117dp, it yields an android measure spec of 410px = (int)(float)Math.Ceiling(117dp * 3.5). Because this is an exact measure 410px is returned by the android platform measure, and that gets converted to dp again 117.142857142857dp = 410px / 3.5. So now we have an oversized element, exceeding even the original MaximumWidthRequest by 0.142857142857dp.

If you try to size a layout exactly so it can contain N elements of W size via setting the WidthRequest (or MaximumWidthRequest) of the elements to W and the WidthRequest of the containing layout to N * W then things won't actually fit. Take the simple case where N = 2 and W = 117. The layout will measure to exactly 234dp = 117dp * 2. Since 234dp is even, no half is added in the to px conversion nor does the conversion back to dp grow. However, the elements of the layout will still measure to 117.142857142857dp and thus take up in total 234.285714285714dp = 2 * 117.142857142857dp which is greater than the layout constrained size of 234dp by 0.285714285714dp.

Depending on the layout, the error can compound and cause cutoff elements, unwanted scrolling or elements to get pushed onto the next row. A good example of this is a FlexLayout of BoxViews. When FlexLayout.WidthRequest = 234, FlexLayout.Wrap = Wrap and BoxView.WidthRequest = 117, one expects 2 elements to fit side by side, but only one does due to the compounding error.

Now if the density scale factor is always a whole or half, then one could say just don't use odd numbers and put a giant warning/note box in the maui docs, but is that really the case?

In general, this whole mess could be worked around by taking the floor of the px -> dp conversion to strip the error added by the ceiling when going dp -> px as long as the density scale factor is at least 1, but is that safe in all cases? Is the error actually wanted somewhere for some reason?

Even with all that, something seems fundamentally awry given the surprising different Layout.Measure vs. VisualElement.Measure implementations and results, which one would expect to actually be the same.

@LeadAssimilator LeadAssimilator changed the title Android Grid.WidthRequest/Grid.HeightRequest mismatches Grid.Measure results Android Layout.WidthRequest/Layout.HeightRequest mismatches Layout.Measure results Oct 22, 2024
@PureWeen PureWeen added this to the .NET 9 Servicing milestone Oct 22, 2024
@PureWeen PureWeen removed the s/needs-attention Issue has more information and needs another look label Oct 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-layout StackLayout, GridLayout, ContentView, AbsoluteLayout, FlexLayout, ContentPresenter layout-grid migration-compatibility Xamarin.Forms to .NET MAUI Migration, Upgrade Assistant, Try-Convert platform/android 🤖 t/bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants