Browse Source

Added UniformGridLayout.

pull/2603/head
Steven Kirk 7 years ago
parent
commit
aef3a2c9bd
  1. 23
      src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs
  2. 7
      src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs
  3. 13
      src/Avalonia.Controls/Repeaters/StackLayout.cs
  4. 389
      src/Avalonia.Controls/Repeaters/UniformGridLayout.cs
  5. 176
      src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs

23
src/Avalonia.Controls/Repeaters/FlowLayoutAlgorithm.cs

@ -656,7 +656,7 @@ namespace Avalonia.Controls.Repeaters
}
}
void SetLayoutOrigin()
private void SetLayoutOrigin()
{
if (IsVirtualizingContext)
{
@ -670,6 +670,27 @@ namespace Avalonia.Controls.Repeaters
}
}
public IControl GetElementIfRealized(int dataIndex)
{
if (_elementManager.IsDataIndexRealized(dataIndex))
{
return _elementManager.GetRealizedElement(dataIndex);
}
return null;
}
public bool TryAddElement0(IControl element)
{
if (_elementManager.GetRealizedElementCount() == 0)
{
_elementManager.Add(element, 0);
return true;
}
return false;
}
public enum LineAlignment
{
Start,

7
src/Avalonia.Controls/Repeaters/OrientationBasedMeasures.cs

@ -61,6 +61,13 @@
}
}
public Rect MinorMajorRect(double minor, double major, double minorSize, double majorSize)
{
return ScrollOrientation == Orientation.Vertical ?
new Rect(minor, major, minorSize, majorSize) :
new Rect(major, minor, majorSize, minorSize);
}
public Point MinorMajorPoint(double minor, double major)
{
return ScrollOrientation == Orientation.Vertical ?

13
src/Avalonia.Controls/Repeaters/StackLayout.cs

@ -5,14 +5,19 @@ namespace Avalonia.Controls.Repeaters
{
public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
{
public static readonly AvaloniaProperty<Orientation> OrientationProperty
= StackPanel.OrientationProperty.AddOwner<StackLayout>();
public static readonly StyledProperty<Orientation> OrientationProperty =
StackPanel.OrientationProperty.AddOwner<StackLayout>();
public static readonly AvaloniaProperty<double> SpacingProperty
= StackPanel.SpacingProperty.AddOwner<StackLayout>();
public static readonly StyledProperty<double> SpacingProperty =
StackPanel.SpacingProperty.AddOwner<StackLayout>();
private readonly OrientationBasedMeasures _orientation = new OrientationBasedMeasures();
public StackLayout()
{
LayoutId = "StackLayout";
}
public Orientation Orientation
{
get => GetValue(OrientationProperty);

389
src/Avalonia.Controls/Repeaters/UniformGridLayout.cs

@ -0,0 +1,389 @@
using System;
using System.Collections.Specialized;
namespace Avalonia.Controls.Repeaters
{
public enum UniformGridLayoutItemsJustification
{
Start = 0,
Center = 1,
End = 2,
SpaceAround = 3,
SpaceBetween = 4,
SpaceEvenly = 5,
};
public enum UniformGridLayoutItemsStretch
{
None = 0,
Fill = 1,
Uniform = 2,
};
public class UniformGridLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
{
public static readonly StyledProperty<UniformGridLayoutItemsJustification> ItemsJustificationProperty =
AvaloniaProperty.Register<UniformGridLayout, UniformGridLayoutItemsJustification>(nameof(ItemsJustification));
public static readonly StyledProperty<UniformGridLayoutItemsStretch> ItemsStretchProperty =
AvaloniaProperty.Register<UniformGridLayout, UniformGridLayoutItemsStretch>(nameof(ItemsStretch));
public static readonly StyledProperty<double> MinColumnSpacingProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinColumnSpacing));
public static readonly StyledProperty<double> MinItemHeightProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinItemHeight));
public static readonly StyledProperty<double> MinItemWidthProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinItemWidth));
public static readonly StyledProperty<double> MinRowSpacingProperty =
AvaloniaProperty.Register<UniformGridLayout, double>(nameof(MinRowSpacing));
public static readonly StyledProperty<Orientation> OrientationProperty =
StackPanel.OrientationProperty.AddOwner<StackLayout>();
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;
public UniformGridLayout()
{
LayoutId = "UniformGridLayout";
}
static UniformGridLayout()
{
OrientationProperty.OverrideDefaultValue<UniformGridLayout>(Orientation.Horizontal);
}
public UniformGridLayoutItemsJustification ItemsJustification
{
get => GetValue(ItemsJustificationProperty);
set => SetValue(ItemsJustificationProperty, value);
}
public UniformGridLayoutItemsStretch ItemsStretch
{
get => GetValue(ItemsStretchProperty);
set => SetValue(ItemsStretchProperty, value);
}
public double MinColumnSpacing
{
get => GetValue(MinColumnSpacingProperty);
set => SetValue(MinColumnSpacingProperty, value);
}
public double MinItemHeight
{
get => GetValue(MinItemHeightProperty);
set => SetValue(MinItemHeightProperty, value);
}
public double MinItemWidth
{
get => GetValue(MinItemWidthProperty);
set => SetValue(MinItemWidthProperty, value);
}
public double MinRowSpacing
{
get => GetValue(MinRowSpacingProperty);
set => SetValue(MinRowSpacingProperty, 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;
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);
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));
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 = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
int indexOfFirstInLine = (targetIndex / itemsPerLine) * itemsPerLine;
index = indexOfFirstInLine;
var state = context.LayoutState as UniformGridLayoutState;
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,
IControl firstRealized,
int firstRealizedItemIndex,
Rect firstRealizedLayoutBounds,
IControl lastRealized,
int lastRealizedItemIndex,
Rect lastRealizedLayoutBounds)
{
var extent = new Rect();
// Constants
int itemsCount = context.ItemCount;
double availableSizeMinor = _orientation.Minor(availableSize);
int itemsPerLine = Math.Max(1, !double.IsInfinity(availableSizeMinor) ?
(int)(availableSizeMinor / GetMinorSizeWithSpacing(context)) : itemsCount);
double lineSize = GetMajorSizeWithSpacing(context);
if (itemsCount > 0)
{
_orientation.SetMinorSize(
ref extent,
!double.IsInfinity(availableSizeMinor) ?
availableSizeMinor :
Math.Max(0.0, itemsCount * GetMinorSizeWithSpacing(context) - (double)MinItemSpacing));
_orientation.SetMajorSize(
ref extent,
Math.Max(0.0, (itemsCount / itemsPerLine) * lineSize - (double)LineSpacing));
if (firstRealized != null)
{
////MUX_ASSERT(lastRealized);
_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);
}
}
return extent;
}
void IFlowLayoutAlgorithmDelegates.Algorithm_OnElementMeasured(IControl 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 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 override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
var gridState = (UniformGridLayoutState)context.LayoutState;
gridState.UninitializeForContext(context);
}
protected 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);
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
context,
true,
MinItemSpacing,
LineSpacing,
_orientation.ScrollOrientation,
LayoutId);
return new Size(desiredSize.Width, desiredSize.Height);
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
var value = GetFlowAlgorithm(context).Arrange(
finalSize,
context,
(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(AvaloniaPropertyChangedEventArgs args)
{
if (args.Property == OrientationProperty)
{
var orientation = (Orientation)args.NewValue;
//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) ? Orientation.Vertical : Orientation.Horizontal;
_orientation.ScrollOrientation = scrollOrientation;
}
else if (args.Property == MinColumnSpacingProperty)
{
_minColumnSpacing = (double)args.NewValue;
}
else if (args.Property == MinRowSpacingProperty)
{
_minRowSpacing = (double)args.NewValue;
}
else if (args.Property == ItemsJustificationProperty)
{
_itemsJustification = (UniformGridLayoutItemsJustification)args.NewValue;
}
else if (args.Property == ItemsStretchProperty)
{
_itemsStretch = (UniformGridLayoutItemsStretch)args.NewValue;
}
else if (args.Property == MinItemWidthProperty)
{
_minItemWidth = (double)args.NewValue;
}
else if (args.Property == MinItemHeightProperty)
{
_minItemHeight = (double)args.NewValue;
}
InvalidateLayout();
}
private double GetMinorSizeWithSpacing(VirtualizingLayoutContext context)
{
var minItemSpacing = MinItemSpacing;
var gridState = (UniformGridLayoutState)context.LayoutState;
return _orientation.ScrollOrientation == Orientation.Vertical?
gridState.EffectiveItemWidth + minItemSpacing :
gridState.EffectiveItemHeight + minItemSpacing;
}
private double GetMajorSizeWithSpacing(VirtualizingLayoutContext context)
{
var lineSpacing = LineSpacing;
var gridState = (UniformGridLayoutState)context.LayoutState;
return _orientation.ScrollOrientation == Orientation.Vertical ?
gridState.EffectiveItemHeight + lineSpacing :
gridState.EffectiveItemWidth + lineSpacing;
}
Rect GetLayoutRectForDataIndex(
Size availableSize,
int index,
Rect lastExtent,
VirtualizingLayoutContext context)
{
int itemsPerLine = Math.Max(1, (int)(_orientation.Minor(availableSize) / GetMinorSizeWithSpacing(context)));
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 == Orientation.Vertical ? gridState.EffectiveItemWidth : gridState.EffectiveItemHeight,
_orientation.ScrollOrientation == Orientation.Vertical ? gridState.EffectiveItemHeight : gridState.EffectiveItemWidth);
return bounds;
}
private void InvalidateLayout() => InvalidateMeasure();
private FlowLayoutAlgorithm GetFlowAlgorithm(VirtualizingLayoutContext context) => ((UniformGridLayoutState)context.LayoutState).FlowAlgorithm;
}
}

