committed by
GitHub
7 changed files with 607 additions and 4 deletions
@ -0,0 +1,37 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Layout; |
||||
|
|
||||
|
namespace Avalonia.Layout |
||||
|
{ |
||||
|
internal struct UvBounds |
||||
|
{ |
||||
|
public UvBounds(Orientation orientation, Rect rect) |
||||
|
{ |
||||
|
if (orientation == Orientation.Horizontal) |
||||
|
{ |
||||
|
UMin = rect.Left; |
||||
|
UMax = rect.Right; |
||||
|
VMin = rect.Top; |
||||
|
VMax = rect.Bottom; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
UMin = rect.Top; |
||||
|
UMax = rect.Bottom; |
||||
|
VMin = rect.Left; |
||||
|
VMax = rect.Right; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public double UMin { get; } |
||||
|
|
||||
|
public double UMax { get; } |
||||
|
|
||||
|
public double VMin { get; } |
||||
|
|
||||
|
public double VMax { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
using Avalonia.Layout; |
||||
|
|
||||
|
namespace Avalonia.Layout |
||||
|
{ |
||||
|
internal struct UvMeasure |
||||
|
{ |
||||
|
internal static readonly UvMeasure Zero = default(UvMeasure); |
||||
|
|
||||
|
internal double U { get; set; } |
||||
|
|
||||
|
internal double V { get; set; } |
||||
|
|
||||
|
public UvMeasure(Orientation orientation, double width, double height) |
||||
|
{ |
||||
|
if (orientation == Orientation.Horizontal) |
||||
|
{ |
||||
|
U = width; |
||||
|
V = height; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
U = height; |
||||
|
V = width; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override bool Equals(object obj) |
||||
|
{ |
||||
|
if (obj is UvMeasure measure) |
||||
|
{ |
||||
|
return (measure.U == U) && (measure.V == V); |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public override int GetHashCode() |
||||
|
{ |
||||
|
return base.GetHashCode(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Layout; |
||||
|
|
||||
|
namespace Avalonia.Layout |
||||
|
{ |
||||
|
internal class WrapItem |
||||
|
{ |
||||
|
public WrapItem(int index) |
||||
|
{ |
||||
|
this.Index = index; |
||||
|
} |
||||
|
|
||||
|
public int Index { get; } |
||||
|
|
||||
|
public UvMeasure? Measure { get; internal set; } |
||||
|
|
||||
|
public UvMeasure? Position { get; internal set; } |
||||
|
|
||||
|
public ILayoutable Element { get; internal set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,327 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Layout; |
||||
|
using System; |
||||
|
using System.Collections.Specialized; |
||||
|
|
||||
|
namespace Avalonia.Layout |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Arranges elements by wrapping them to fit the available space.
|
||||
|
/// When <see cref="Orientation"/> is set to Orientation.Horizontal, element are arranged in rows until the available width is reached and then to a new row.
|
||||
|
/// When <see cref="Orientation"/> is set to Orientation.Vertical, element are arranged in columns until the available height is reached.
|
||||
|
/// </summary>
|
||||
|
public class WrapLayout : VirtualizingLayout |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets or sets a uniform Horizontal distance (in pixels) between items when <see cref="Orientation"/> is set to Horizontal,
|
||||
|
/// or between columns of items when <see cref="Orientation"/> is set to Vertical.
|
||||
|
/// </summary>
|
||||
|
public double HorizontalSpacing |
||||
|
{ |
||||
|
get { return (double)GetValue(HorizontalSpacingProperty); } |
||||
|
set { SetValue(HorizontalSpacingProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Identifies the <see cref="HorizontalSpacing"/> dependency property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<double> HorizontalSpacingProperty = |
||||
|
AvaloniaProperty.Register<WrapLayout, double>(nameof(HorizontalSpacing), 0); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a uniform Vertical distance (in pixels) between items when <see cref="Orientation"/> is set to Vertical,
|
||||
|
/// or between rows of items when <see cref="Orientation"/> is set to Horizontal.
|
||||
|
/// </summary>
|
||||
|
public double VerticalSpacing |
||||
|
{ |
||||
|
get { return (double)GetValue(VerticalSpacingProperty); } |
||||
|
set { SetValue(VerticalSpacingProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Identifies the <see cref="VerticalSpacing"/> dependency property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<double> VerticalSpacingProperty = |
||||
|
AvaloniaProperty.Register<WrapLayout, double>( |
||||
|
nameof(VerticalSpacing), 0d); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the orientation of the WrapLayout.
|
||||
|
/// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls.
|
||||
|
/// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added.
|
||||
|
/// </summary>
|
||||
|
public Orientation Orientation |
||||
|
{ |
||||
|
get { return (Orientation)GetValue(OrientationProperty); } |
||||
|
set { SetValue(OrientationProperty, value); } |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Identifies the <see cref="Orientation"/> dependency property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<Orientation> OrientationProperty = |
||||
|
AvaloniaProperty.Register<WrapLayout, Orientation>( |
||||
|
nameof(Orientation), |
||||
|
Orientation.Horizontal); |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) |
||||
|
{ |
||||
|
var state = new WrapLayoutState(context); |
||||
|
context.LayoutState = state; |
||||
|
base.InitializeForContextCore(context); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) |
||||
|
{ |
||||
|
context.LayoutState = null; |
||||
|
base.UninitializeForContextCore(context); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected internal override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args) |
||||
|
{ |
||||
|
var state = (WrapLayoutState)context.LayoutState; |
||||
|
|
||||
|
switch (args.Action) |
||||
|
{ |
||||
|
case NotifyCollectionChangedAction.Add: |
||||
|
state.RemoveFromIndex(args.NewStartingIndex); |
||||
|
break; |
||||
|
case NotifyCollectionChangedAction.Move: |
||||
|
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex); |
||||
|
state.RemoveFromIndex(minIndex); |
||||
|
|
||||
|
state.RecycleElementAt(args.OldStartingIndex); |
||||
|
state.RecycleElementAt(args.NewStartingIndex); |
||||
|
break; |
||||
|
case NotifyCollectionChangedAction.Remove: |
||||
|
state.RemoveFromIndex(args.OldStartingIndex); |
||||
|
break; |
||||
|
case NotifyCollectionChangedAction.Replace: |
||||
|
state.RemoveFromIndex(args.NewStartingIndex); |
||||
|
state.RecycleElementAt(args.NewStartingIndex); |
||||
|
break; |
||||
|
case NotifyCollectionChangedAction.Reset: |
||||
|
state.Clear(); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
base.OnItemsChangedCore(context, source, args); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) |
||||
|
{ |
||||
|
var totalMeasure = UvMeasure.Zero; |
||||
|
var parentMeasure = new UvMeasure(Orientation, availableSize.Width, availableSize.Height); |
||||
|
var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); |
||||
|
var realizationBounds = new UvBounds(Orientation, context.RealizationRect); |
||||
|
var position = UvMeasure.Zero; |
||||
|
|
||||
|
var state = (WrapLayoutState)context.LayoutState; |
||||
|
if (state.Orientation != Orientation) |
||||
|
{ |
||||
|
state.SetOrientation(Orientation); |
||||
|
} |
||||
|
|
||||
|
if (spacingMeasure.Equals(state.Spacing) == false) |
||||
|
{ |
||||
|
state.ClearPositions(); |
||||
|
state.Spacing = spacingMeasure; |
||||
|
} |
||||
|
|
||||
|
if (state.AvailableU != parentMeasure.U) |
||||
|
{ |
||||
|
state.ClearPositions(); |
||||
|
state.AvailableU = parentMeasure.U; |
||||
|
} |
||||
|
|
||||
|
double currentV = 0; |
||||
|
for (int i = 0; i < context.ItemCount; i++) |
||||
|
{ |
||||
|
bool measured = false; |
||||
|
WrapItem item = state.GetItemAt(i); |
||||
|
if (item.Measure == null) |
||||
|
{ |
||||
|
item.Element = context.GetOrCreateElementAt(i); |
||||
|
item.Element.Measure(availableSize); |
||||
|
item.Measure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); |
||||
|
measured = true; |
||||
|
} |
||||
|
|
||||
|
UvMeasure currentMeasure = item.Measure.Value; |
||||
|
if (currentMeasure.U == 0) |
||||
|
{ |
||||
|
continue; // ignore collapsed items
|
||||
|
} |
||||
|
|
||||
|
if (item.Position == null) |
||||
|
{ |
||||
|
if (parentMeasure.U < position.U + currentMeasure.U) |
||||
|
{ |
||||
|
// New Row
|
||||
|
position.U = 0; |
||||
|
position.V += currentV + spacingMeasure.V; |
||||
|
currentV = 0; |
||||
|
} |
||||
|
|
||||
|
item.Position = position; |
||||
|
} |
||||
|
|
||||
|
position = item.Position.Value; |
||||
|
|
||||
|
double vEnd = position.V + currentMeasure.V; |
||||
|
if (vEnd < realizationBounds.VMin) |
||||
|
{ |
||||
|
// Item is "above" the bounds
|
||||
|
if (item.Element != null) |
||||
|
{ |
||||
|
context.RecycleElement(item.Element); |
||||
|
item.Element = null; |
||||
|
} |
||||
|
} |
||||
|
else if (position.V > realizationBounds.VMax) |
||||
|
{ |
||||
|
// Item is "below" the bounds.
|
||||
|
if (item.Element != null) |
||||
|
{ |
||||
|
context.RecycleElement(item.Element); |
||||
|
item.Element = null; |
||||
|
} |
||||
|
|
||||
|
// We don't need to measure anything below the bounds
|
||||
|
break; |
||||
|
} |
||||
|
else if (measured == false) |
||||
|
{ |
||||
|
// Always measure elements that are within the bounds
|
||||
|
item.Element = context.GetOrCreateElementAt(i); |
||||
|
item.Element.Measure(availableSize); |
||||
|
|
||||
|
currentMeasure = new UvMeasure(Orientation, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height); |
||||
|
if (currentMeasure.Equals(item.Measure) == false) |
||||
|
{ |
||||
|
// this item changed size; we need to recalculate layout for everything after this
|
||||
|
state.RemoveFromIndex(i + 1); |
||||
|
item.Measure = currentMeasure; |
||||
|
|
||||
|
// did the change make it go into the new row?
|
||||
|
if (parentMeasure.U < position.U + currentMeasure.U) |
||||
|
{ |
||||
|
// New Row
|
||||
|
position.U = 0; |
||||
|
position.V += currentV + spacingMeasure.V; |
||||
|
currentV = 0; |
||||
|
} |
||||
|
|
||||
|
item.Position = position; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
position.U += currentMeasure.U + spacingMeasure.U; |
||||
|
currentV = Math.Max(currentMeasure.V, currentV); |
||||
|
} |
||||
|
|
||||
|
// update value with the last line
|
||||
|
// if the the last loop is(parentMeasure.U > currentMeasure.U + lineMeasure.U) the total isn't calculated then calculate it
|
||||
|
// if the last loop is (parentMeasure.U > currentMeasure.U) the currentMeasure isn't added to the total so add it here
|
||||
|
// for the last condition it is zeros so adding it will make no difference
|
||||
|
// this way is faster than an if condition in every loop for checking the last item
|
||||
|
totalMeasure.U = parentMeasure.U; |
||||
|
totalMeasure.V = state.GetHeight(); |
||||
|
|
||||
|
totalMeasure.U = Math.Ceiling(totalMeasure.U); |
||||
|
|
||||
|
return Orientation == Orientation.Horizontal ? new Size(totalMeasure.U, totalMeasure.V) : new Size(totalMeasure.V, totalMeasure.U); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc />
|
||||
|
protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) |
||||
|
{ |
||||
|
if (context.ItemCount > 0) |
||||
|
{ |
||||
|
var parentMeasure = new UvMeasure(Orientation, finalSize.Width, finalSize.Height); |
||||
|
var spacingMeasure = new UvMeasure(Orientation, HorizontalSpacing, VerticalSpacing); |
||||
|
var realizationBounds = new UvBounds(Orientation, context.RealizationRect); |
||||
|
|
||||
|
var state = (WrapLayoutState)context.LayoutState; |
||||
|
bool Arrange(WrapItem item, bool isLast = false) |
||||
|
{ |
||||
|
if (item.Measure.HasValue == false) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
if (item.Position == null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var desiredMeasure = item.Measure.Value; |
||||
|
if (desiredMeasure.U == 0) |
||||
|
{ |
||||
|
return true; // if an item is collapsed, avoid adding the spacing
|
||||
|
} |
||||
|
|
||||
|
UvMeasure position = item.Position.Value; |
||||
|
|
||||
|
// Stretch the last item to fill the available space
|
||||
|
if (isLast) |
||||
|
{ |
||||
|
desiredMeasure.U = parentMeasure.U - position.U; |
||||
|
} |
||||
|
|
||||
|
if (((position.V + desiredMeasure.V) >= realizationBounds.VMin) && (position.V <= realizationBounds.VMax)) |
||||
|
{ |
||||
|
// place the item
|
||||
|
var child = context.GetOrCreateElementAt(item.Index); |
||||
|
if (Orientation == Orientation.Horizontal) |
||||
|
{ |
||||
|
child.Arrange(new Rect(position.U, position.V, desiredMeasure.U, desiredMeasure.V)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
child.Arrange(new Rect(position.V, position.U, desiredMeasure.V, desiredMeasure.U)); |
||||
|
} |
||||
|
} |
||||
|
else if (position.V > realizationBounds.VMax) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
for (var i = 0; i < context.ItemCount; i++) |
||||
|
{ |
||||
|
bool continueArranging = Arrange(state.GetItemAt(i)); |
||||
|
if (continueArranging == false) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return finalSize; |
||||
|
} |
||||
|
|
||||
|
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change) |
||||
|
{ |
||||
|
base.OnPropertyChanged(change); |
||||
|
|
||||
|
if (change.Property == OrientationProperty || change.Property == HorizontalSpacingProperty || change.Property == VerticalSpacingProperty) |
||||
|
{ |
||||
|
InvalidateMeasure(); |
||||
|
InvalidateArrange(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,143 @@ |
|||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
// See the LICENSE file in the project root for more information.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
|
||||
|
namespace Avalonia.Layout |
||||
|
{ |
||||
|
internal class WrapLayoutState |
||||
|
{ |
||||
|
private List<WrapItem> _items = new List<WrapItem>(); |
||||
|
private VirtualizingLayoutContext _context; |
||||
|
|
||||
|
public WrapLayoutState(VirtualizingLayoutContext context) |
||||
|
{ |
||||
|
this._context = context; |
||||
|
} |
||||
|
|
||||
|
public Orientation Orientation { get; private set; } |
||||
|
|
||||
|
public UvMeasure Spacing { get; internal set; } |
||||
|
|
||||
|
public double AvailableU { get; internal set; } |
||||
|
|
||||
|
internal WrapItem GetItemAt(int index) |
||||
|
{ |
||||
|
if (index < 0) |
||||
|
{ |
||||
|
throw new IndexOutOfRangeException(); |
||||
|
} |
||||
|
|
||||
|
if (index <= (_items.Count - 1)) |
||||
|
{ |
||||
|
return _items[index]; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
WrapItem item = new WrapItem(index); |
||||
|
_items.Add(item); |
||||
|
return item; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal void Clear() |
||||
|
{ |
||||
|
_items.Clear(); |
||||
|
} |
||||
|
|
||||
|
internal void RemoveFromIndex(int index) |
||||
|
{ |
||||
|
if (index >= _items.Count) |
||||
|
{ |
||||
|
// Item was added/removed but we haven't realized that far yet
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
int numToRemove = _items.Count - index; |
||||
|
_items.RemoveRange(index, numToRemove); |
||||
|
} |
||||
|
|
||||
|
internal void SetOrientation(Orientation orientation) |
||||
|
{ |
||||
|
foreach (var item in _items.Where(i => i.Measure.HasValue)) |
||||
|
{ |
||||
|
UvMeasure measure = item.Measure.Value; |
||||
|
double v = measure.V; |
||||
|
measure.V = measure.U; |
||||
|
measure.U = v; |
||||
|
item.Measure = measure; |
||||
|
item.Position = null; |
||||
|
} |
||||
|
|
||||
|
Orientation = orientation; |
||||
|
AvailableU = 0; |
||||
|
} |
||||
|
|
||||
|
internal void ClearPositions() |
||||
|
{ |
||||
|
foreach (var item in _items) |
||||
|
{ |
||||
|
item.Position = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal double GetHeight() |
||||
|
{ |
||||
|
if (_items.Count == 0) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
bool calculateAverage = true; |
||||
|
if ((_items.Count == _context.ItemCount) && _items[_items.Count - 1].Position.HasValue) |
||||
|
{ |
||||
|
calculateAverage = false; |
||||
|
} |
||||
|
|
||||
|
UvMeasure? lastPosition = null; |
||||
|
double maxV = 0; |
||||
|
|
||||
|
int itemCount = _items.Count; |
||||
|
for (int i = _items.Count - 1; i >= 0; i--) |
||||
|
{ |
||||
|
var item = _items[i]; |
||||
|
if (item.Position == null) |
||||
|
{ |
||||
|
itemCount--; |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
if (lastPosition != null) |
||||
|
{ |
||||
|
if (lastPosition.Value.V > item.Position.Value.V) |
||||
|
{ |
||||
|
// This is a row above the last item. Exit and calculate the average
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
lastPosition = item.Position; |
||||
|
maxV = Math.Max(maxV, item.Measure.Value.V); |
||||
|
} |
||||
|
|
||||
|
double totalHeight = lastPosition.Value.V + maxV; |
||||
|
if (calculateAverage) |
||||
|
{ |
||||
|
return (totalHeight / itemCount) * _context.ItemCount; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return totalHeight; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal void RecycleElementAt(int index) |
||||
|
{ |
||||
|
var element = _context.GetOrCreateElementAt(index); |
||||
|
_context.RecycleElement(element); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue