Browse Source

Merge branch 'master' into refactor/remove_duplicate_RoundLayoutValue

pull/4136/head
Dariusz Komosiński 6 years ago
committed by GitHub
parent
commit
da1a67f0b0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  2. 34
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  3. 13
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  4. 20
      src/Avalonia.Controls/IScrollAnchorProvider.cs
  5. 198
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  6. 8
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  7. 7
      src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs
  8. 27
      src/Avalonia.Controls/Repeater/ViewManager.cs
  9. 159
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  10. 1
      src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
  11. 37
      src/Avalonia.Controls/ScrollViewer.cs
  12. 2
      src/Avalonia.Layout/AttachedLayout.cs
  13. 4
      src/Avalonia.Layout/ElementManager.cs
  14. 65
      src/Avalonia.Layout/FlowLayoutAlgorithm.cs
  15. 2
      src/Avalonia.Layout/LayoutQueue.cs
  16. 4
      src/Avalonia.Layout/NonVirtualizingStackLayout.cs
  17. 8
      src/Avalonia.Layout/StackLayout.cs
  18. 7
      src/Avalonia.Layout/UniformGridLayout.cs
  19. 8
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
  20. 6
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs

7
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -16,6 +16,8 @@
<Button Command="{Binding AddItem}">Add Item</Button> <Button Command="{Binding AddItem}">Add Item</Button>
<Button Command="{Binding RandomizeHeights}">Randomize Heights</Button> <Button Command="{Binding RandomizeHeights}">Randomize Heights</Button>
<Button Command="{Binding ResetItems}">Reset items</Button> <Button Command="{Binding ResetItems}">Reset items</Button>
<Button x:Name="scrollToLast">Scroll to Last</Button>
<Button x:Name="scrollToRandom">Scroll to Random</Button>
</StackPanel> </StackPanel>
<Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16"> <Border BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Margin="0 0 0 16">
<ScrollViewer Name="scroller" <ScrollViewer Name="scroller"
@ -24,7 +26,10 @@
<ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}"> <ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}">
<ItemsRepeater.ItemTemplate> <ItemsRepeater.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Focusable="True" Height="{Binding Height}" Text="{Binding Text}"/> <TextBlock Focusable="True"
Background="{Binding Background}"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate> </DataTemplate>
</ItemsRepeater.ItemTemplate> </ItemsRepeater.ItemTemplate>
</ItemsRepeater> </ItemsRepeater>

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

@ -5,23 +5,32 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
using ControlCatalog.ViewModels; using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages namespace ControlCatalog.Pages
{ {
public class ItemsRepeaterPage : UserControl public class ItemsRepeaterPage : UserControl
{ {
private readonly ItemsRepeaterPageViewModel _viewModel;
private ItemsRepeater _repeater; private ItemsRepeater _repeater;
private ScrollViewer _scroller; private ScrollViewer _scroller;
private Button _scrollToLast;
private Button _scrollToRandom;
private Random _random = new Random(0);
public ItemsRepeaterPage() public ItemsRepeaterPage()
{ {
this.InitializeComponent(); this.InitializeComponent();
_repeater = this.FindControl<ItemsRepeater>("repeater"); _repeater = this.FindControl<ItemsRepeater>("repeater");
_scroller = this.FindControl<ScrollViewer>("scroller"); _scroller = this.FindControl<ScrollViewer>("scroller");
_scrollToLast = this.FindControl<Button>("scrollToLast");
_scrollToRandom = this.FindControl<Button>("scrollToRandom");
_repeater.PointerPressed += RepeaterClick; _repeater.PointerPressed += RepeaterClick;
_repeater.KeyDown += RepeaterOnKeyDown; _repeater.KeyDown += RepeaterOnKeyDown;
DataContext = new ItemsRepeaterPageViewModel(); _scrollToLast.Click += scrollToLast_Click;
_scrollToRandom.Click += scrollToRandom_Click;
DataContext = _viewModel = new ItemsRepeaterPageViewModel();
} }
private void InitializeComponent() private void InitializeComponent()
@ -73,18 +82,37 @@ namespace ControlCatalog.Pages
} }
} }
private void ScrollTo(int index)
{
System.Diagnostics.Debug.WriteLine("Scroll to " + index);
var layoutManager = ((Window)this.GetVisualRoot()).LayoutManager;
var element = _repeater.GetOrCreateElement(index);
layoutManager.ExecuteLayoutPass();
element.BringIntoView();
}
private void RepeaterClick(object sender, PointerPressedEventArgs e) private void RepeaterClick(object sender, PointerPressedEventArgs e)
{ {
var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item; var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item;
((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item; _viewModel.SelectedItem = item;
} }
private void RepeaterOnKeyDown(object sender, KeyEventArgs e) private void RepeaterOnKeyDown(object sender, KeyEventArgs e)
{ {
if (e.Key == Key.F5) if (e.Key == Key.F5)
{ {
((ItemsRepeaterPageViewModel)DataContext).ResetItems(); _viewModel.ResetItems();
} }
} }
private void scrollToLast_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
ScrollTo(_viewModel.Items.Count - 1);
}
private void scrollToRandom_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
ScrollTo(_random.Next(_viewModel.Items.Count - 1));
}
} }
} }

13
samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Avalonia.Media;
using ReactiveUI; using ReactiveUI;
namespace ControlCatalog.ViewModels namespace ControlCatalog.ViewModels
@ -27,7 +28,7 @@ namespace ControlCatalog.ViewModels
public void AddItem() public void AddItem()
{ {
var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1; var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1;
Items.Insert(index + 1, new Item { Text = $"New Item {_newItemIndex++}" }); Items.Insert(index + 1, new Item(index + 1) { Text = $"New Item {_newItemIndex++}" });
} }
public void RandomizeHeights() public void RandomizeHeights()
@ -52,7 +53,7 @@ namespace ControlCatalog.ViewModels
_newGenerationIndex++; _newGenerationIndex++;
return new ObservableCollection<Item>( return new ObservableCollection<Item>(
Enumerable.Range(1, 100000).Select(i => new Item Enumerable.Range(1, 100000).Select(i => new Item(i)
{ {
Text = $"Item {i.ToString()} {suffix}" Text = $"Item {i.ToString()} {suffix}"
})); }));
@ -61,6 +62,12 @@ namespace ControlCatalog.ViewModels
public class Item : ReactiveObject public class Item : ReactiveObject
{ {
private double _height = double.NaN; private double _height = double.NaN;
private int _index;
public Item(int index)
{
_index = index;
}
public string Text { get; set; } public string Text { get; set; }
@ -69,6 +76,8 @@ namespace ControlCatalog.ViewModels
get => _height; get => _height;
set => this.RaiseAndSetIfChanged(ref _height, value); set => this.RaiseAndSetIfChanged(ref _height, value);
} }
public IBrush Background => ((_index % 2) == 0) ? Brushes.Yellow : Brushes.Wheat;
} }
} }
} }

