diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Android.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Android.cs index 189b4ea19fdd..8542a9dbf5d9 100644 --- a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Android.cs +++ b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Android.cs @@ -109,8 +109,37 @@ void UpdateEmptyViewSize(double width, double height) if (adapter is EmptyViewAdapter emptyViewAdapter) { + double tolerance = 0.001; + var widthChanged = Math.Abs(emptyViewAdapter.RecyclerViewWidth - width) > tolerance; + var heightChanged = Math.Abs(emptyViewAdapter.RecyclerViewHeight - height) > tolerance; + emptyViewAdapter.RecyclerViewWidth = width; emptyViewAdapter.RecyclerViewHeight = height; + + if (widthChanged || heightChanged) + { + // EmptyView position depends on whether the CollectionView has a header + var structuredItemsView = VirtualView as StructuredItemsView; + var hasHeader = (structuredItemsView?.Header ?? structuredItemsView?.HeaderTemplate) is not null; + var emptyViewPosition = hasHeader ? 1 : 0; + + // Check if ViewHolder exists and is ready for immediate layout request + var viewHolder = PlatformView.FindViewHolderForAdapterPosition(emptyViewPosition); + if (viewHolder is not null) + { + // ViewHolder exists, request layout immediately to avoid frame delay + viewHolder.ItemView.RequestLayout(); + } + else + { + // ViewHolder not created yet, defer layout request to next UI loop iteration + PlatformView.Post(() => + { + var vh = PlatformView.FindViewHolderForAdapterPosition(emptyViewPosition); + vh?.ItemView.RequestLayout(); + }); + } + } } } } diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/EmptyViewShouldRemeasureWhenParentLayoutChanges.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/EmptyViewShouldRemeasureWhenParentLayoutChanges.png new file mode 100644 index 000000000000..d6943cc480d0 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/EmptyViewShouldRemeasureWhenParentLayoutChanges.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue33324.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue33324.cs new file mode 100644 index 000000000000..a4c8b153f354 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue33324.cs @@ -0,0 +1,171 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 33324, "CollectionView.EmptyView does not remeasure its height when the parent layout changes dynamically", PlatformAffected.Android)] +public class Issue33324 : ContentPage +{ + readonly Issue33324ViewModel _viewModel; + + public Issue33324() + { + Title = "Issue 33324 - EmptyView Height"; + + _viewModel = new Issue33324ViewModel(); + BindingContext = _viewModel; + + var firstCollectionView = new CollectionView2 + { + AutomationId = "FirstCollectionView", + HeightRequest = 380, + ItemTemplate = new DataTemplate(() => + { + var label = new Label { Margin = new Thickness(10, 5) }; + label.SetBinding(Label.TextProperty, "."); + return label; + }) + }; + firstCollectionView.SetBinding(CollectionView.ItemsSourceProperty, nameof(Issue33324ViewModel.MyItems)); + + var itemsContainer = new VerticalStackLayout(); + itemsContainer.SetBinding(VisualElement.IsVisibleProperty, nameof(Issue33324ViewModel.HasItems)); + itemsContainer.Children.Add(new Label + { + Text = "Items Loaded:", + FontAttributes = FontAttributes.Bold, + Margin = new Thickness(10, 0, 0, 0) + }); + itemsContainer.Children.Add(firstCollectionView); + + var loadButton = new Button + { + Text = "Load Items", + AutomationId = "LoadItemsButton", + Margin = new Thickness(10) + }; + loadButton.SetBinding(Button.CommandProperty, nameof(Issue33324ViewModel.LoadItemsCommand)); + + var topContainer = new VerticalStackLayout(); + topContainer.Children.Add(itemsContainer); + topContainer.Children.Add(loadButton); + + var emptyViewLabel = new Label + { + Text = "No players available", + AutomationId = "EmptyViewLabel", + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + FontSize = 18, + TextColor = Colors.Black + }; + + var emptyViewGrid = new Grid + { + AutomationId = "EmptyViewGrid" + }; + emptyViewGrid.Children.Add(new BoxView + { + Color = Colors.Pink, + AutomationId = "EmptyViewBackground" + }); + emptyViewGrid.Children.Add(emptyViewLabel); + + var secondCollectionView = new CollectionView2 + { + AutomationId = "SecondCollectionView", + EmptyView = emptyViewGrid + }; + secondCollectionView.SetBinding(CollectionView.ItemsSourceProperty, nameof(Issue33324ViewModel.Players)); + + var mainGrid = new Grid + { + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Star } + }, + RowSpacing = 20 + }; + + mainGrid.Add(topContainer, 0, 0); + mainGrid.Add(secondCollectionView, 0, 1); + + Content = mainGrid; + } +} + +public class Issue33324ViewModel : INotifyPropertyChanged +{ + bool _hasItems; + ObservableCollection _myItems; + ObservableCollection _players; + + public Issue33324ViewModel() + { + _myItems = new ObservableCollection(); + _players = new ObservableCollection(); + _hasItems = false; + + LoadItemsCommand = new Command(LoadItems); + } + + public bool HasItems + { + get => _hasItems; + set + { + if (_hasItems != value) + { + _hasItems = value; + OnPropertyChanged(); + } + } + } + + public ObservableCollection MyItems + { + get => _myItems; + set + { + if (_myItems != value) + { + _myItems = value; + OnPropertyChanged(); + } + } + } + + public ObservableCollection Players + { + get => _players; + set + { + if (_players != value) + { + _players = value; + OnPropertyChanged(); + } + } + } + + public Command LoadItemsCommand { get; } + + void LoadItems() + { + MyItems.Clear(); + for (int i = 1; i <= 10; i++) + { + MyItems.Add($"Item {i}"); + } + HasItems = true; + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33324.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33324.cs new file mode 100644 index 000000000000..4eb971f321dc --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33324.cs @@ -0,0 +1,22 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue33324 : _IssuesUITest +{ + public override string Issue => "CollectionView.EmptyView does not remeasure its height when the parent layout changes dynamically"; + + public Issue33324(TestDevice device) : base(device) { } + + [Test] + [Category(UITestCategories.CollectionView)] + public void EmptyViewShouldRemeasureWhenParentLayoutChanges() + { + App.WaitForElement("LoadItemsButton"); + App.Tap("LoadItemsButton"); + App.WaitForElement("SecondCollectionView"); + VerifyScreenshot(); + } +} diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/EmptyViewShouldRemeasureWhenParentLayoutChanges.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/EmptyViewShouldRemeasureWhenParentLayoutChanges.png new file mode 100644 index 000000000000..728c4f6c61de Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/EmptyViewShouldRemeasureWhenParentLayoutChanges.png differ