A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

561 lines
24 KiB

// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections.Specialized;
using Avalonia.Data;
using Avalonia.Logging;
namespace Avalonia.Layout
{
/// <summary>
/// Defines constants that specify how items are aligned on the non-scrolling or non-virtualizing axis.
/// </summary>
public enum UniformGridLayoutItemsJustification
{
/// <summary>
/// Items are aligned with the start of the row or column, with extra space at the end.
/// Spacing between items does not change.
/// </summary>
Start = 0,
/// <summary>
/// Items are aligned in the center of the row or column, with extra space at the start and
/// end. Spacing between items does not change.
/// </summary>
Center = 1,
/// <summary>
/// Items are aligned with the end of the row or column, with extra space at the start.
/// Spacing between items does not change.
/// </summary>
End = 2,
/// <summary>
/// Items are aligned so that extra space is added evenly before and after each item.
/// </summary>
SpaceAround = 3,
/// <summary>
/// Items are aligned so that extra space is added evenly between adjacent items. No space
/// is added at the start or end.
/// </summary>
SpaceBetween = 4,
SpaceEvenly = 5,
};
/// <summary>
/// Defines constants that specify how items are sized to fill the available space.
/// </summary>
public enum UniformGridLayoutItemsStretch
{
/// <summary>
/// The item retains its natural size. Use of extra space is determined by the
/// <see cref="UniformGridLayout.ItemsJustification"/> property.
/// </summary>
None = 0,
/// <summary>
/// The item is sized to fill the available space in the non-scrolling direction. Item size
/// in the scrolling direction is not changed.
/// </summary>
Fill = 1,
/// <summary>
/// The item is sized to both fill the available space in the non-scrolling direction and
/// maintain its aspect ratio.
/// </summary>
Uniform = 2,
};
/// <summary>
/// Positions elements sequentially from left to right or top to bottom in a wrapping layout.
/// </summary>
public class UniformGridLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
{
/// <summary>
/// Defines the <see cref="ItemsJustification"/> property.
/// </summary>
public static readonly StyledProperty<UniformGridLayoutItemsJustification> ItemsJustificationProperty =
AvaloniaProperty.Register<UniformGridLayout, UniformGridLayoutItemsJustification>(nameof(ItemsJustification));
/// <summary>
/// Defines the <see cref="ItemsStretch"/> property.
/// </summary>
public static readonly StyledProperty<UniformGridLayoutItemsStretch> ItemsStretchProperty =
AvaloniaProperty.Register<UniformGridLayout, UniformGridLayoutItemsStretch>(nameof(ItemsStretch));
/// <summary>
/// Defines the <see cref="MinColumnSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> MinColumnSpacingProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinColumnSpacing));
/// <summary>
/// Defines the <see cref="MinItemHeight"/> property.
/// </summary>
public static readonly StyledProperty<double> MinItemHeightProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinItemHeight));
/// <summary>
/// Defines the <see cref="MinItemWidth"/> property.
/// </summary>
public static readonly StyledProperty<double> MinItemWidthProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinItemWidth));
/// <summary>
/// Defines the <see cref="MinRowSpacing"/> property.
/// </summary>
public static readonly StyledProperty<double> MinRowSpacingProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinRowSpacing));
/// <summary>
/// Defines the <see cref="MaximumRowsOrColumnsProperty"/> property.
/// </summary>
public static readonly StyledProperty<int> MaximumRowsOrColumnsProperty =
AvaloniaProperty.Register<UniformGridLayout, int>(nameof(MinItemWidth));
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly StyledProperty<Orientation> OrientationProperty =
StackLayout.OrientationProperty.AddOwner<UniformGridLayout>();
private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
private double _minItemWidth = double.NaN;
private double _minItemHeight = double.NaN;
private double _minRowSpacing;
private double _minColumnSpacing;
private UniformGridLayoutItemsJustification _itemsJustification;
private UniformGridLayoutItemsStretch _itemsStretch;
private int _maximumRowsOrColumns = int.MaxValue;
/// <summary>
/// Initializes a new instance of the <see cref="UniformGridLayout"/> class.
/// </summary>
public UniformGridLayout()
{
LayoutId = "UniformGridLayout";
}
static UniformGridLayout()
{
OrientationProperty.OverrideDefaultValue<UniformGridLayout>(Orientation.Horizontal);
}
/// <summary>
/// Gets or sets a value that indicates how items are aligned on the non-scrolling or non-
/// virtualizing axis.
/// </summary>
/// <value>
/// An enumeration value that indicates how items are aligned. The default is Start.
/// </value>
public UniformGridLayoutItemsJustification ItemsJustification
{
get => GetValue(ItemsJustificationProperty);
set => SetValue(ItemsJustificationProperty, value);
}
/// <summary>
/// Gets or sets a value that indicates how items are sized to fill the available space.
/// </summary>
/// <value>
/// An enumeration value that indicates how items are sized to fill the available space.
/// The default is None.
/// </value>
/// <remarks>
/// This property enables adaptive layout behavior where the items are sized to fill the
/// available space along the non-scrolling axis, and optionally maintain their aspect ratio.
/// </remarks>
public UniformGridLayoutItemsStretch ItemsStretch
{
get => GetValue(ItemsStretchProperty);
set => SetValue(ItemsStretchProperty, value);
}
/// <summary>
/// Gets or sets the minimum space between items on the horizontal axis.
/// </summary>
/// <remarks>
/// The spacing may exceed this minimum value when <see cref="ItemsJustification"/> is set
/// to SpaceEvenly, SpaceAround, or SpaceBetween.
/// </remarks>
public double MinColumnSpacing
{
get => GetValue(MinColumnSpacingProperty);
set => SetValue(MinColumnSpacingProperty, value);
}
/// <summary>
/// Gets or sets the minimum height of each item.
/// </summary>
/// <value>
/// The minimum height (in pixels) of each item. The default is NaN, in which case the
/// height of the first item is used as the minimum.
/// </value>
public double MinItemHeight
{
get => GetValue(MinItemHeightProperty);
set => SetValue(MinItemHeightProperty, value);
}
/// <summary>
/// Gets or sets the minimum width of each item.
/// </summary>
/// <value>
/// The minimum width (in pixels) of each item. The default is NaN, in which case the width
/// of the first item is used as the minimum.
/// </value>
public double MinItemWidth
{
get => GetValue(MinItemWidthProperty);
set => SetValue(MinItemWidthProperty, value);
}
/// <summary>
/// Gets or sets the minimum space between items on the vertical axis.
/// </summary>
/// <remarks>
/// The spacing may exceed this minimum value when <see cref="ItemsJustification"/> is set
/// to SpaceEvenly, SpaceAround, or SpaceBetween.
/// </remarks>
public double MinRowSpacing
{
get => GetValue(MinRowSpacingProperty);
set => SetValue(MinRowSpacingProperty, value);
}
/// <summary>
/// Gets or sets the maximum row or column count.
/// </summary>
public int MaximumRowsOrColumns
{
get => GetValue(MaximumRowsOrColumnsProperty);
set => SetValue(MaximumRowsOrColumnsProperty, value);
}
/// <summary>
/// Gets or sets the axis along which items are laid out.
/// </summary>
/// <value>
/// One of the enumeration values that specifies the axis along which items are laid out.
/// The default is Vertical.
/// </value>
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
internal double LineSpacing => Orientation == Orientation.Horizontal ? _minRowSpacing : _minColumnSpacing;
internal double MinItemSpacing => Orientation == Orientation.Horizontal ? _minColumnSpacing : _minRowSpacing;
Size IFlowLayoutAlgorithmDelegates.Algorithm_GetMeasureSize(
int index,
Size availableSize,
VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState!;
return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight);
}
Size IFlowLayoutAlgorithmDelegates.Algorithm_GetProvisionalArrangeSize(
int index,
Size measureSize,
Size desiredSize,
VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState!;
return new Size(gridState.EffectiveItemWidth, gridState.EffectiveItemHeight);
}
bool IFlowLayoutAlgorithmDelegates.Algorithm_ShouldBreakLine(int index, double remainingSpace) => remainingSpace < 0;
FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForRealizationRect(
Size availableSize,
VirtualizingLayoutContext context)
{
Rect bounds = new Rect(double.NaN, double.NaN, double.NaN, double.NaN);
int anchorIndex = -1;
int itemsCount = context.ItemCount;
var realizationRect = context.RealizationRect;
if (itemsCount > 0 && _orientation.MajorSize(realizationRect) > 0)
{
var gridState = (UniformGridLayoutState)context.LayoutState!;
var lastExtent = gridState.FlowAlgorithm.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 = (int)Math.Max(0, Math.Min(itemsCount - 1, anchorRowIndex * itemsPerLine));
bounds = GetLayoutRectForDataIndex(availableSize, anchorIndex, lastExtent, context);
}
}
return new FlowLayoutAnchorInfo
{
Index = anchorIndex,
Offset = _orientation.MajorStart(bounds)
};
}
FlowLayoutAnchorInfo IFlowLayoutAlgorithmDelegates.Algorithm_GetAnchorForTargetElement(
int targetIndex,
Size availableSize,
VirtualizingLayoutContext context)
{
int index = -1;
double offset = double.NaN;
int count = context.ItemCount;
if (targetIndex >= 0 && targetIndex < count)
{
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 = (UniformGridLayoutState)context.LayoutState!;
offset = _orientation.MajorStart(GetLayoutRectForDataIndex(availableSize, indexOfFirstInLine, state.FlowAlgorithm.LastExtent, context));
}
return new FlowLayoutAnchorInfo
{
Index = index,
Offset = offset
};
}
Rect IFlowLayoutAlgorithmDelegates.Algorithm_GetExtent(
Size availableSize,
VirtualizingLayoutContext context,
ILayoutable? firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
ILayoutable? lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds)
{
var extent = new Rect();
// Constants
int itemsCount = context.ItemCount;
double availableSizeMinor = _orientation.Minor(availableSize);
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)
{
_orientation.SetMinorSize(
ref extent,
!double.IsInfinity(availableSizeMinor) && _itemsStretch == UniformGridLayoutItemsStretch.Fill ?
availableSizeMinor :
Math.Max(0.0, itemsPerLine * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing));
_orientation.SetMajorSize(
ref extent,
Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing));
if (firstRealized != null)
{
_orientation.SetMajorStart(
ref extent,
_orientation.MajorStart(firstRealizedLayoutBounds) - (firstRealizedItemIndex / itemsPerLine) * lineSize);
int remainingItems = itemsCount - lastRealizedItemIndex - 1;
_orientation.SetMajorSize(
ref extent,
_orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize);
}
else
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Estimating extent with no realized elements", LayoutId);
}
}
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Extent is ({Size}). Based on lineSize {LineSize} and items per line {ItemsPerLine}",
LayoutId, extent.Size, lineSize, itemsPerLine);
return extent;
}
void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(ILayoutable element, int index, Size availableSize, Size measureSize, Size desiredSize, Size provisionalArrangeSize, VirtualizingLayoutContext context)
{
}
void IFlowLayoutAlgorithmDelegates.Algorithm_OnLineArranged(int startIndex, int countInLine, double lineSize, VirtualizingLayoutContext context)
{
}
protected internal override void InitializeForContextCore(VirtualizingLayoutContext context)
{
var state = context.LayoutState;
var gridState = state as UniformGridLayoutState;
if (gridState == null)
{
if (state != null)
{
throw new InvalidOperationException("LayoutState must derive from UniformGridLayoutState.");
}
// Custom deriving layouts could potentially be stateful.
// If that is the case, we will just create the base state required by UniformGridLayout ourselves.
gridState = new UniformGridLayoutState();
}
gridState.InitializeForContext(context, this);
}
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState!;
gridState.UninitializeForContext(context);
}
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
// 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, _maximumRowsOrColumns);
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
context,
true,
MinItemSpacing,
LineSpacing,
_maximumRowsOrColumns,
_orientation.ScrollOrientation,
false,
LayoutId);
// If after Measure the first item is in the realization rect, then we revoke grid state's ownership,
// and only use the layout when to clear it when it's done.
gridState.EnsureFirstElementOwnership(context);
return desiredSize;
}
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,
context,
true,
(FlowLayoutAlgorithm.LineAlignment)_itemsJustification,
LayoutId);
return new Size(value.Width, value.Height);
}
protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object? source, NotifyCollectionChangedEventArgs args)
{
GetFlowAlgorithm(context).OnItemsSourceChanged(source, args, context);
// Always invalidate layout to keep the view accurate.
InvalidateLayout();
var gridState = (UniformGridLayoutState)context.LayoutState!;
gridState.ClearElementOnDataSourceChange(context, args);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (change.Property == OrientationProperty)
{
var orientation = change.NewValue.GetValueOrDefault<Orientation>();
//Note: For UniformGridLayout Vertical Orientation means we have a Horizontal ScrollOrientation. Horizontal Orientation means we have a Vertical ScrollOrientation.
//i.e. the properties are the inverse of each other.
var scrollOrientation = (orientation == Orientation.Horizontal) ? ScrollOrientation.Vertical : ScrollOrientation.Horizontal;
_orientation.ScrollOrientation = scrollOrientation;
}
else if (change.Property == MinColumnSpacingProperty)
{
_minColumnSpacing = change.NewValue.GetValueOrDefault<double>();
}
else if (change.Property == MinRowSpacingProperty)
{
_minRowSpacing = change.NewValue.GetValueOrDefault<double>();
}
else if (change.Property == ItemsJustificationProperty)
{
_itemsJustification = change.NewValue.GetValueOrDefault<UniformGridLayoutItemsJustification>();
}
else if (change.Property == ItemsStretchProperty)
{
_itemsStretch = change.NewValue.GetValueOrDefault<UniformGridLayoutItemsStretch>();
}
else if (change.Property == MinItemWidthProperty)
{
_minItemWidth = change.NewValue.GetValueOrDefault<double>();
}
else if (change.Property == MinItemHeightProperty)
{
_minItemHeight = change.NewValue.GetValueOrDefault<double>();
}
else if (change.Property == MaximumRowsOrColumnsProperty)
{
_maximumRowsOrColumns = change.NewValue.GetValueOrDefault<int>();
}
InvalidateLayout();
}
private double GetMinorSizeWithSpacing(VirtualizingLayoutContext context)
{
var minItemSpacing = MinItemSpacing;
var gridState = (UniformGridLayoutState)context.LayoutState!;
return _orientation.ScrollOrientation == ScrollOrientation.Vertical?
gridState.EffectiveItemWidth + minItemSpacing :
gridState.EffectiveItemHeight + minItemSpacing;
}
private double GetMajorSizeWithSpacing(VirtualizingLayoutContext context)
{
var lineSpacing = LineSpacing;
var gridState = (UniformGridLayoutState)context.LayoutState!;
return _orientation.ScrollOrientation == ScrollOrientation.Vertical ?
gridState.EffectiveItemHeight + lineSpacing :
gridState.EffectiveItemWidth + lineSpacing;
}
Rect GetLayoutRectForDataIndex(
Size availableSize,
int index,
Rect lastExtent,
VirtualizingLayoutContext 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);
var gridState = (UniformGridLayoutState)context.LayoutState!;
Rect bounds = _orientation.MinorMajorRect(
indexInRow * GetMinorSizeWithSpacing(context) + _orientation.MinorStart(lastExtent),
rowIndex * GetMajorSizeWithSpacing(context) + _orientation.MajorStart(lastExtent),
_orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight,
_orientation.ScrollOrientation == ScrollOrientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth);
return bounds;
}
private void InvalidateLayout() => InvalidateMeasure();
private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((UniformGridLayoutState)context.LayoutState!).FlowAlgorithm;
}
}