maui icon indicating copy to clipboard operation
maui copied to clipboard

[Android] updating StackLayout.Orientation does not trigger a re-layout of the children

Open azeymur opened this issue 2 years ago • 11 comments

Description

The code I wrote to show two squares in a responsive layout does not work properly on Android Systems because the width and height values passed to the OnSizeAllocated method are incorrect.

Screen Shot 2022-05-13 at 10 49 35 Screen Shot 2022-05-13 at 10 50 00

Steps to Reproduce

  1. Create a new MAUI project.
  2. Add following code to MainPage.xaml to showing two square.
    <StackLayout x:Name="parent"
        Orientation="Horizontal">

        <BoxView Color="Blue"
                 HorizontalOptions="FillAndExpand"
                 VerticalOptions="FillAndExpand"/>

        <BoxView Color="Green"
                 HorizontalOptions="FillAndExpand"
                 VerticalOptions="FillAndExpand"/>        

    </StackLayout>
  1. Add following code to the MainPage class in the MainPage.xaml.cs file to getting responsive layout.
    double width, height;

    protected override void OnSizeAllocated(double width, double height)
    {
        base.OnSizeAllocated(width, height);

        if (width != this.width || height != this.height)
        {
            this.width = width;
            this.height = height;

            if (width > height)
            {
                parent.Orientation = StackOrientation.Horizontal;
            }
            else
            {
                parent.Orientation = StackOrientation.Vertical;
            }
        }
    }

Version with bug

Release Candidate 3 (current)

Last version that worked well

Unknown/Other

Affected platforms

Android

Affected platform versions

Android 12

Did you find any workaround?

No response

Relevant log output

No response

azeymur avatar May 13 '22 09:05 azeymur

Verified this issue with Visual Studio Enterprise 17.3.0 Preview 1.0 [32427.455.main]. Repro on Android 12. Sample Project: MauiApp9.zip

XamlTest avatar May 13 '22 09:05 XamlTest

I am experiencing the same problem but with some differences.

In my case the code I use to change orientation to the stacklayout is this:

DisplayOrientation _displayOrientation = DisplayOrientation.Unknown;
protected override void OnSizeAllocated(double width, double height)
{
    base.OnSizeAllocated(width, height);

    if (DeviceDisplay.MainDisplayInfo.Orientation != _displayOrientation)
    {
        _displayOrientation = DeviceDisplay.MainDisplayInfo.Orientation;

        if (_displayOrientation == DisplayOrientation.Portrait)
            mainStackLayout.Orientation = StackOrientation.Vertical;
        else if (_displayOrientation == DisplayOrientation.Landscape)
            mainStackLayout.Orientation = StackOrientation.Horizontal;

        //this.ForceLayout();
    }
}

Even using this, the orientation of the stacklayout is wrong. It is as if once the Orientation variable is set the actual change is not propagated and the layout is not updated. By rotating the screen again, the change happens, but it turns out to be the previous one. So if we try to reverse it:

if (_displayOrientation == DisplayOrientation.Portrait)
    mainStackLayout.Orientation = StackOrientation.Horizontal;
else if (_displayOrientation == DisplayOrientation.Landscape)
    mainStackLayout.Orientation = StackOrientation.Vertical;

the result turns out to be partially correct.

By debugging the function, the height and width values look correct to me. The problem does not occur on windows or on ios.

FedericoNembrini avatar Jul 05 '22 10:07 FedericoNembrini

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

ghost avatar Aug 30 '22 14:08 ghost

This issue is still present (current framework, latest VS 2022, etc.)

euquiq avatar Feb 07 '23 02:02 euquiq

