From 36fdb8e534f3a43a07a8380d3acb34189e94412e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jan 2020 09:24:28 +0100 Subject: [PATCH] Add a MaximumRowsOrColumns to Uniformgridlayout * Add a MaximumRowsOrColumns property to UniformGridLayout * Fix nit * Fix bugs Ported from https://github.com/microsoft/microsoft-ui-xaml/commit/6bd03c98f770fed69312ed5efb6cc06b428fa758?w=1 --- src/Avalonia.Layout/FlowLayoutAlgorithm.cs | 14 +++--- src/Avalonia.Layout/StackLayout.cs | 1 + src/Avalonia.Layout/UniformGridLayout.cs | 49 +++++++++++++++---- src/Avalonia.Layout/UniformGridLayoutState.cs | 34 ++++++++++--- 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs index 615ce725bd..7343f4a6e9 100644 --- a/src/Avalonia.Layout/FlowLayoutAlgorithm.cs +++ b/src/Avalonia.Layout/FlowLayoutAlgorithm.cs @@ -72,6 +72,7 @@ namespace Avalonia.Layout bool isWrapping, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, ScrollOrientation orientation, string layoutId) { @@ -94,14 +95,14 @@ namespace Avalonia.Layout _elementManager.OnBeginMeasure(orientation); int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId); - Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); - Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); + Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); if (isWrapping && IsReflowRequired()) { var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); _orientation.SetMinorStart(ref firstElementBounds, 0); _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); - Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, layoutId); + Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId); } RaiseLineArranged(); @@ -270,6 +271,7 @@ namespace Avalonia.Layout Size availableSize, double minItemSpacing, double lineSpacing, + int maxItemsPerLine, string layoutId) { if (anchorIndex != -1) @@ -280,7 +282,7 @@ namespace Avalonia.Layout var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); var lineOffset = _orientation.MajorStart(anchorBounds); var lineMajorSize = _orientation.MajorSize(anchorBounds); - int countInLine = 1; + var countInLine = 1; int count = 0; bool lineNeedsReposition = false; @@ -301,7 +303,7 @@ namespace Avalonia.Layout if (direction == GenerateDirection.Forward) { double remainingSpace = _orientation.Minor(availableSize) - (_orientation.MinorStart(previousElementBounds) + _orientation.MinorSize(previousElementBounds) + minItemSpacing + _orientation.Minor(desiredSize)); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // No more space in this row. wrap to next row. _orientation.SetMinorStart(ref currentBounds, 0); @@ -339,7 +341,7 @@ namespace Avalonia.Layout { // Backward double remainingSpace = _orientation.MinorStart(previousElementBounds) - (_orientation.Minor(desiredSize) + minItemSpacing); - if (_algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) + if (countInLine >= maxItemsPerLine || _algorithmCallbacks.Algorithm_ShouldBreakLine(currentIndex, remainingSpace)) { // Does not fit, wrap to the previous row var availableSizeMinor = _orientation.Minor(availableSize); diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index e9735b9b31..a3fe80fab7 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -267,6 +267,7 @@ namespace Avalonia.Layout false, 0, Spacing, + int.MaxValue, _orientation.ScrollOrientation, LayoutId); diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index edc2042922..16d21508a9 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -110,6 +110,12 @@ namespace Avalonia.Layout public static readonly StyledProperty MinRowSpacingProperty = AvaloniaProperty.Register(nameof(MinRowSpacing)); + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumRowsOrColumnsProperty = + AvaloniaProperty.Register(nameof(MinItemWidth)); + /// /// Defines the property. /// @@ -123,6 +129,7 @@ namespace Avalonia.Layout private double _minColumnSpacing; private UniformGridLayoutItemsJustification _itemsJustification; private UniformGridLayoutItemsStretch _itemsStretch; + private int _maximumRowsOrColumns = int.MaxValue; /// /// Initializes a new instance of the class. @@ -219,6 +226,15 @@ namespace Avalonia.Layout set => SetValue(MinRowSpacingProperty, value); } + /// + /// Gets or sets the maximum row or column count. + /// + public int MaximumRowsOrColumns + { + get => GetValue(MaximumRowsOrColumnsProperty); + set => SetValue(MaximumRowsOrColumnsProperty, value); + } + /// /// Gets or sets the axis along which items are laid out. /// @@ -269,15 +285,17 @@ namespace Avalonia.Layout { var gridState = (UniformGridLayoutState)context.LayoutState; var lastExtent = gridState.FlowAlgorithm.LastExtent; - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); - double majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); - double realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); + var itemsPerLine = Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, (uint)_maximumRowsOrColumns)); + var majorSize = (itemsCount / itemsPerLine) * GetMajorSizeWithSpacing(context); + var realizationWindowStartWithinExtent = _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent); if ((realizationWindowStartWithinExtent + _orientation.MajorSize(realizationRect)) >= 0 && realizationWindowStartWithinExtent <= majorSize) { double offset = Math.Max(0.0, _orientation.MajorStart(realizationRect) - _orientation.MajorStart(lastExtent)); int anchorRowIndex = (int)(offset / GetMajorSizeWithSpacing(context)); - anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); + anchorIndex = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine)); bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context); } } @@ -299,7 +317,9 @@ namespace Avalonia.Layout int count = context.ItemCount; if (targetIndex >= 0 && targetIndex < count) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( // note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine; index = indexOfFirstInLine; var state = context.LayoutState as UniformGridLayoutState; @@ -329,8 +349,12 @@ namespace Avalonia.Layout // Constants int itemsCount = context.ItemCount; double availableSizeMinor = _orientation.Minor(availableSize); - int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ? - (int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount); + int itemsPerLine = + (int)Math.Min( // note use of unsigned ints + Math.Max(1u, !double.IsInfinity(availableSizeMinor) + ? (uint)(availableSizeMinor / GetMinorSizeWithSpacing(context)) + : (uint)itemsCount), + Math.Max(1u, _maximumRowsOrColumns)); double lineSize = GetMajorSizeWithSpacing(context); if (itemsCount > 0) @@ -398,7 +422,7 @@ namespace Avalonia.Layout // Set the width and height on the grid state. If the user already set them then use the preset. // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items. var gridState = (UniformGridLayoutState)context.LayoutState; - gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing); + gridState.EnsureElementSize(availableSize, context, _minItemWidth, _minItemHeight, _itemsStretch, Orientation, MinRowSpacing, MinColumnSpacing, _maximumRowsOrColumns); var desiredSize = GetFlowAlgorithm(context).Measure( availableSize, @@ -406,6 +430,7 @@ namespace Avalonia.Layout true, MinItemSpacing, LineSpacing, + _maximumRowsOrColumns, _orientation.ScrollOrientation, LayoutId); @@ -471,6 +496,10 @@ namespace Avalonia.Layout { _minItemHeight = (double)args.NewValue; } + else if (args.Property == MaximumRowsOrColumnsProperty) + { + _maximumRowsOrColumns = (int)args.NewValue; + } InvalidateLayout(); } @@ -499,7 +528,9 @@ namespace Avalonia.Layout Rect lastExtent, VirtualizingLayoutContext context) { - int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))); + int itemsPerLine = (int)Math.Min( //note use of unsigned ints + Math.Max(1u, (uint)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context))), + Math.Max(1u, _maximumRowsOrColumns)); int rowIndex = (int)(index / itemsPerLine); int indexInRow = index - (rowIndex * itemsPerLine); diff --git a/src/Avalonia.Layout/UniformGridLayoutState.cs b/src/Avalonia.Layout/UniformGridLayoutState.cs index e6d75bcf35..62c5174775 100644 --- a/src/Avalonia.Layout/UniformGridLayoutState.cs +++ b/src/Avalonia.Layout/UniformGridLayoutState.cs @@ -48,8 +48,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + if (context.ItemCount > 0) { // If the first element is realized we don't need to cache it or to get it from the context @@ -57,7 +63,7 @@ namespace Avalonia.Layout if (realizedElement != null) { realizedElement.Measure(availableSize); - SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); _cachedFirstElement = null; } else @@ -72,7 +78,7 @@ namespace Avalonia.Layout _cachedFirstElement.Measure(availableSize); - SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing); + SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine); // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache. bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement); @@ -92,8 +98,14 @@ namespace Avalonia.Layout UniformGridLayoutItemsStretch stretch, Orientation orientation, double minRowSpacing, - double minColumnSpacing) + double minColumnSpacing, + int maxItemsPerLine) { + if (maxItemsPerLine == 0) + { + maxItemsPerLine = 1; + } + EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth); EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight); @@ -101,11 +113,17 @@ namespace Avalonia.Layout var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing; var itemSizeMinor = orientation == Orientation.Horizontal ? EffectiveItemWidth : EffectiveItemHeight; - itemSizeMinor += minorItemSpacing; - var numItemsPerColumn = (int)(Math.Max(1.0, availableSizeMinor / itemSizeMinor)); - var remainingSpace = ((int)availableSizeMinor) % ((int)itemSizeMinor); - var extraMinorPixelsForEachItem = remainingSpace / numItemsPerColumn; + double extraMinorPixelsForEachItem = 0.0; + if (!double.IsInfinity(availableSizeMinor)) + { + var numItemsPerColumn = Math.Min( + maxItemsPerLine, + Math.Max(1.0, availableSizeMinor / (itemSizeMinor + minorItemSpacing))); + var usedSpace = (numItemsPerColumn * (itemSizeMinor + minorItemSpacing)) - minorItemSpacing; + var remainingSpace = ((int)(availableSizeMinor - usedSpace)); + extraMinorPixelsForEachItem = remainingSpace / ((int)numItemsPerColumn); + } if (stretch == UniformGridLayoutItemsStretch.Fill) {