diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs index 363e82a56ecc..add6957a0970 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs @@ -15,7 +15,7 @@ namespace Microsoft.Maui.Controls.Handlers.Items { - public class MauiRecyclerView : RecyclerView, IMauiRecyclerView + public class MauiRecyclerView : RecyclerView, IMauiRecyclerView, IMauiRecyclerView where TItemsView : ItemsView where TAdapter : ItemsViewAdapter where TItemsViewSource : IItemsViewSource @@ -84,11 +84,8 @@ public virtual void TearDownOldElement(TItemsView oldElement) ItemsViewAdapter?.Dispose(); } - if (_snapManager != null) - { - _snapManager.Dispose(); - _snapManager = null; - } + _snapManager?.Dispose(); + _snapManager = null; if (_itemDecoration != null) { @@ -102,11 +99,8 @@ public virtual void TearDownOldElement(TItemsView oldElement) _itemTouchHelper = null; } - if (_itemTouchHelperCallback != null) - { - _itemTouchHelperCallback.Dispose(); - _itemTouchHelperCallback = null; - } + _itemTouchHelperCallback?.Dispose(); + _itemTouchHelperCallback = null; } public virtual void SetUpNewElement(TItemsView newElement) @@ -276,22 +270,26 @@ public virtual void UpdateCanReorderItems() _itemTouchHelper.Dispose(); _itemTouchHelper = null; } - if (_itemTouchHelperCallback != null) - { - _itemTouchHelperCallback.Dispose(); - _itemTouchHelperCallback = null; - } + + _itemTouchHelperCallback?.Dispose(); + _itemTouchHelperCallback = null; } } public virtual void UpdateLayoutManager() { - _layoutPropertyChangedProxy?.Unsubscribe(); + var itemsLayout = _getItemsLayout(); + + if (itemsLayout == ItemsLayout) + { + return; + } - ItemsLayout = _getItemsLayout(); + _layoutPropertyChangedProxy?.Unsubscribe(); + ItemsLayout = itemsLayout; // Keep track of the ItemsLayout's property changes - if (ItemsLayout != null) + if (ItemsLayout is not null) { _layoutPropertyChanged ??= LayoutPropertyChanged; _layoutPropertyChangedProxy = new WeakNotifyPropertyChangedProxy(ItemsLayout, _layoutPropertyChanged); @@ -427,9 +425,9 @@ protected virtual int DetermineTargetPosition(ScrollToRequestEventArgs args) if (args.Mode == ScrollToMode.Position) { // Do not use `IGroupableItemsViewSource` since `UngroupedItemsSource` also implements that interface - if (ItemsViewAdapter.ItemsSource is UngroupedItemsSource) + if (ItemsViewAdapter.ItemsSource is UngroupedItemsSource ungroupedSource) { - return args.Index; + return ungroupedSource.HasHeader ? args.Index + 1 : args.Index; } else if (ItemsViewAdapter.ItemsSource is IGroupableItemsViewSource groupItemSource) { @@ -473,12 +471,26 @@ protected virtual void UpdateItemSpacing() if (_itemDecoration is SpacingItemDecoration spacingDecoration) { - // SpacingItemDecoration applies spacing to all items & all 4 sides of the items. - // We need to adjust the padding on the RecyclerView so this spacing isn't visible around the outer edge of our control. - // Horizontal & vertical spacing should only exist between items. - var horizontalPadding = -spacingDecoration.HorizontalOffset; - var verticalPadding = -spacingDecoration.VerticalOffset; - SetPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); + // SpacingItemDecoration now removes spacing on outer edges (first/last row or column), + // so we only need negative padding on the cross-axis for grid layouts to compensate + // for the spacing between columns (vertical grid) or rows (horizontal grid). + if (ItemsLayout is GridItemsLayout gridItemsLayout) + { + if (gridItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal) + { + var verticalPadding = -spacingDecoration.VerticalOffset; + SetPadding(0, verticalPadding, 0, verticalPadding); + } + else + { + var horizontalPadding = -spacingDecoration.HorizontalOffset; + SetPadding(horizontalPadding, 0, horizontalPadding, 0); + } + } + else + { + SetPadding(0, 0, 0, 0); + } } } @@ -513,6 +525,7 @@ protected virtual void LayoutPropertyChanged(object sender, PropertyChangedEvent if (GetLayoutManager() is GridLayoutManager gridLayoutManager) { gridLayoutManager.SpanCount = ((GridItemsLayout)ItemsLayout).Span; + UpdateItemSpacing(); } } else if (propertyChanged.IsOneOf(Microsoft.Maui.Controls.ItemsLayout.SnapPointsTypeProperty, Microsoft.Maui.Controls.ItemsLayout.SnapPointsAlignmentProperty)) @@ -585,6 +598,34 @@ internal void UpdateEmptyViewVisibility() SwapAdapter(ItemsViewAdapter, true); UpdateLayoutManager(); } + else if (showEmptyView && currentAdapter == _emptyViewAdapter) + { + if (ShouldUpdateEmptyView()) + { + // Header/footer properties changed - detach and reattach adapter to force RecyclerView to recalculate the positions. + SetAdapter(null); + SwapAdapter(_emptyViewAdapter, true); + UpdateEmptyView(); + } + } + } + + bool ShouldUpdateEmptyView() + { + if (ItemsView is StructuredItemsView structuredItemsView) + { + if (_emptyViewAdapter.Header != structuredItemsView.Header || + _emptyViewAdapter.HeaderTemplate != structuredItemsView.HeaderTemplate || + _emptyViewAdapter.Footer != structuredItemsView.Footer || + _emptyViewAdapter.FooterTemplate != structuredItemsView.FooterTemplate || + _emptyViewAdapter.EmptyView != ItemsView.EmptyView || + _emptyViewAdapter.EmptyViewTemplate != ItemsView.EmptyViewTemplate) + { + return true; + } + } + + return false; } internal void AdjustScrollForItemUpdate() diff --git a/src/Controls/src/Core/Handlers/Items/Android/SpacingItemDecoration.cs b/src/Controls/src/Core/Handlers/Items/Android/SpacingItemDecoration.cs index c0545e9c368a..ca2525e79658 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/SpacingItemDecoration.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/SpacingItemDecoration.cs @@ -13,6 +13,10 @@ public class SpacingItemDecoration : RecyclerView.ItemDecoration public int VerticalOffset { get; } + int _span = 1; + + ItemsLayoutOrientation _orientation; + public SpacingItemDecoration(Context context, IItemsLayout itemsLayout) { // The original "SpacingItemDecoration" applied spacing based on an item's current span index. @@ -35,6 +39,8 @@ public SpacingItemDecoration(Context context, IItemsLayout itemsLayout) case GridItemsLayout gridItemsLayout: horizontalOffset = gridItemsLayout.HorizontalItemSpacing / 2.0; verticalOffset = gridItemsLayout.VerticalItemSpacing / 2.0; + _span = gridItemsLayout.Span; + _orientation = gridItemsLayout.Orientation; break; case LinearItemsLayout listItemsLayout: if (listItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal) @@ -47,10 +53,12 @@ public SpacingItemDecoration(Context context, IItemsLayout itemsLayout) horizontalOffset = 0; verticalOffset = listItemsLayout.ItemSpacing / 2.0; } + _orientation = listItemsLayout.Orientation; break; default: horizontalOffset = 0; verticalOffset = 0; + _orientation = ItemsLayoutOrientation.Vertical; break; } @@ -62,10 +70,39 @@ public override void GetItemOffsets(ARect outRect, AView view, RecyclerView pare { base.GetItemOffsets(outRect, view, parent, state); + int position = parent.GetChildAdapterPosition(view); + if (position == RecyclerView.NoPosition) + return; + + int itemCount = state.ItemCount; + if (itemCount <= 0) + return; + outRect.Left = HorizontalOffset; outRect.Right = HorizontalOffset; outRect.Bottom = VerticalOffset; outRect.Top = VerticalOffset; + + // Remove spacing on the outer edges so spacing only appears between items. + // A linear layout is effectively span=1, so the same math works for both. + int rowCol = _span <= 1 ? position : position / _span; + int totalRowsCols = _span <= 1 ? itemCount : (itemCount + _span - 1) / _span; + int lastRowCol = totalRowsCols - 1; + + if (_orientation == ItemsLayoutOrientation.Vertical) + { + if (rowCol == 0) + outRect.Top = 0; + if (rowCol == lastRowCol) + outRect.Bottom = 0; + } + else + { + if (rowCol == 0) + outRect.Left = 0; + if (rowCol == lastRowCol) + outRect.Right = 0; + } } } } \ No newline at end of file