The issue is present for me with VS 2022 version 17.5.0, following the exact description by @FedericoNembrini above (https://github.com/dotnet/maui/issues/7134#issuecomment-1174873953). The values of width and height seem correct, but setting the orientation yields the opposite orientation, on Android only.

nshtinkov avatar Feb 23 '23 22:02 nshtinkov

I'm trying to play with the source code a bit, from what I can see when changing the orientation, the method OrientationChanged is called

static void OrientationChanged(BindableObject bindable, object oldValue, object newValue)
{
	var layout = (StackLayout)bindable;
	layout.InvalidateMeasure();
}

This however, at least for android, does not re-arrange the children of the element. At the moment, I have not yet been able to understand what the InvalidateMeasure function (and its sub-functions) actually does. By manually calling up the arrangement of it's child, the stacklyout is displayed correctly. I see that there is a predisposition for a function called InvalidateArrange, but it is not currently used.

PS: In my opinion, this issue should change its name to reflect the problem.

FedericoNembrini avatar Feb 28 '23 13:02 FedericoNembrini

Verified this issue with Visual Studio Enterprise 17.6.0 Preview 5.0. Repro on Android emulator with Sample Project: MauiApp9.zip image image

homeyf avatar May 05 '23 03:05 homeyf

I checked the code and I agree with @FedericoNembrini - this is not an OnSizeAllocated issue because the values in there are correct - but the subsequint layout is not being reflected. I also tried calling InvalidateMeasure after updating the orientation and this does not appear to do anything.

One workaround was to use the dispatcher to request a re-layout outside the current layout pass:

Dispatcher.Dispatch(() => ((IView)parent).InvalidateMeasure());

I just added this to the bottom of the OnSizeAllocated method and it seems to work.

mattleibow avatar May 19 '23 08:05 mattleibow

In the issue linked above (#9227), @mattleibow suggested wrapping the StackLayout in a Grid (which, AIUI, provides something to respond to the layout invalidation and actually process the remeasure).

That issue is about containing the control in a ScrollView, but as this issue shows, that is not the only problem scenario. Nevertheless, I found the same fix worked when the Stacklayout is the root layout in a ContentPage and was similarly reacting inconsistently, wrongly or not at all to Orientation changes (on Android - not tested other platforms).

TL;DR: A workaround in some (perhaps all) circumstances is to wrap the StackLayout in a Grid.

BobSammers avatar Jul 04 '23 09:07 BobSammers

Thanks for the workarounds. I am able to repro with .NET 7 on Pixel 4 device running Android 13. Keeping controls inside a StackLayout and changing the layout orientation when a mobile device is tilted (portrait -> landscape, & vice versa) seems like a common design choice for .NET MAUI.

<Style x:Key="DynamicStackLayout" TargetType="StackLayout">
    <Setter Property="VisualStateManager.VisualStateGroups">
        <VisualStateGroupList>
            <VisualStateGroup>
                <VisualState x:Name="Horizontal">
                    <VisualState.StateTriggers>
                        <OrientationStateTrigger Orientation="Landscape" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Property="Orientation" Value="Horizontal" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="Vertical">
                    <VisualState.StateTriggers>
                        <OrientationStateTrigger Orientation="Portrait" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Property="Orientation" Value="Vertical" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateGroupList>
    </Setter>
</Style>

Designing UI that accounts for device orientation has been challenging enough with .NET MAUI. I can't justify the time adding a workaround to my app with hundreds of ContentPages, especially when the issue is specific to one platform. Hoping this issue can get resolved sooner than later, it has been a real time sink.

williambohrmann3 avatar Oct 18 '23 20:10 williambohrmann3

A similar issue is still present in MAUI .Net8, my issue has been with a custom grid layout component that reacts to the aspect ratio of the available space for the control. The control fails to work as expected on android but works on other platforms, my workaround is to nest the control inside a content view and uses Dispatcher.Dispatch(() => ((IView)Parent).InvalidateMeasure()); as @mattleibow suggests.

Fred-Hudson-CST avatar Jan 11 '24 04:01 Fred-Hudson-CST

this issue keeps showing up and causing problems ... has anyone found a work around that does not involve nesting views, I need to control shell behaviour based on screen size and cant call Dispatcher.Dispatch(() => ((IView)Parent).InvalidateMeasure()); to resolve the problem

Fred-Hudson-CST avatar Feb 12 '24 20:02 Fred-Hudson-CST

this issue keeps showing up and causing problems ... has anyone found a work around that does not involve nesting views, I need to control shell behaviour based on screen size and cant call Dispatcher.Dispatch(() => ((IView)Parent).InvalidateMeasure()); to resolve the problem

My issue is resolved on Maui 8.0.7 Nuget. No Dispatcher with InvalidateMeasure is needed now. Entry tests are showing, that it works flawlessly

SebastianMikulski avatar Feb 26 '24 10:02 SebastianMikulski

this issue keeps showing up and causing problems ... has anyone found a work around that does not involve nesting views, I need to control shell behaviour based on screen size and cant call Dispatcher.Dispatch(() => ((IView)Parent).InvalidateMeasure()); to resolve the problem

My issue is resolved on Maui 8.0.7 Nuget. No Dispatcher with InvalidateMeasure is needed now. Entry tests are showing, that it works flawlessly

Wish I could replicate your success, android behavior is still buggy and inconsistent for me on 8.0.7

Fred-Hudson-CST avatar Feb 26 '24 17:02 Fred-Hudson-CST