Browse Source

Merge branch 'master' into feature/relative-panel

pull/3987/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
65c4676b24
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  2. 20
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  3. 2
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  4. 37
      src/Avalonia.Layout/WrapLayout/UvBounds.cs
  5. 45
      src/Avalonia.Layout/WrapLayout/UvMeasure.cs
  6. 25
      src/Avalonia.Layout/WrapLayout/WrapItem.cs
  7. 327
      src/Avalonia.Layout/WrapLayout/WrapLayout.cs
  8. 143
      src/Avalonia.Layout/WrapLayout/WrapLayoutState.cs

14
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -12,6 +12,8 @@
<ComboBoxItem>Stack - Horizontal</ComboBoxItem>
<ComboBoxItem>UniformGrid - Vertical</ComboBoxItem>
<ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem>
<ComboBoxItem>WrapLayout - Horizontal</ComboBoxItem>
<ComboBoxItem>WrapLayout - Veritcal</ComboBoxItem>
</ComboBox>
<Button Command="{Binding AddItem}">Add Item</Button>
<Button Command="{Binding RandomizeHeights}">Randomize Heights</Button>
@ -24,12 +26,16 @@
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}">
<ItemsRepeater.Styles>
<Style Selector="Border:pointerover">
<Setter Property="Width" Value="200" />
</Style>
</ItemsRepeater.Styles>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<TextBlock Focusable="True"
Background="{Binding Background}"
Height="{Binding Height}"
Text="{Binding Text}"/>
<Border Background="Purple" BorderThickness="2" BorderBrush="Green" MinHeight="100" MinWidth="100">
<TextBlock Focusable="True" Height="{Binding Height}" Text="{Binding Text}" Foreground="White" />
</Border>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>

20
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs

@ -79,6 +79,26 @@ namespace ControlCatalog.Pages
MinItemHeight = 200,
};
break;
case 4:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
_repeater.Layout = new WrapLayout
{
Orientation = Orientation.Vertical,
HorizontalSpacing = 20,
VerticalSpacing = 20
};
break;
case 5:
_scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled;
_scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
_repeater.Layout = new WrapLayout
{
Orientation = Orientation.Horizontal,
HorizontalSpacing = 20,
VerticalSpacing = 20
};
break;
}
}

2
src/Avalonia.Headless/HeadlessWindowImpl.cs

@ -244,6 +244,8 @@ namespace Avalonia.Headless
public Action LostFocus { get; set; }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => new AcrylicPlatformCompensationLevels(1, 1, 1);
void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers)
{
Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot, RawKeyEventType.KeyDown, key, modifiers));

37
src/Avalonia.Layout/WrapLayout/UvBounds.cs

@ -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; }
}
}

45
src/Avalonia.Layout/WrapLayout/UvMeasure.cs

@ -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();
}
}
}

25
src/Avalonia.Layout/WrapLayout/WrapItem.cs

@ -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; }
}
}

327
src/Avalonia.Layout/WrapLayout/WrapLayout.cs

@ -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();
}
}
}
}

143
src/Avalonia.Layout/WrapLayout/WrapLayoutState.cs

@ -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…
Cancel
Save