20
src/Avalonia.Controls/IScrollAnchorProvider.cs

@ -1,9 +1,29 @@
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary>
/// Specifies a contract for a scrolling control that supports scroll anchoring.
/// </summary>
public interface IScrollAnchorProvider public interface IScrollAnchorProvider
{ {
/// <summary>
/// The currently chosen anchor element to use for scroll anchoring.
/// </summary>
IControl CurrentAnchor { get; } IControl CurrentAnchor { get; }
/// <summary>
/// Registers a control as a potential scroll anchor candidate.
/// </summary>
/// <param name="element">
/// A control within the subtree of the <see cref="IScrollAnchorProvider"/>.
/// </param>
void RegisterAnchorCandidate(IControl element); void RegisterAnchorCandidate(IControl element);
/// <summary>
/// Unregisters a control as a potential scroll anchor candidate.
/// </summary>
/// <param name="element">
/// A control within the subtree of the <see cref="IScrollAnchorProvider"/>.
/// </param>
void UnregisterAnchorCandidate(IControl element); void UnregisterAnchorCandidate(IControl element);
} }
} }

198
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Runtime.InteropServices.ComTypes;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
@ -14,7 +12,7 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Presents a scrolling view of content inside a <see cref="ScrollViewer"/>. /// Presents a scrolling view of content inside a <see cref="ScrollViewer"/>.
/// </summary> /// </summary>
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
{ {
/// <summary> /// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property. /// Defines the <see cref="CanHorizontallyScroll"/> property.
@ -58,13 +56,19 @@ namespace Avalonia.Controls.Presenters
o => o.Viewport, o => o.Viewport,
(o, v) => o.Viewport = v); (o, v) => o.Viewport = v);
// Arbitrary chosen value, probably need to ask ILogicalScrollable
private const int LogicalScrollItemSize = 50;
private bool _canHorizontallyScroll; private bool _canHorizontallyScroll;
private bool _canVerticallyScroll; private bool _canVerticallyScroll;
private bool _arranging;
private Size _extent; private Size _extent;
private Vector _offset; private Vector _offset;
private IDisposable _logicalScrollSubscription; private IDisposable _logicalScrollSubscription;
private Size _viewport; private Size _viewport;
private Dictionary<int, Vector> _activeLogicalGestureScrolls; private Dictionary<int, Vector> _activeLogicalGestureScrolls;
private List<IControl> _anchorCandidates;
private (IControl control, Rect bounds) _anchor;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class. /// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -73,7 +77,6 @@ namespace Avalonia.Controls.Presenters
{ {
ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true);
ChildProperty.Changed.AddClassHandler<ScrollContentPresenter>((x, e) => x.ChildChanged(e)); ChildProperty.Changed.AddClassHandler<ScrollContentPresenter>((x, e) => x.ChildChanged(e));
AffectsArrange<ScrollContentPresenter>(OffsetProperty);
} }
/// <summary> /// <summary>
@ -87,6 +90,8 @@ namespace Avalonia.Controls.Presenters
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
} }
internal event EventHandler<VectorEventArgs> PreArrange;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally. /// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary> /// </summary>
@ -120,7 +125,7 @@ namespace Avalonia.Controls.Presenters
public Vector Offset public Vector Offset
{ {
get { return _offset; } get { return _offset; }
set { SetAndRaise(OffsetProperty, ref _offset, value); } set { SetAndRaise(OffsetProperty, ref _offset, ScrollViewer.CoerceOffset(Extent, Viewport, value)); }
} }
/// <summary> /// <summary>
@ -132,6 +137,9 @@ namespace Avalonia.Controls.Presenters
private set { SetAndRaise(ViewportProperty, ref _viewport, value); } private set { SetAndRaise(ViewportProperty, ref _viewport, value); }
} }
/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => _anchor.control;
/// <summary> /// <summary>
/// Attempts to bring a portion of the target visual into view by scrolling the content. /// Attempts to bring a portion of the target visual into view by scrolling the content.
/// </summary> /// </summary>
@ -196,6 +204,30 @@ namespace Avalonia.Controls.Presenters
return result; return result;
} }
/// <inheritdoc/>
void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element)
{
if (!this.IsVisualAncestorOf(element))
{
throw new InvalidOperationException(
"An anchor control must be a visual descendent of the ScrollContentPresenter.");
}
_anchorCandidates ??= new List<IControl>();
_anchorCandidates.Add(element);
}
/// <inheritdoc/>
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
{
_anchorCandidates?.Remove(element);
if (_anchor.control == element)
{
_anchor = default;
}
}
/// <inheritdoc/> /// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
@ -215,22 +247,85 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/> /// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
PreArrange?.Invoke(this, new VectorEventArgs
{
Vector = new Vector(finalSize.Width, finalSize.Height),
});
if (_logicalScrollSubscription != null || Child == null) if (_logicalScrollSubscription != null || Child == null)
{ {
return base.ArrangeOverride(finalSize); return base.ArrangeOverride(finalSize);
} }
try
{
_arranging = true;
return ArrangeWithAnchoring(finalSize);
}
finally
{
_arranging = false;
}
}
private Size ArrangeWithAnchoring(Size finalSize)
{
var size = new Size( var size = new Size(
CanHorizontallyScroll ? Math.Max(Child.DesiredSize.Width, finalSize.Width) : finalSize.Width, CanHorizontallyScroll ? Math.Max(Child.DesiredSize.Width, finalSize.Width) : finalSize.Width,
CanVerticallyScroll ? Math.Max(Child.DesiredSize.Height, finalSize.Height) : finalSize.Height); CanVerticallyScroll ? Math.Max(Child.DesiredSize.Height, finalSize.Height) : finalSize.Height);
Vector TrackAnchor()
{
// If we have an anchor and its position relative to Child has changed during the
// arrange then that change wasn't just due to scrolling (as scrolling doesn't adjust
// relative positions within Child).
if (_anchor.control != null &&
TranslateBounds(_anchor.control, Child, out var updatedBounds) &&
updatedBounds.Position != _anchor.bounds.Position)
{
var offset = updatedBounds.Position - _anchor.bounds.Position;
return offset;
}
return default;
}
// Calculate the new anchor element.
_anchor = CalculateCurrentAnchor();
// Do the arrange.
ArrangeOverrideImpl(size, -Offset); ArrangeOverrideImpl(size, -Offset);
// If the anchor moved during the arrange, we need to adjust the offset and do another arrange.
var anchorShift = TrackAnchor();
if (anchorShift != default)
{
var newOffset = Offset + anchorShift;
var newExtent = Extent;
var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height);
if (newOffset.X > maxOffset.X)
{
newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width);
}
if (newOffset.Y > maxOffset.Y)
{
newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height);
}
Extent = newExtent;
Offset = newOffset;
ArrangeOverrideImpl(size, -Offset);
}
Viewport = finalSize; Viewport = finalSize;
Extent = Child.Bounds.Size.Inflate(Child.Margin); Extent = Child.Bounds.Size.Inflate(Child.Margin);
return finalSize; return finalSize;
} }
// Arbitrary chosen value, probably need to ask ILogicalScrollable
private const int LogicalScrollItemSize = 50;
private void OnScrollGesture(object sender, ScrollGestureEventArgs e) private void OnScrollGesture(object sender, ScrollGestureEventArgs e)
{ {
if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width) if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
@ -327,6 +422,16 @@ namespace Avalonia.Controls.Presenters
} }
} }
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (change.Property == OffsetProperty && !_arranging)
{
InvalidateArrange();
}
base.OnPropertyChanged(change);
}
private void BringIntoViewRequested(object sender, RequestBringIntoViewEventArgs e) private void BringIntoViewRequested(object sender, RequestBringIntoViewEventArgs e)
{ {
e.Handled = BringDescendantIntoView(e.TargetObject, e.TargetRect); e.Handled = BringDescendantIntoView(e.TargetObject, e.TargetRect);
@ -390,5 +495,84 @@ namespace Avalonia.Controls.Presenters
Offset = scrollable.Offset; Offset = scrollable.Offset;
} }
} }
private (IControl, Rect) CalculateCurrentAnchor()
{
if (_anchorCandidates == null)
{
return default;
}
var bestCandidate = default(IControl);
var bestCandidateDistance = double.MaxValue;
// Find the anchor candidate that is scrolled closest to the top-left of this
// ScrollContentPresenter.
foreach (var element in _anchorCandidates)
{
if (element.IsVisible && GetViewportBounds(element, out var bounds))
{
var distance = (Vector)bounds.Position;
var candidateDistance = Math.Abs(distance.Length);
if (candidateDistance < bestCandidateDistance)
{
bestCandidate = element;
bestCandidateDistance = candidateDistance;
}
}
}
if (bestCandidate != null)
{
// We have a candidate, calculate its bounds relative to Child. Because these
// bounds aren't relative to the ScrollContentPresenter itself, if they change
// then we know it wasn't just due to scrolling.
var unscrolledBounds = TranslateBounds(bestCandidate, Child);
return (bestCandidate, unscrolledBounds);
}
return default;
}
private bool GetViewportBounds(IControl element, out Rect bounds)
{
if (TranslateBounds(element, Child, out var childBounds))
{
// We want the bounds relative to the new Offset, regardless of whether the child
// control has actually been arranged to this offset yet, so translate first to the
// child control and then apply Offset rather than translating directly to this
// control.
var thisBounds = new Rect(Bounds.Size);
bounds = new Rect(childBounds.Position - Offset, childBounds.Size);
return bounds.Intersects(thisBounds);
}
bounds = default;
return false;
}
private Rect TranslateBounds(IControl control, IControl to)
{
if (TranslateBounds(control, to, out var bounds))
{
return bounds;
}
throw new InvalidOperationException("The control's bounds could not be translated to the requested control.");
}
private bool TranslateBounds(IControl control, IControl to, out Rect bounds)
{
if (!control.IsVisible)
{
bounds = default;
return false;
}
var p = control.TranslatePoint(default, to);
bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default;
return p.HasValue;
}
} }
} }

