Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Controls/src/Core/Page/Page.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace Microsoft.Maui.Controls
/// <remarks><see cref = "Page" /> is primarily a base class for more useful derived types. Objects that are derived from the <see cref="Page"/> class are most prominently used as the top level UI element in .NET MAUI applications. In addition to their role as the main pages of applications, <see cref="Page"/> objects and their descendants can be used with navigation classes, such as <see cref="NavigationPage"/> or <see cref="FlyoutPage"/>, among others, to provide rich user experiences that conform to the expected behaviors on each platform.
/// </remarks>
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
public partial class Page : VisualElement, ILayout, IPageController, IElementConfiguration<Page>, IPaddingElement, ISafeAreaView, ISafeAreaView2, IView, ITitledElement, IToolbarElement, IConstrainedView
public partial class Page : VisualElement, ILayout, IPageController, IElementConfiguration<Page>, IPaddingElement, ISafeAreaView, ISafeAreaPage, IView, ITitledElement, IToolbarElement, IConstrainedView
#if IOS
,IiOSPageSpecifics
#endif
Expand Down Expand Up @@ -253,7 +253,7 @@ int IiOSPageSpecifics.PreferredStatusBarUpdateAnimationMode
#endif

/// <inheritdoc/>
Thickness ISafeAreaView2.SafeAreaInsets
Thickness ISafeAreaPage.SafeAreaInsets
{
set
{
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue24246.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue24246"
Title="Issue24246"
xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"
ios:Page.UseSafeArea="false">
<VerticalStackLayout Background="Purple" IgnoreSafeArea="False" VerticalOptions="Start" IsClippedToBounds="True" >
<Entry AutomationId="entry" Background="Green"></Entry>
<Label AutomationId="label" Text="If you can't interact with the above entry field, the test has failed" LineBreakMode="CharacterWrap"></Label>
</VerticalStackLayout>
</ContentPage>

@albyrock87 albyrock87 Mar 10, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the goal of this UITest, the entry should be visible and the user should be able to interact with it.
image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

I was curious what this looked like on main so if anyone else was wondering :)
edit: this is already here: #28264

11 changes: 11 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue24246.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Maui.Controls.Sample.Issues;


[Issue(IssueTracker.Github, 24246, "SafeArea arrange insets are currently insetting based on an incorrect Bounds", PlatformAffected.iOS)]
public partial class Issue24246 : ContentPage
{
public Issue24246()
{
InitializeComponent();
}
}
53 changes: 53 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue27715.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue27715">

<ContentPage.Resources>
<Style TargetType="Border"
x:Key="CardStyle">
<Setter Property="StrokeShape"
Value="RoundRectangle 20"/>
<Setter Property="Background"
Value="#E0E0E0"/>
<Setter Property="StrokeThickness"
Value="0"/>
<Setter Property="Padding"
Value="15"/>
</Style>
</ContentPage.Resources>



<Grid BackgroundColor="red">
<ScrollView Orientation="Vertical">
<VerticalStackLayout Spacing="5"
Padding="15">
<Border Style="{StaticResource CardStyle}">
<Label Text="Border"/>
</Border>
<ScrollView Orientation="Horizontal"
Margin="-30,0">
<HorizontalStackLayout Spacing="15"
Padding="30,0"
BindableLayout.ItemsSource="{Binding Projects}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource CardStyle}">
<VerticalStackLayout Spacing="15">
<Image HorizontalOptions="Start"
Source="dotnet_bot.png"
Aspect="Center">
</Image>
</VerticalStackLayout>
</Border>
</DataTemplate>
</BindableLayout.ItemTemplate>
</HorizontalStackLayout>
</ScrollView>
<Label Text="Tasks"
AutomationId="label"/>
</VerticalStackLayout>
</ScrollView>
</Grid>
</ContentPage>
27 changes: 27 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue27715.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Collections.ObjectModel;

namespace Maui.Controls.Sample.Issues
{
[Issue(IssueTracker.Github, 27715, "ScrollView inside a Grid expands width past device screen when rotated", PlatformAffected.iOS)]
public partial class Issue27715 : ContentPage
{
public Issue27715()
{
InitializeComponent();
BindingContext = new Issue27715_ViewModel();

}
}

public class Issue27715_ViewModel
{
static string[] items = new[] { "Project 1", "Project 2", "Project 3", "Project 4", "Project 5" };

public ObservableCollection<string> Projects { get; set; } = new ObservableCollection<string>(items);

public Issue27715_ViewModel()
{
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue24246 : _IssuesUITest
{
public Issue24246(TestDevice testDevice) : base(testDevice)
{
}

public override string Issue => "SafeArea arrange insets are currently insetting based on an incorrect Bounds";

[Test]
[Category(UITestCategories.Layout)]
public void TapThenDoubleTap()
{
App.WaitForElement("entry");
App.EnterText("entry", "Hello, World!");

var result = App.WaitForElement("entry").GetText();
Assert.That(result, Is.EqualTo("Hello, World!"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#if ANDROID || IOS //Issue is only reproducible on Android and iOS platforms.
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue27715 : _IssuesUITest
{

public Issue27715(TestDevice testDevice) : base(testDevice)
{
}

public override string Issue => "ScrollView inside a Grid expands width past device screen when rotated";

[Test]
[Category(UITestCategories.ScrollView)]
public void ScrollViewShouldRenderWithinBounds()
{
App.WaitForElement("label");
App.SetOrientationLandscape();
VerifyScreenshot();
}
}
}
#endif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/// <remarks>
/// This interface is only recognized on the iOS/Mac Catalyst platforms; other platforms will ignore it.
/// </remarks>
internal interface ISafeAreaView2
internal interface ISafeAreaPage
{
/// <summary>
/// Internal property for the Page's SafeAreaInsets Thickness that may be changed in the future.
Expand Down
116 changes: 108 additions & 8 deletions src/Core/src/Platform/iOS/MauiScrollView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ namespace Microsoft.Maui.Platform
public class MauiScrollView : UIScrollView, IUIViewLifeCycleEvents, ICrossPlatformLayoutBacking, IPlatformMeasureInvalidationController
{
bool _invalidateParentWhenMovedToWindow;
bool? _scrollViewDescendant;

double _lastMeasureHeight;
double _lastMeasureWidth;
double _lastArrangeHeight;
double _lastArrangeWidth;

UIUserInterfaceLayoutDirection? _previousEffectiveUserInterfaceLayoutDirection;
SafeAreaPadding _safeArea = SafeAreaPadding.Empty;
bool _safeAreaInvalidated = true;
bool _appliesSafeAreaAdjustments;

WeakReference<ICrossPlatformLayout>? _crossPlatformLayoutReference;

Expand All @@ -26,8 +31,37 @@ public class MauiScrollView : UIScrollView, IUIViewLifeCycleEvents, ICrossPlatfo

internal ICrossPlatformLayout? CrossPlatformLayout => ((ICrossPlatformLayoutBacking)this).CrossPlatformLayout;

public MauiScrollView()
{
ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never;
}

bool RespondsToSafeArea()
{
return !(_scrollViewDescendant ??= Superview.GetParentOfType<UIScrollView>() is not null);
}

public override void SafeAreaInsetsDidChange()
{
// Note: UIKit invokes LayoutSubviews right after this method
base.SafeAreaInsetsDidChange();

_safeAreaInvalidated = true;
}

public override void LayoutSubviews()
{
if (CrossPlatformLayout is null)
{
base.LayoutSubviews();
return;
}

if (!ValidateSafeArea())
{
InvalidateConstraintsCache();
}

// LayoutSubviews is invoked while scrolling, so we need to arrange the content only when it's necessary.
// This could be done via `override ScrollViewHandler.PlatformArrange` but that wouldn't cover the case
// when the ScrollView is attached to a non-MauiView parent (i.e. DeviceTests).
Expand All @@ -37,21 +71,20 @@ public override void LayoutSubviews()
var frameChanged = _lastArrangeWidth != widthConstraint || _lastArrangeHeight != heightConstraint;

// If the frame changed, we need to arrange (and potentially measure) the content again
if (frameChanged && CrossPlatformLayout is { } crossPlatformLayout)
if (frameChanged)
{
_lastArrangeWidth = widthConstraint;
_lastArrangeHeight = heightConstraint;

if (!IsMeasureValid(widthConstraint, heightConstraint))
{
crossPlatformLayout.CrossPlatformMeasure(widthConstraint, heightConstraint);
CrossPlatformMeasure(widthConstraint, heightConstraint);
CacheMeasureConstraints(widthConstraint, heightConstraint);
}

Size crossPlatformBounds;
// Account for safe area adjustments automatically added by iOS
var crossPlatformBounds = AdjustedContentInset.InsetRect(bounds).Size.ToSize();
var crossPlatformContentSize = crossPlatformLayout.CrossPlatformArrange(new Rect(new Point(), crossPlatformBounds));
var contentSize = crossPlatformContentSize.ToCGSize();
var contentSize = CrossPlatformArrange(Bounds, out crossPlatformBounds).ToCGSize();

// For Right-To-Left (RTL) layouts, we need to adjust the content arrangement and offset
// to ensure the content is correctly aligned and scrolled. This involves a second layout
Expand All @@ -61,7 +94,7 @@ public override void LayoutSubviews()
if (EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft)
{
var horizontalOffset = contentSize.Width - crossPlatformBounds.Width;
crossPlatformLayout.CrossPlatformArrange(new Rect(new Point(-horizontalOffset, 0), crossPlatformBounds));
CrossPlatformArrange(new Rect(new Point(-horizontalOffset, 0), crossPlatformBounds), out crossPlatformBounds);
ContentOffset = new CGPoint(horizontalOffset, 0);
}
else
Expand Down Expand Up @@ -89,17 +122,80 @@ public override void LayoutSubviews()
base.LayoutSubviews();
}

bool ValidateSafeArea()
{
// If nothing changed, we don't need to do anything
if (!_safeAreaInvalidated)
{
return true;
}

// Mark the safe area as validated given that we're about to check it
_safeAreaInvalidated = false;

var oldSafeArea = _safeArea;
_safeArea = SafeAreaInsets.ToSafeAreaInsets();

var oldApplyingSafeAreaAdjustments = _appliesSafeAreaAdjustments;
_appliesSafeAreaAdjustments = RespondsToSafeArea() && !_safeArea.IsEmpty;

// Return whether the way safe area interacts with our view has changed
return oldApplyingSafeAreaAdjustments == _appliesSafeAreaAdjustments &&
(oldSafeArea == _safeArea || !_appliesSafeAreaAdjustments);
}

Size CrossPlatformArrange(CGRect bounds, out Size adjustedBounds)
{
bounds = new CGRect(CGPoint.Empty, bounds.Size);
if (_appliesSafeAreaAdjustments)
{
bounds = _safeArea.InsetRect(bounds);
}

adjustedBounds = bounds.Size.ToSize();

var size = CrossPlatformLayout?.CrossPlatformArrange(bounds.ToRectangle()) ?? Size.Zero;

if (_appliesSafeAreaAdjustments)
{
size = new Size(size.Width + _safeArea.HorizontalThickness, size.Height + _safeArea.VerticalThickness);
}

return size;
}

Size CrossPlatformMeasure(double widthConstraint, double heightConstraint)
{
if (_appliesSafeAreaAdjustments)
{
// When responding to safe area, we need to adjust the constraints to account for the safe area.
widthConstraint -= _safeArea.HorizontalThickness;
heightConstraint -= _safeArea.VerticalThickness;
}

var crossPlatformSize = CrossPlatformLayout?.CrossPlatformMeasure(widthConstraint, heightConstraint) ?? Size.Zero;

if (_appliesSafeAreaAdjustments)
{
// If we're responding to the safe area, we need to add the safe area back to the size so the container can allocate the correct space
crossPlatformSize = new Size(crossPlatformSize.Width + _safeArea.HorizontalThickness, crossPlatformSize.Height + _safeArea.VerticalThickness);
}

return crossPlatformSize;
}

public override CGSize SizeThatFits(CGSize size)
{
if (CrossPlatformLayout is not { } crossPlatformLayout)
if (CrossPlatformLayout is null)
{
return new CGSize();
}

var widthConstraint = (double)size.Width;
var heightConstraint = (double)size.Height;

var contentSize = crossPlatformLayout.CrossPlatformMeasure(widthConstraint, heightConstraint);
var contentSize = CrossPlatformMeasure(widthConstraint, heightConstraint);

CacheMeasureConstraints(widthConstraint, heightConstraint);

return contentSize;
Expand Down Expand Up @@ -159,7 +255,11 @@ event EventHandler IUIViewLifeCycleEvents.MovedToWindow
public override void MovedToWindow()
{
base.MovedToWindow();

_movedToWindow?.Invoke(this, EventArgs.Empty);
_scrollViewDescendant = null;
_safeAreaInvalidated = true;

if (_invalidateParentWhenMovedToWindow)
{
_invalidateParentWhenMovedToWindow = false;
Expand Down
Loading