diff --git a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs index 04d41a5607a4..34e440f4c18b 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs @@ -448,6 +448,13 @@ public virtual void UpdateFlowDirection() { foreach (var child in ItemsView.LogicalChildrenInternal) { + // Skip the empty view element — its flow direction is handled + // separately in AlignEmptyView to avoid double application + if (child == _emptyViewFormsElement) + { + continue; + } + if (child is VisualElement ve && ve.Handler?.PlatformView is UIView view) { view.UpdateFlowDirection(ve); @@ -775,37 +782,30 @@ void AlignEmptyView() return; } - bool isRtl; - - if (OperatingSystem.IsIOSVersionAtLeast(10) || OperatingSystem.IsTvOSVersionAtLeast(10)) - isRtl = CollectionView.EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft; - else - isRtl = CollectionView.SemanticContentAttribute == UISemanticContentAttribute.ForceRightToLeft; - - if (isRtl) + if (_emptyViewFormsElement is not null) { - if (_emptyUIView.Transform.A == -1) + // The empty view's FlowDirection is handled here instead of in UpdateFlowDirection() + // to ensure proper alignment independent of the CollectionView's layout flip behavior. + if (_emptyViewFormsElement.Handler?.PlatformView is UIView emptyView) { - return; + emptyView.UpdateFlowDirection(_emptyViewFormsElement); } - - FlipEmptyView(); } - else + else if (_emptyUIView is UILabel label) { - if (_emptyUIView.Transform.A == -1) + // For UILabel, set the text alignment to center to ensure consistent behavior with Windows and Android + label.TextAlignment = UITextAlignment.Center; + label.SemanticContentAttribute = ItemsView.FlowDirection switch { - FlipEmptyView(); - } + FlowDirection.RightToLeft => UISemanticContentAttribute.ForceRightToLeft, + FlowDirection.LeftToRight => UISemanticContentAttribute.ForceLeftToRight, + _ => CollectionView.EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft + ? UISemanticContentAttribute.ForceRightToLeft + : UISemanticContentAttribute.ForceLeftToRight + }; } } - void FlipEmptyView() - { - // Flip the empty view 180 degrees around the X axis - _emptyUIView.Transform = CGAffineTransform.Scale(_emptyUIView.Transform, -1, 1); - } - void ShowEmptyView() { if (_emptyViewDisplayed || _emptyUIView == null) diff --git a/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs b/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs index a92f32bc467e..15ff6c5ea258 100644 --- a/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs +++ b/src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs @@ -306,6 +306,13 @@ public virtual void UpdateFlowDirection() itemsView.UpdateFlowDirection(ItemsView); foreach (var child in ItemsView.LogicalChildrenInternal) { + // Skip the empty view element — its flow direction is handled + // separately in AlignEmptyView to avoid double application + if (child == _emptyViewFormsElement) + { + continue; + } + if (child is VisualElement ve && ve.Handler?.PlatformView is UIView view) { view.UpdateFlowDirection(ve); @@ -320,7 +327,7 @@ public virtual void UpdateFlowDirection() cell.Label.UpdateFlowDirection(ItemsView); } } - + CollectionView.UpdateFlowDirection(ItemsView); } @@ -529,37 +536,30 @@ void AlignEmptyView() return; } - bool isRtl; - - if (OperatingSystem.IsIOSVersionAtLeast(10) || OperatingSystem.IsTvOSVersionAtLeast(10)) - isRtl = CollectionView.EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft; - else - isRtl = CollectionView.SemanticContentAttribute == UISemanticContentAttribute.ForceRightToLeft; - - if (isRtl) + if (_emptyViewFormsElement is not null) { - if (_emptyUIView.Transform.A == -1) + // The empty view's FlowDirection is handled here instead of in UpdateFlowDirection() + // to ensure proper alignment independent of the CollectionView's layout flip behavior. + if (_emptyViewFormsElement.Handler?.PlatformView is UIView emptyView) { - return; + emptyView.UpdateFlowDirection(_emptyViewFormsElement); } - - FlipEmptyView(); } - else + else if (_emptyUIView is UILabel label) { - if (_emptyUIView.Transform.A == -1) + // For UILabel, set the text alignment to center to ensure consistent behavior with Windows and Android + label.TextAlignment = UITextAlignment.Center; + label.SemanticContentAttribute = ItemsView.FlowDirection switch { - FlipEmptyView(); - } + FlowDirection.RightToLeft => UISemanticContentAttribute.ForceRightToLeft, + FlowDirection.LeftToRight => UISemanticContentAttribute.ForceLeftToRight, + _ => CollectionView.EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft + ? UISemanticContentAttribute.ForceRightToLeft + : UISemanticContentAttribute.ForceLeftToRight + }; } } - void FlipEmptyView() - { - // Flip the empty view 180 degrees around the X axis - _emptyUIView.Transform = CGAffineTransform.Scale(_emptyUIView.Transform, -1, 1); - } - void ShowEmptyView() { if (_emptyViewDisplayed || _emptyUIView == null) @@ -568,7 +568,25 @@ void ShowEmptyView() } _emptyUIView.Tag = EmptyTag; - CollectionView.AddSubview(_emptyUIView); + + // Add the empty view to the CollectionView's superview instead of the CollectionView itself. + // The compositional layout's flipsHorizontallyInOppositeLayoutDirection (default true) causes + // the CollectionView to flip its content coordinate system when SemanticContentAttribute is + // ForceRightToLeft. Layout-managed views (cells, supplementary views) are compensated by the + // layout, but direct subviews are NOT — resulting in mirror-flipped rendering. + // Adding to the superview avoids this flip zone entirely. + var targetView = CollectionView.Superview; + if (targetView is not null) + { + targetView.InsertSubviewAbove(_emptyUIView, CollectionView); + } + else + { + // TODO: DetermineEmptyViewFrame() returns superview-coordinate-space values (CollectionView.Frame.X/Y), + // which are incorrect when the empty view is a child of CollectionView. This fallback is unlikely + // to execute in practice since Superview is expected to be non-null by the time ShowEmptyView() is called. + CollectionView.AddSubview(_emptyUIView); + } if (((IElementController)ItemsView).LogicalChildren.IndexOf(_emptyViewFormsElement) == -1) { diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png new file mode 100644 index 000000000000..9915200a0c26 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png new file mode 100644 index 000000000000..3b3531630ead Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue32404.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue32404.cs new file mode 100644 index 000000000000..049d11ee8692 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue32404.cs @@ -0,0 +1,120 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 32404, "[Android, iOS, MacOS] FlowDirection not working on EmptyView in CollectionView", PlatformAffected.iOS | PlatformAffected.macOS)] +public class Issue32404 : ContentPage +{ + Label flowDirectionLabel; + CollectionView emptyViewStringCollectionView; + CollectionView emptyViewViewCollectionView; + CollectionView emptyViewTemplateCollectionView; + + public Issue32404() + { + var grid = new Grid + { + Padding = new Thickness(10), + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Star }, + new RowDefinition { Height = GridLength.Star }, + new RowDefinition { Height = GridLength.Star } + } + }; + + var toggleButton = new Button + { + Text = "Toggle FlowDirection", + HorizontalOptions = LayoutOptions.Center, + Margin = new Thickness(0, 10), + AutomationId = "Issue32404ToggleButton" + }; + + toggleButton.Clicked += OnToggleFlowDirectionClicked; + grid.Add(toggleButton); + Grid.SetRow(toggleButton, 0); + + flowDirectionLabel = new Label + { + Text = "Current FlowDirection: LeftToRight", + HorizontalOptions = LayoutOptions.Center, + FontAttributes = FontAttributes.Bold + }; + grid.Add(flowDirectionLabel); + Grid.SetRow(flowDirectionLabel, 1); + + // String EmptyView + emptyViewStringCollectionView = new CollectionView + { + BackgroundColor = Colors.LightGray, + FlowDirection = FlowDirection.LeftToRight, + EmptyView = "EmptyView Text (String)", + AutomationId = "CollectionView1" + }; + + // View EmptyView + emptyViewViewCollectionView = new CollectionView + { + BackgroundColor = Colors.LightBlue, + FlowDirection = FlowDirection.LeftToRight, + AutomationId = "CollectionView2" + }; + + var emptyViewGrid = new Grid(); + var emptyViewLabel = new Label + { + Text = "EmptyView (Grid View)", + }; + emptyViewGrid.Add(emptyViewLabel); + emptyViewViewCollectionView.EmptyView = emptyViewGrid; + + // DataTemplate EmptyView + emptyViewTemplateCollectionView = new CollectionView + { + BackgroundColor = Colors.LightGreen, + FlowDirection = FlowDirection.LeftToRight, + AutomationId = "CollectionView3" + }; + + emptyViewTemplateCollectionView.EmptyViewTemplate = new DataTemplate(() => + { + var stackLayout = new VerticalStackLayout(); + var templateLabel = new Label + { + Text = "EmptyView Template", + }; + + stackLayout.Add(templateLabel); + return stackLayout; + }); + + grid.Add(emptyViewStringCollectionView); + Grid.SetRow(emptyViewStringCollectionView, 2); + grid.Add(emptyViewViewCollectionView); + Grid.SetRow(emptyViewViewCollectionView, 3); + grid.Add(emptyViewTemplateCollectionView); + Grid.SetRow(emptyViewTemplateCollectionView, 4); + // Set Grid as Content + Content = grid; + } + + void OnToggleFlowDirectionClicked(object sender, EventArgs e) + { + // Toggle between LeftToRight and RightToLeft + if (emptyViewStringCollectionView.FlowDirection == FlowDirection.LeftToRight) + { + emptyViewStringCollectionView.FlowDirection = FlowDirection.RightToLeft; + emptyViewViewCollectionView.FlowDirection = FlowDirection.RightToLeft; + emptyViewTemplateCollectionView.FlowDirection = FlowDirection.RightToLeft; + flowDirectionLabel.Text = "Current FlowDirection: RightToLeft"; + } + else + { + emptyViewStringCollectionView.FlowDirection = FlowDirection.LeftToRight; + emptyViewViewCollectionView.FlowDirection = FlowDirection.LeftToRight; + emptyViewTemplateCollectionView.FlowDirection = FlowDirection.LeftToRight; + flowDirectionLabel.Text = "Current FlowDirection: LeftToRight"; + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png new file mode 100644 index 000000000000..fc9dfd836db7 Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png differ diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png new file mode 100644 index 000000000000..cb948f93f17d Binary files /dev/null and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png differ diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32404.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32404.cs new file mode 100644 index 000000000000..e4e1babb4918 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32404.cs @@ -0,0 +1,26 @@ +#if TEST_FAILS_ON_WINDOWS // https://github.com/dotnet/maui/issues/18551 +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue32404 : _IssuesUITest +{ + public Issue32404(TestDevice testDevice) : base(testDevice) + { + } + public override string Issue => "[Android, iOS, MacOS] FlowDirection not working on EmptyView in CollectionView"; + + [Test] + [Category(UITestCategories.CollectionView)] + public void FlowDirectionShouldWorkOnEmptyView() + { + App.WaitForElement("Issue32404ToggleButton"); + App.Tap("Issue32404ToggleButton"); + VerifyScreenshot("FlowDirectionShouldWorkOnEmptyView_RightToLeft"); + App.Tap("Issue32404ToggleButton"); + VerifyScreenshot("FlowDirectionShouldWorkOnEmptyView_LeftToRight"); + } +} +#endif \ No newline at end of file diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png new file mode 100644 index 000000000000..7b3d7574e118 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png new file mode 100644 index 000000000000..5d788b89edac Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios-26/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png new file mode 100644 index 000000000000..0660f6821ba0 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/FlowDirectionShouldWorkOnEmptyView_LeftToRight.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png new file mode 100644 index 000000000000..01c0eadb85c4 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/FlowDirectionShouldWorkOnEmptyView_RightToLeft.png differ