8
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -80,6 +81,7 @@ namespace Avalonia.Controls
static ItemsRepeater() static ItemsRepeater()
{ {
ClipToBoundsProperty.OverrideDefaultValue<ItemsRepeater>(true); ClipToBoundsProperty.OverrideDefaultValue<ItemsRepeater>(true);
RequestBringIntoViewEvent.AddClassHandler<ItemsRepeater>((x, e) => x.OnRequestBringIntoView(e));
} }
/// <summary> /// <summary>
@ -305,6 +307,7 @@ namespace Avalonia.Controls
virtInfo.AutoRecycleCandidate && virtInfo.AutoRecycleCandidate &&
!virtInfo.KeepAlive) !virtInfo.KeepAlive)
{ {
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "AutoClear - {Index}", virtInfo.Index);
ClearElementImpl(element); ClearElementImpl(element);
} }
} }
@ -743,6 +746,11 @@ namespace Avalonia.Controls
} }
} }
private void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)
{
_viewportManager.OnBringIntoViewRequested(e);
}
private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure(); private void InvalidateMeasureForLayout(object sender, EventArgs e) => InvalidateMeasure();
private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange(); private void InvalidateArrangeForLayout(object sender, EventArgs e) => InvalidateArrange();

7
src/Avalonia.Controls/Repeater/RepeaterLayoutContext.cs

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Logging;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -58,7 +59,11 @@ namespace Avalonia.Controls
protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index); protected override object GetItemAtCore(int index) => _owner.ItemsSourceView.GetAt(index);
protected override void RecycleElementCore(ILayoutable element) => _owner.ClearElementImpl((IControl)element); protected override void RecycleElementCore(ILayoutable element)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "RepeaterLayout - RecycleElement: {Index}", _owner.GetElementIndex((IControl)element));
_owner.ClearElementImpl((IControl)element);
}
protected override Rect RealizationRectCore() => _owner.RealizationWindow; protected override Rect RealizationRectCore() => _owner.RealizationWindow;
} }

27
src/Avalonia.Controls/Repeater/ViewManager.cs