176
src/Avalonia.Controls/Repeaters/UniformGridLayoutState.cs

@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
namespace Avalonia.Controls.Repeaters
{
public class UniformGridLayoutState
{
// We need to measure the element at index 0 to know what size to measure all other items.
// If FlowlayoutAlgorithm has already realized element 0 then we can use that.
// If it does not, then we need to do context.GetElement(0) at which point we have requested an element and are on point to clear it.
// If we are responsible for clearing element 0 we keep m_cachedFirstElement valid.
// If we are not (because FlowLayoutAlgorithm is holding it for us) then we just null out this field and use the one from FlowLayoutAlgorithm.
private IControl _cachedFirstElement;
internal FlowLayoutAlgorithm FlowAlgorithm { get; } = new FlowLayoutAlgorithm();
internal double EffectiveItemWidth { get; private set; }
internal double EffectiveItemHeight { get; private set; }
internal void InitializeForContext(VirtualizingLayoutContext context, IFlowLayoutAlgorithmDelegates callbacks)
{
FlowAlgorithm.InitializeForContext(context, callbacks);
context.LayoutState = this;
}
internal void UninitializeForContext(VirtualizingLayoutContext context)
{
FlowAlgorithm.UninitializeForContext(context);
if (_cachedFirstElement != null)
{
context.RecycleElement(_cachedFirstElement);
}
}
internal void EnsureElementSize(
Size availableSize,
VirtualizingLayoutContext context,
double layoutItemWidth,
double LayoutItemHeight,
UniformGridLayoutItemsStretch stretch,
Orientation orientation,
double minRowSpacing,
double minColumnSpacing)
{
if (context.ItemCount > 0)
{
// If the first element is realized we don't need to cache it or to get it from the context
var realizedElement = FlowAlgorithm.GetElementIfRealized(0);
if (realizedElement != null)
{
realizedElement.Measure(availableSize);
SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
_cachedFirstElement = null;
}
else
{
if (_cachedFirstElement == null)
{
// we only cache if we aren't realizing it
_cachedFirstElement = context.GetOrCreateElementAt(
0,
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); // expensive
}
_cachedFirstElement.Measure(availableSize);
SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing);
// 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);
if (added)
{
_cachedFirstElement = null;
}
}
}
}
private void SetSize(
IControl element,
double layoutItemWidth,
double LayoutItemHeight,
Size availableSize,
UniformGridLayoutItemsStretch stretch,
Orientation orientation,
double minRowSpacing,
double minColumnSpacing)
{
EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth);
EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight);
var availableSizeMinor = orientation == Orientation.Horizontal ? availableSize.Width : availableSize.Height;
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;
if (stretch == UniformGridLayoutItemsStretch.Fill)
{
if (orientation == Orientation.Horizontal)
{
EffectiveItemWidth += extraMinorPixelsForEachItem;
}
else
{
EffectiveItemHeight += extraMinorPixelsForEachItem;
}
}
else if (stretch == UniformGridLayoutItemsStretch.Uniform)
{
var itemSizeMajor = orientation == Orientation.Horizontal ? EffectiveItemHeight : EffectiveItemWidth;
var extraMajorPixelsForEachItem = itemSizeMajor * (extraMinorPixelsForEachItem / itemSizeMinor);
if (orientation == Orientation.Horizontal)
{
EffectiveItemWidth += extraMinorPixelsForEachItem;
EffectiveItemHeight += extraMajorPixelsForEachItem;
}
else
{
EffectiveItemHeight += extraMinorPixelsForEachItem;
EffectiveItemWidth += extraMajorPixelsForEachItem;
}
}
}
internal void EnsureFirstElementOwnership()
{
if (FlowAlgorithm.GetElementIfRealized(0) != null)
{
_cachedFirstElement = null;
}
}
internal void ClearElementOnDataSourceChange(
VirtualizingLayoutContext context,
NotifyCollectionChangedEventArgs args)
{
if (_cachedFirstElement != null)
{
bool shouldClear = false;
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
shouldClear = args.NewStartingIndex == 0;
break;
case NotifyCollectionChangedAction.Replace:
shouldClear = args.NewStartingIndex == 0 || args.OldStartingIndex == 0;
break;
case NotifyCollectionChangedAction.Remove:
shouldClear = args.OldStartingIndex == 0;
break;
case NotifyCollectionChangedAction.Reset:
shouldClear = true;
break;
case NotifyCollectionChangedAction.Move:
throw new NotImplementedException();
}
if (shouldClear)
{
context.RecycleElement(_cachedFirstElement);
_cachedFirstElement = null;
}
}
}
}
}
Loading…
Cancel
Save