-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[iOS/macOS] CollectionView: Fix FlowDirection not working on EmptyView #32674
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
Changes from all commits
c378f3d
7ddcc06
f89ad41
bbec226
19e88a8
27ea35b
380f4de
16f6ade
68ae4e6
62b7408
ffad002
08b0400
4821544
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -306,6 +306,13 @@ public virtual void UpdateFlowDirection() | |
| itemsView.UpdateFlowDirection(ItemsView); | ||
| foreach (var child in ItemsView.LogicalChildrenInternal) | ||
|
Dhivya-SF4094 marked this conversation as resolved.
|
||
| { | ||
| // 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; | ||
| } | ||
|
Dhivya-SF4094 marked this conversation as resolved.
|
||
|
|
||
| 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) | ||
|
Dhivya-SF4094 marked this conversation as resolved.
|
||
| { | ||
| 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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding |
||
| } | ||
| 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 | ||
|
Dhivya-SF4094 marked this conversation as resolved.
|
||
| // to execute in practice since Superview is expected to be non-null by the time ShowEmptyView() is called. | ||
| CollectionView.AddSubview(_emptyUIView); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The author's TODO acknowledges that |
||
| } | ||
|
|
||
| if (((IElementController)ItemsView).LogicalChildren.IndexOf(_emptyViewFormsElement) == -1) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)] | ||
|
Dhivya-SF4094 marked this conversation as resolved.
|
||
| 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"; | ||
| } | ||
| } | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. File missing trailing newline ( |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| #if TEST_FAILS_ON_WINDOWS // https://github.com/dotnet/maui/issues/18551 | ||
|
Dhivya-SF4094 marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No |
||
| 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() | ||
|
Dhivya-SF4094 marked this conversation as resolved.
|
||
| { | ||
| App.WaitForElement("Issue32404ToggleButton"); | ||
| App.Tap("Issue32404ToggleButton"); | ||
| VerifyScreenshot("FlowDirectionShouldWorkOnEmptyView_RightToLeft"); | ||
|
Dhivya-SF4094 marked this conversation as resolved.
|
||
| App.Tap("Issue32404ToggleButton"); | ||
| VerifyScreenshot("FlowDirectionShouldWorkOnEmptyView_LeftToRight"); | ||
|
Dhivya-SF4094 marked this conversation as resolved.
|
||
| } | ||
| } | ||
| #endif | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unconditionally setting
label.TextAlignment = UITextAlignment.Centeris a behavioral change for users who previously relied on the implicit (Natural / Left) alignment of an internal UILabel-rendered string EmptyView. The PR description mentions this was done "to provide a better user experience" and to align with Windows/Android, but it is technically a regression for any user comparing pixel-exact baselines. Consider only forcing center when no alignment was previously set, or document this in release notes.