@ -11,6 +11,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -60,11 +61,13 @@ namespace Avalonia.Controls
if (suppressAutoRecycle) if (suppressAutoRecycle)
{ {
virtInfo.AutoRecycleCandidate = false; virtInfo.AutoRecycleCandidate = false;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} Not AutoRecycleCandidate:", virtInfo.Index);
} }
else else
{ {
virtInfo.AutoRecycleCandidate = true; virtInfo.AutoRecycleCandidate = true;
virtInfo.KeepAlive = true; virtInfo.KeepAlive = true;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "GetElement: {Index} AutoRecycleCandidate:", virtInfo.Index);
} }
return element; return element;
@ -107,10 +110,28 @@ namespace Avalonia.Controls
} }
} }
// We need to clear the datacontext to prevent crashes from happening,
// however we only do that if we were the ones setting it.
// That is when one of the following is the case (numbering taken from line ~642):
// 1.2 No ItemTemplate, data is not a UIElement
// 2.1 ItemTemplate, data is not FrameworkElement
// 2.2.2 Itemtemplate, data is FrameworkElement, ElementFactory returned Element different to data
//
// In all of those three cases, we the ItemTemplateShim is NOT null.
// Luckily when we create the items, we store whether we were the once setting the DataContext.
public void ClearElementToElementFactory(IControl element) public void ClearElementToElementFactory(IControl element)
{ {
_owner.OnElementClearing(element); _owner.OnElementClearing(element);
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToElementFactory();
// During creation of this object, we were the one setting the DataContext, so clear it now.
if (virtInfo.MustClearDataContext)
{
element.DataContext = null;
}
if (_owner.ItemTemplateShim != null) if (_owner.ItemTemplateShim != null)
{ {
_owner.ItemTemplateShim.RecycleElement(_owner, element); _owner.ItemTemplateShim.RecycleElement(_owner, element);
@ -124,9 +145,6 @@ namespace Avalonia.Controls
} }
} }
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToElementFactory();
if (_lastFocusedElement == element) if (_lastFocusedElement == element)
{ {
// Focused element is going away. Remove the tracked last focused element // Focused element is going away. Remove the tracked last focused element
@ -594,11 +612,14 @@ namespace Avalonia.Controls
{ {
virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element); virtInfo = ItemsRepeater.CreateAndInitializeVirtualizationInfo(element);
} }
// Clear flag
virtInfo.MustClearDataContext = false;
if (data != element) if (data != element)
{ {
// Prepare the element // Prepare the element
element.DataContext = data; element.DataContext = data;
virtInfo.MustClearDataContext = true;
} }
virtInfo.MoveOwnershipToLayoutFromElementFactory( virtInfo.MoveOwnershipToLayoutFromElementFactory(

159
src/Avalonia.Controls/Repeater/ViewportManager.cs

@ -5,9 +5,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls.Presenters;
using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Reactive;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -35,8 +41,8 @@ namespace Avalonia.Controls
// actually happened. This can happen in cases where no scrollviewer // actually happened. This can happen in cases where no scrollviewer
// in the parent chain can scroll in the shift direction. // in the parent chain can scroll in the shift direction.
private Point _unshiftableShift; private Point _unshiftableShift;
private double _maximumHorizontalCacheLength = 0.0; private double _maximumHorizontalCacheLength = 2.0;
private double _maximumVerticalCacheLength = 0.0; private double _maximumVerticalCacheLength = 2.0;
private double _horizontalCacheBufferPerSide; private double _horizontalCacheBufferPerSide;
private double _verticalCacheBufferPerSide; private double _verticalCacheBufferPerSide;
private bool _isBringIntoViewInProgress; private bool _isBringIntoViewInProgress;
@ -184,6 +190,9 @@ namespace Avalonia.Controls
// We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much. // We tolerate viewport imprecisions up to 1 pixel to avoid invaliding layout too much.
if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1) if (Math.Abs(_expectedViewportShift.X) > 1 || Math.Abs(_expectedViewportShift.Y) > 1)
{ {
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Expecting viewport shift of ({Shift})",
_owner.Layout.LayoutId, _expectedViewportShift);
// There are cases where we might be expecting a shift but not get it. We will // There are cases where we might be expecting a shift but not get it. We will
// be waiting for the effective viewport event but if the scroll viewer is not able // be waiting for the effective viewport event but if the scroll viewer is not able
// to perform the shift (perhaps because it cannot scroll in negative offset), // to perform the shift (perhaps because it cannot scroll in negative offset),
@ -229,14 +238,12 @@ namespace Avalonia.Controls
public void OnElementPrepared(IControl element) public void OnElementPrepared(IControl element)
{ {
// If we have an anchor element, we do not want the _scroller?.RegisterAnchorCandidate(element);
// scroll anchor provider to start anchoring some other element.
////element.CanBeScrollAnchor(true);
} }
public void OnElementCleared(ILayoutable element) public void OnElementCleared(IControl element)
{ {
////element.CanBeScrollAnchor(false); _scroller?.UnregisterAnchorCandidate(element);
} }
public void OnOwnerMeasuring() public void OnOwnerMeasuring()
@ -282,6 +289,7 @@ namespace Avalonia.Controls
private void OnLayoutUpdated(object sender, EventArgs args) private void OnLayoutUpdated(object sender, EventArgs args)
{ {
_owner.LayoutUpdated -= OnLayoutUpdated; _owner.LayoutUpdated -= OnLayoutUpdated;
_layoutUpdatedSubscribed = false;
if (_managingViewportDisabled) if (_managingViewportDisabled)
{ {
return; return;
@ -293,6 +301,10 @@ namespace Avalonia.Controls
// that can scroll in the direction where the shift is expected. // that can scroll in the direction where the shift is expected.
if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0) if (_pendingViewportShift.X != 0 || _pendingViewportShift.Y != 0)
{ {
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout Updated with pending shift {Shift}- invalidating measure",
_owner.Layout.LayoutId,
_pendingViewportShift);
// Assume this is never going to come. // Assume this is never going to come.
_unshiftableShift = new Point( _unshiftableShift = new Point(
_unshiftableShift.X + _pendingViewportShift.X, _unshiftableShift.X + _pendingViewportShift.X,
@ -306,8 +318,11 @@ namespace Avalonia.Controls
public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange) public void OnMakeAnchor(IControl anchor, bool isAnchorOutsideRealizedRange)
{ {
_makeAnchorElement = anchor; if (_makeAnchorElement != anchor)
_isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange; {
_makeAnchorElement = anchor;
_isAnchorOutsideRealizedRange = isAnchorOutsideRealizedRange;
}
} }
public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args) public void OnBringIntoViewRequested(RequestBringIntoViewEventArgs args)
@ -328,26 +343,28 @@ namespace Avalonia.Controls
// Make sure that only the target child can be the anchor during the bring into view operation. // Make sure that only the target child can be the anchor during the bring into view operation.
foreach (var child in _owner.Children) foreach (var child in _owner.Children)
{ {
////if (child.CanBeScrollAnchor && child != targetChild) if (child != targetChild)
////{ {
//// child.CanBeScrollAnchor = false; _scroller.UnregisterAnchorCandidate(child);
////} }
} }
// Register to rendering event to go back to how things were before where any child can be the anchor. // Register action to go back to how things were before where any child can be the anchor. Here,
_isBringIntoViewInProgress = true; // WinUI uses CompositionTarget.Rendering but we don't currently have that, so post an action to
////if (!m_renderingToken) // run *after* rendering has completed (priority needs to be lower than Render as Transformed
////{ // bounds must have been set in order for OnEffectiveViewportChanged to trigger).
//// winrt::Windows::UI::Xaml::Media::CompositionTarget compositionTarget{ nullptr }; if (!_isBringIntoViewInProgress)
//// m_renderingToken = compositionTarget.Rendering(winrt::auto_revoke, { this, &ViewportManagerWithPlatformFeatures::OnCompositionTargetRendering }); {
////} _isBringIntoViewInProgress = true;
Dispatcher.UIThread.Post(OnCompositionTargetRendering, DispatcherPriority.Loaded);
}
} }
} }
private IControl GetImmediateChildOfRepeater(IControl descendant) private IControl GetImmediateChildOfRepeater(IControl descendant)
{ {
var targetChild = descendant; var targetChild = descendant;
var parent = descendant.Parent; var parent = (IControl)descendant.VisualParent;
while (parent != null && parent != _owner) while (parent != null && parent != _owner)
{ {
targetChild = parent; targetChild = parent;
@ -362,26 +379,50 @@ namespace Avalonia.Controls
return targetChild; return targetChild;
} }
public void ResetScrollers() private void OnCompositionTargetRendering()
{ {
_scroller = null; _isBringIntoViewInProgress = false;
_effectiveViewportChangedRevoker?.Dispose(); _makeAnchorElement = null;
_effectiveViewportChangedRevoker = null;
_ensuredScroller = false; if (_scroller is object)
{
foreach (var child in _owner.Children)
{
var info = ItemsRepeater.GetVirtualizationInfo(child);
if (info.IsRealized && info.IsHeldByLayout)
{
_scroller.RegisterAnchorCandidate(child);
}
}
}
// HACK: Invalidate measure now that the anchor has been removed so that a layout can be
// done with a proper realization rect. This is a hack not present upstream to try to fix
// https://github.com/microsoft/microsoft-ui-xaml/issues/1422
TryInvalidateMeasure();
} }
private void OnEffectiveViewportChanged(TransformedBounds? bounds) public void ResetScrollers()
{ {
if (!bounds.HasValue) if (_scroller is object)
{ {
return; foreach (var child in _owner.Children)
{
_scroller.UnregisterAnchorCandidate(child);
}
_scroller = null;
} }
var globalClip = bounds.Value.Clip; _effectiveViewportChangedRevoker?.Dispose();
var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value; _effectiveViewportChangedRevoker = null;
var clip = globalClip.TransformToAABB(transform); _ensuredScroller = false;
var effectiveViewport = clip.Intersect(bounds.Value.Bounds); }
private void OnEffectiveViewportChanged(Rect effectiveViewport)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: EffectiveViewportChanged event callback", _owner.Layout.LayoutId);
UpdateViewport(effectiveViewport); UpdateViewport(effectiveViewport);
_pendingViewportShift = default; _pendingViewportShift = default;
@ -397,6 +438,7 @@ namespace Avalonia.Controls
if (_layoutUpdatedSubscribed) if (_layoutUpdatedSubscribed)
{ {
_owner.LayoutUpdated -= OnLayoutUpdated; _owner.LayoutUpdated -= OnLayoutUpdated;
_layoutUpdatedSubscribed = false;
} }
} }
@ -437,10 +479,17 @@ namespace Avalonia.Controls
private void UpdateViewport(Rect viewport) private void UpdateViewport(Rect viewport)
{ {
var currentVisibleWindow = viewport; var currentVisibleWindow = viewport;
var previousVisibleWindow = _visibleWindow;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Effective Viewport: ({Before})->({After})",
_owner.Layout.LayoutId,
previousVisibleWindow,
viewport);
if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X && if (-currentVisibleWindow.X <= ItemsRepeater.ClearedElementsArrangePosition.X &&
-currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y) -currentVisibleWindow.Y <= ItemsRepeater.ClearedElementsArrangePosition.Y)
{ {
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Viewport is invalid. visible window cleared", _owner.Layout.LayoutId);
// We got cleared. // We got cleared.
_visibleWindow = default; _visibleWindow = default;
} }
@ -449,7 +498,14 @@ namespace Avalonia.Controls
_visibleWindow = currentVisibleWindow; _visibleWindow = currentVisibleWindow;
} }
TryInvalidateMeasure(); if (_visibleWindow != previousVisibleWindow)
{
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Used Viewport: ({Before})->({After})",
_owner.Layout.LayoutId,
previousVisibleWindow,
currentVisibleWindow);
TryInvalidateMeasure();
}
} }
private static void ValidateCacheLength(double cacheLength) private static void ValidateCacheLength(double cacheLength)
@ -468,6 +524,7 @@ namespace Avalonia.Controls
// We invalidate measure instead of just invalidating arrange because // We invalidate measure instead of just invalidating arrange because
// we don't invalidate measure in UpdateViewport if the view is changing to // we don't invalidate measure in UpdateViewport if the view is changing to
// avoid layout cycles. // avoid layout cycles.
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Invalidating measure due to viewport change", _owner.Layout.LayoutId);
_owner.InvalidateMeasure(); _owner.InvalidateMeasure();
} }
} }
@ -476,16 +533,32 @@ namespace Avalonia.Controls
{ {
// HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater - // HACK: This is a bit of a hack. We need the effective viewport of the ItemsRepeater -
// we can get this from TransformedBounds, but this property is updated after layout has // we can get this from TransformedBounds, but this property is updated after layout has
// run, resulting in the UI being updated too late when scrolling quickly. We can // run, which is too late. Instead, for now lets just hook into an internal event on
// partially remedey this by triggering also on Bounds changes, but this won't work so // ScrollContentPresenter to find out what the offset and viewport will be after arrange
// well for nested ItemsRepeaters. // and use those values. Note that this doesn't handle nested ScrollViewers at all, but
// it's enough to get scrolling to non-uniformly sized items working for now.
// //
// UWP uses the EffectiveBoundsChanged event (which I think was implemented specially // UWP uses the EffectiveViewportChanged event (which I think was implemented specially
// for this case): we need to implement that in Avalonia. // for this case): we need to implement that in Avalonia, but the semantics of it aren't
return control.GetObservable(Visual.TransformedBoundsProperty) // clear to me. Hopefully the source for this event will be released with WinUI 3.
.Merge(control.GetObservable(Visual.BoundsProperty).Select(_ => control.TransformedBounds)) if (control.VisualParent is ScrollContentPresenter scp)
.Skip(1) {
.Subscribe(OnEffectiveViewportChanged); scp.PreArrange += ScrollContentPresenterPreArrange;
return Disposable.Create(() => scp.PreArrange -= ScrollContentPresenterPreArrange);
}
return Disposable.Empty;
}
private void ScrollContentPresenterPreArrange(object sender, VectorEventArgs e)
{
var scp = (ScrollContentPresenter)sender;
var effectiveViewport = new Rect((Point)scp.Offset, new Size(e.Vector.X, e.Vector.Y));
if (effectiveViewport != _visibleWindow)
{
OnEffectiveViewportChanged(effectiveViewport);
}
} }
private class ScrollerInfo private class ScrollerInfo

1
src/Avalonia.Controls/Repeater/VirtualizationInfo.cs

@ -36,6 +36,7 @@ namespace Avalonia.Controls
public bool IsHeldByLayout => Owner == ElementOwner.Layout; public bool IsHeldByLayout => Owner == ElementOwner.Layout;
public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool; public bool IsRealized => IsHeldByLayout || Owner == ElementOwner.PinnedPool;
public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool; public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool;
public bool MustClearDataContext { get; set; }
public bool KeepAlive { get; set; } public bool KeepAlive { get; set; }
public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory; public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory;
public string UniqueId { get; private set; } public string UniqueId { get; private set; }

37
src/Avalonia.Controls/ScrollViewer.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Security.Authentication.ExtendedProtection;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
@ -244,9 +245,7 @@ namespace Avalonia.Controls
set set
{ {
value = ValidateOffset(this, value); if (SetAndRaise(OffsetProperty, ref _offset, CoerceOffset(Extent, Viewport, value)))
if (SetAndRaise(OffsetProperty, ref _offset, value))
{ {
CalculatedPropertiesChanged(); CalculatedPropertiesChanged();
} }
@ -316,6 +315,9 @@ namespace Avalonia.Controls
get { return VerticalScrollBarVisibility != ScrollBarVisibility.Disabled; } get { return VerticalScrollBarVisibility != ScrollBarVisibility.Disabled; }
} }
/// <inheritdoc/>
public IControl CurrentAnchor => (Presenter as IScrollAnchorProvider)?.CurrentAnchor;
/// <summary> /// <summary>
/// Gets the maximum horizontal scrollbar value. /// Gets the maximum horizontal scrollbar value.
/// </summary> /// </summary>
@ -382,9 +384,6 @@ namespace Avalonia.Controls
get { return _viewport.Height; } get { return _viewport.Height; }
} }
/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
/// <summary> /// <summary>
/// Scrolls the content up one line. /// Scrolls the content up one line.
/// </summary> /// </summary>
@ -473,14 +472,16 @@ namespace Avalonia.Controls
control.SetValue(VerticalScrollBarVisibilityProperty, value); control.SetValue(VerticalScrollBarVisibilityProperty, value);
} }
void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element) /// <inheritdoc/>
public void RegisterAnchorCandidate(IControl element)
{ {
// TODO: Implement (Presenter as IScrollAnchorProvider)?.RegisterAnchorCandidate(element);
} }
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) /// <inheritdoc/>
public void UnregisterAnchorCandidate(IControl element)
{ {
// TODO: Implement (Presenter as IScrollAnchorProvider)?.UnregisterAnchorCandidate(element);
} }
protected override bool RegisterContentPresenter(IContentPresenter presenter) protected override bool RegisterContentPresenter(IContentPresenter presenter)
@ -517,22 +518,6 @@ namespace Avalonia.Controls
return double.IsNaN(result) ? 0 : result; return double.IsNaN(result) ? 0 : result;
} }
private static Vector ValidateOffset(AvaloniaObject o, Vector value)
{
ScrollViewer scrollViewer = o as ScrollViewer;
if (scrollViewer != null)
{
var extent = scrollViewer.Extent;
var viewport = scrollViewer.Viewport;
return CoerceOffset(extent, viewport, value);
}
else
{
return value;
}
}
private void ChildChanged(IControl child) private void ChildChanged(IControl child)
{ {
if (_logicalScrollable is object) if (_logicalScrollable is object)

2
src/Avalonia.Layout/AttachedLayout.cs

@ -12,7 +12,7 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
public abstract class AttachedLayout : AvaloniaObject public abstract class AttachedLayout : AvaloniaObject
{ {
internal string LayoutId { get; set; } public string LayoutId { get; set; }
/// <summary> /// <summary>
/// Occurs when the measurement state (layout) has been invalidated. /// Occurs when the measurement state (layout) has been invalidated.

4
src/Avalonia.Layout/ElementManager.cs

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Layout.Utils; using Avalonia.Layout.Utils;
using Avalonia.Logging;
namespace Avalonia.Layout namespace Avalonia.Layout
{ {
@ -78,6 +79,7 @@ namespace Avalonia.Layout
{ {
// Sentinel. Create the element now since we need it. // Sentinel. Create the element now since we need it.
int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex); int dataIndex = GetDataIndexFromRealizedRangeIndex(realizedIndex);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "Creating element for sentinal with data index {Index}", dataIndex);
element = _context.GetOrCreateElementAt( element = _context.GetOrCreateElementAt(
dataIndex, dataIndex,
ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
@ -232,6 +234,8 @@ namespace Avalonia.Layout
{ {
Insert(0, dataIndex, element); Insert(0, dataIndex, element);
} }
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Created element for index {index}", layoutId, dataIndex);
} }
} }

65
src/Avalonia.Layout/FlowLayoutAlgorithm.cs

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Logging;
namespace Avalonia.Layout namespace Avalonia.Layout
{ {
@ -82,6 +83,9 @@ namespace Avalonia.Layout
// If minor size is infinity, there is only one line and no need to align that line. // If minor size is infinity, there is only one line and no need to align that line.
_scrollOrientationSameAsFlow = double.IsInfinity(_orientation.Minor(availableSize)); _scrollOrientationSameAsFlow = double.IsInfinity(_orientation.Minor(availableSize));
var realizationRect = RealizationRect; var realizationRect = RealizationRect;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: MeasureLayout Realization({Rect})",
layoutId,
realizationRect);
var suggestedAnchorIndex = _context.RecommendedAnchorIndex; var suggestedAnchorIndex = _context.RecommendedAnchorIndex;
if (_elementManager.IsIndexValidInData(suggestedAnchorIndex)) if (_elementManager.IsIndexValidInData(suggestedAnchorIndex))
@ -100,6 +104,7 @@ namespace Avalonia.Layout
Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId); Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
if (isWrapping && IsReflowRequired()) if (isWrapping && IsReflowRequired())
{ {
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Reflow Pass", layoutId);
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
_orientation.SetMinorStart(ref firstElementBounds, 0); _orientation.SetMinorStart(ref firstElementBounds, 0);
_elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds); _elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
@ -121,6 +126,7 @@ namespace Avalonia.Layout
LineAlignment lineAlignment, LineAlignment lineAlignment,
string layoutId) string layoutId)
{ {
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: ArrangeLayout", layoutId);
ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId); ArrangeVirtualizingLayout(finalSize, lineAlignment, isWrapping, layoutId);
return new Size( return new Size(
@ -184,6 +190,7 @@ namespace Avalonia.Layout
if (isAnchorSuggestionValid) if (isAnchorSuggestionValid)
{ {
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Using suggested anchor {Anchor}", layoutId, suggestedAnchorIndex);
anchorIndex = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement( anchorIndex = _algorithmCallbacks.Algorithm_GetAnchorForTargetElement(
suggestedAnchorIndex, suggestedAnchorIndex,
availableSize, availableSize,
@ -223,6 +230,9 @@ namespace Avalonia.Layout
} }
else if (needAnchorColumnRevaluation || !isRealizationWindowConnected) else if (needAnchorColumnRevaluation || !isRealizationWindowConnected)
{ {
if (needAnchorColumnRevaluation) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: NeedAnchorColumnReevaluation", layoutId); }
if (!isRealizationWindowConnected) { Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Disconnected Window", layoutId); }
// The anchor is based on the realization window because a connected ItemsRepeater might intersect the realization window // The anchor is based on the realization window because a connected ItemsRepeater might intersect the realization window
// but not the visible window. In that situation, we still need to produce a valid anchor. // but not the visible window. In that situation, we still need to produce a valid anchor.
var anchorInfo = _algorithmCallbacks.Algorithm_GetAnchorForRealizationRect(availableSize, context); var anchorInfo = _algorithmCallbacks.Algorithm_GetAnchorForRealizationRect(availableSize, context);
@ -231,6 +241,7 @@ namespace Avalonia.Layout
} }
else else
{ {
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Connected Window - picking first realized element as anchor", layoutId);
// No suggestion - just pick first in realized range // No suggestion - just pick first in realized range
anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0); anchorIndex = _elementManager.GetDataIndexFromRealizedRangeIndex(0);
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0); var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
@ -238,12 +249,14 @@ namespace Avalonia.Layout
} }
} }
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Picked anchor: {Anchor}", layoutId, anchorIndex);
_firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex; _firstRealizedDataIndexInsideRealizationWindow = _lastRealizedDataIndexInsideRealizationWindow = anchorIndex;
if (_elementManager.IsIndexValidInData(anchorIndex)) if (_elementManager.IsIndexValidInData(anchorIndex))
{ {
if (!_elementManager.IsDataIndexRealized(anchorIndex)) if (!_elementManager.IsDataIndexRealized(anchorIndex))
{ {
// Disconnected, throw everything and create new anchor // Disconnected, throw everything and create new anchor
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Disconnected Window - throwing away all realized elements", layoutId);
_elementManager.ClearRealizedRange(); _elementManager.ClearRealizedRange();
var anchor = _context.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle); var anchor = _context.GetOrCreateElementAt(anchorIndex, ElementRealizationOptions.ForceCreate | ElementRealizationOptions.SuppressAutoRecycle);
@ -254,9 +267,17 @@ namespace Avalonia.Layout
var desiredSize = MeasureElement(anchorElement, anchorIndex, availableSize, _context); var desiredSize = MeasureElement(anchorElement, anchorIndex, availableSize, _context);
var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height); var layoutBounds = new Rect(anchorPosition.X, anchorPosition.Y, desiredSize.Width, desiredSize.Height);
_elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds); _elementManager.SetLayoutBoundsForDataIndex(anchorIndex, layoutBounds);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout bounds of anchor {anchor} are ({Bounds})",
layoutId,
anchorIndex,
layoutBounds);
} }
else else
{ {
// Throw everything away
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Anchor index is not valid - throwing away all realized elements",
layoutId);
_elementManager.ClearRealizedRange(); _elementManager.ClearRealizedRange();
} }
@ -280,6 +301,12 @@ namespace Avalonia.Layout
if (anchorIndex != -1) if (anchorIndex != -1)
{ {
int step = (direction == GenerateDirection.Forward) ? 1 : -1; int step = (direction == GenerateDirection.Forward) ? 1 : -1;
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Generating {Direction} from anchor {Anchor}",
layoutId,
direction,
anchorIndex);
int previousIndex = anchorIndex; int previousIndex = anchorIndex;
int currentIndex = anchorIndex + step; int currentIndex = anchorIndex + step;
var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex); var anchorBounds = _elementManager.GetLayoutBoundsForDataIndex(anchorIndex);
@ -365,6 +392,10 @@ namespace Avalonia.Layout
_orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing); _orientation.SetMajorStart(ref bounds, previousLineOffset - lineMajorSize - lineSpacing);
_orientation.SetMajorSize(ref bounds, lineMajorSize); _orientation.SetMajorSize(ref bounds, lineMajorSize);
_elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds); _elementManager.SetLayoutBoundsForDataIndex(dataIndex, bounds);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Corrected Layout bounds of element {Index} are ({Bounds})",
layoutId,
dataIndex,
bounds);
} }
} }
} }
@ -387,6 +418,11 @@ namespace Avalonia.Layout
} }
_elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds); _elementManager.SetLayoutBoundsForDataIndex(currentIndex, currentBounds);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Layout bounds of element {Index} are ({Bounds}).",
layoutId,
currentIndex,
currentBounds);
previousIndex = currentIndex; previousIndex = currentIndex;
currentIndex += step; currentIndex += step;
} }
@ -394,20 +430,17 @@ namespace Avalonia.Layout
// If we did not reach the top or bottom of the extent, we realized one // If we did not reach the top or bottom of the extent, we realized one
// extra item before we knew we were outside the realization window. Do not // extra item before we knew we were outside the realization window. Do not
// account for that element in the indicies inside the realization window. // account for that element in the indicies inside the realization window.
if (count > 0) if (direction == GenerateDirection.Forward)
{ {
if (direction == GenerateDirection.Forward) int dataCount = _context.ItemCount;
{ _lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1;
int dataCount = _context.ItemCount; _lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow);
_lastRealizedDataIndexInsideRealizationWindow = previousIndex == dataCount - 1 ? dataCount - 1 : previousIndex - 1; }
_lastRealizedDataIndexInsideRealizationWindow = Math.Max(0, _lastRealizedDataIndexInsideRealizationWindow); else
} {
else int dataCount = _context.ItemCount;
{ _firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1;
int dataCount = _context.ItemCount; _firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow);
_firstRealizedDataIndexInsideRealizationWindow = previousIndex == 0 ? 0 : previousIndex + 1;
_firstRealizedDataIndexInsideRealizationWindow = Math.Min(dataCount - 1, _firstRealizedDataIndexInsideRealizationWindow);
}
} }
_elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex); _elementManager.DiscardElementsOutsideWindow(direction == GenerateDirection.Forward, currentIndex);
@ -508,6 +541,7 @@ namespace Avalonia.Layout
lastDataIndex, lastDataIndex,
lastBounds); lastBounds);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId} Extent: ({Bounds})", layoutId, extent);
return extent; return extent;
} }
@ -676,6 +710,11 @@ namespace Avalonia.Layout
} }
var element = _elementManager.GetAt(rangeIndex); var element = _elementManager.GetAt(rangeIndex);
Logger.TryGet(LogEventLevel.Verbose, "Repeater")?.Log(this, "{LayoutId}: Arranging element {Index} at ({Bounds})",
layoutId,
_elementManager.GetDataIndexFromRealizedRangeIndex(rangeIndex),
bounds);
element.Arrange(bounds); element.Arrange(bounds);
} }
} }

2
src/Avalonia.Layout/LayoutQueue.cs

@ -77,7 +77,7 @@ namespace Avalonia.Layout
{ {
if (_shouldEnqueue(item.Key)) if (_shouldEnqueue(item.Key))
{ {
_loopQueueInfo[item.Key] = new Info() { Active = true, Count = item.Value.Count + 1 }; _loopQueueInfo[item.Key] = new Info() { Active = true, Count = 0 };
_inner.Enqueue(item.Key); _inner.Enqueue(item.Key);
} }
} }

4
src/Avalonia.Layout/NonVirtualizingStackLayout.cs

@ -112,7 +112,9 @@ namespace Avalonia.Layout
u = (isVertical ? bounds.Bottom : bounds.Right) + spacing; u = (isVertical ? bounds.Bottom : bounds.Right) + spacing;
} }
return new Size(bounds.Right, bounds.Bottom); return new Size(
Math.Max(finalSize.Width, bounds.Width),
Math.Max(finalSize.Height, bounds.Height));
} }
private static Rect LayoutVertical(ILayoutable element, double y, Size constraint) private static Rect LayoutVertical(ILayoutable element, double y, Size constraint)

8
src/Avalonia.Layout/StackLayout.cs

@ -6,6 +6,7 @@
using System; using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Logging;
namespace Avalonia.Layout namespace Avalonia.Layout
{ {
@ -107,8 +108,15 @@ namespace Avalonia.Layout
_orientation.MajorStart(extent) + _orientation.MajorStart(extent) +
(remainingItems * averageElementSize)); (remainingItems * averageElementSize));
} }
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 average {Average}",
LayoutId, extent.Size, averageElementSize);
return extent; return extent;
} }

7
src/Avalonia.Layout/UniformGridLayout.cs

@ -6,6 +6,7 @@
using System; using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Logging;
namespace Avalonia.Layout namespace Avalonia.Layout
{ {
@ -379,8 +380,14 @@ namespace Avalonia.Layout
ref extent, ref extent,
_orientation.MajorEnd(lastRealizedLayoutBounds) - _orientation.MajorStart(extent) + (remainingItems / itemsPerLine) * lineSize); _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; return extent;
} }

8
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs

@ -117,13 +117,17 @@ namespace Avalonia.Controls.UnitTests.Presenters
Width = 150, Width = 150,
Height = 150, Height = 150,
}, },
Offset = new Vector(25, 25),
}; };
target.UpdateChild(); target.UpdateChild();
target.Measure(new Size(100, 100)); target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); target.Arrange(new Rect(0, 0, 100, 100));
target.Offset = new Vector(25, 25);
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(-25, -25, 150, 150), content.Bounds); Assert.Equal(new Rect(-25, -25, 150, 150), content.Bounds);
} }
@ -345,4 +349,4 @@ namespace Avalonia.Controls.UnitTests.Presenters
} }
} }
} }
} }

6
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs

@ -59,13 +59,17 @@ namespace Avalonia.Controls.UnitTests
CanHorizontallyScroll = true, CanHorizontallyScroll = true,
CanVerticallyScroll = true, CanVerticallyScroll = true,
Content = scrollable, Content = scrollable,
Offset = new Vector(25, 25),
}; };
target.UpdateChild(); target.UpdateChild();
target.Measure(new Size(100, 100)); target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100)); target.Arrange(new Rect(0, 0, 100, 100));
target.Offset = new Vector(25, 25);
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
Assert.Equal(new Rect(-25, -25, 150, 150), scrollable.Bounds); Assert.Equal(new Rect(-25, -25, 150, 150), scrollable.Bounds);
} }

Loading…
Cancel
Save