Browse Source

Merge pull request #4814 from AvaloniaUI/fixes/4805-scrollcontentpresenter-anchoring

Fix ScrollContentPresenter/ItemsRepeater anchoring
pull/4870/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
7fe819b0f7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      src/Avalonia.Controls/IScrollAnchorProvider.cs
  2. 5
      src/Avalonia.Controls/Panel.cs
  3. 137
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  4. 18
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  5. 2
      src/Avalonia.Controls/Repeater/ViewManager.cs
  6. 14
      src/Avalonia.Controls/Repeater/ViewportManager.cs
  7. 1
      src/Avalonia.Controls/Repeater/VirtualizationInfo.cs
  8. 2
      src/Avalonia.Layout/StackLayout.cs

6
src/Avalonia.Controls/IScrollAnchorProvider.cs

@ -1,4 +1,6 @@
namespace Avalonia.Controls #nullable enable
namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Specifies a contract for a scrolling control that supports scroll anchoring. /// Specifies a contract for a scrolling control that supports scroll anchoring.
@ -8,7 +10,7 @@
/// <summary> /// <summary>
/// The currently chosen anchor element to use for scroll anchoring. /// The currently chosen anchor element to use for scroll anchoring.
/// </summary> /// </summary>
IControl CurrentAnchor { get; } IControl? CurrentAnchor { get; }
/// <summary> /// <summary>
/// Registers a control as a potential scroll anchor candidate. /// Registers a control as a potential scroll anchor candidate.

5
src/Avalonia.Controls/Panel.cs

@ -137,6 +137,11 @@ namespace Avalonia.Controls
throw new NotSupportedException(); throw new NotSupportedException();
} }
InvalidateMeasureOnChildrenChanged();
}
private protected virtual void InvalidateMeasureOnChildrenChanged()
{
InvalidateMeasure(); InvalidateMeasure();
} }

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

@ -7,6 +7,8 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.VisualTree; using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
/// <summary> /// <summary>
@ -14,6 +16,8 @@ namespace Avalonia.Controls.Presenters
/// </summary> /// </summary>
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
{ {
private const double EdgeDetectionTolerance = 0.1;
/// <summary> /// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property. /// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary> /// </summary>
@ -64,11 +68,13 @@ namespace Avalonia.Controls.Presenters
private bool _arranging; 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 List<IControl>? _anchorCandidates;
private (IControl control, Rect bounds) _anchor; private IControl? _anchorElement;
private Rect _anchorElementBounds;
private bool _isAnchorElementDirty;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class. /// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -90,8 +96,6 @@ 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>
@ -138,7 +142,14 @@ namespace Avalonia.Controls.Presenters
} }
/// <inheritdoc/> /// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => _anchor.control; IControl? IScrollAnchorProvider.CurrentAnchor
{
get
{
EnsureAnchorElementSelection();
return _anchorElement;
}
}
/// <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.
@ -215,16 +226,18 @@ namespace Avalonia.Controls.Presenters
_anchorCandidates ??= new List<IControl>(); _anchorCandidates ??= new List<IControl>();
_anchorCandidates.Add(element); _anchorCandidates.Add(element);
_isAnchorElementDirty = true;
} }
/// <inheritdoc/> /// <inheritdoc/>
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
{ {
_anchorCandidates?.Remove(element); _anchorCandidates?.Remove(element);
_isAnchorElementDirty = true;
if (_anchor.control == element) if (_anchorElement == element)
{ {
_anchor = default; _anchorElement = null;
} }
} }
@ -247,11 +260,6 @@ 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);
@ -271,59 +279,69 @@ namespace Avalonia.Controls.Presenters
// If we have an anchor and its position relative to Child has changed during the // 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 // arrange then that change wasn't just due to scrolling (as scrolling doesn't adjust
// relative positions within Child). // relative positions within Child).
if (_anchor.control != null && if (_anchorElement != null &&
TranslateBounds(_anchor.control, Child, out var updatedBounds) && TranslateBounds(_anchorElement, Child, out var updatedBounds) &&
updatedBounds.Position != _anchor.bounds.Position) updatedBounds.Position != _anchorElementBounds.Position)
{ {
var offset = updatedBounds.Position - _anchor.bounds.Position; var offset = updatedBounds.Position - _anchorElementBounds.Position;
return offset; return offset;
} }
return default; return default;
} }
// Calculate the new anchor element. var isAnchoring = Offset.X >= EdgeDetectionTolerance || Offset.Y >= EdgeDetectionTolerance;
_anchor = CalculateCurrentAnchor();
// Do the arrange. if (isAnchoring)
ArrangeOverrideImpl(size, -Offset); {
// Calculate the new anchor element if necessary.
EnsureAnchorElementSelection();
// If the anchor moved during the arrange, we need to adjust the offset and do another arrange. // Do the arrange.
var anchorShift = TrackAnchor(); ArrangeOverrideImpl(size, -Offset);
if (anchorShift != default) // If the anchor moved during the arrange, we need to adjust the offset and do another arrange.
{ var anchorShift = TrackAnchor();
var newOffset = Offset + anchorShift;
var newExtent = Extent;
var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height);
if (newOffset.X > maxOffset.X) if (anchorShift != default)
{ {
newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width); var newOffset = Offset + anchorShift;
} var newExtent = Extent;
var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height);
if (newOffset.Y > maxOffset.Y) if (newOffset.X > maxOffset.X)
{ {
newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height); newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width);
} }
Extent = newExtent; if (newOffset.Y > maxOffset.Y)
{
newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height);
}
try Extent = newExtent;
{
_arranging = true; try
Offset = newOffset; {
} _arranging = true;
finally Offset = newOffset;
{ }
_arranging = false; finally
{
_arranging = false;
}
ArrangeOverrideImpl(size, -Offset);
} }
}
else
{
ArrangeOverrideImpl(size, -Offset); ArrangeOverrideImpl(size, -Offset);
} }
Viewport = finalSize; Viewport = finalSize;
Extent = Child.Bounds.Size.Inflate(Child.Margin); Extent = Child.Bounds.Size.Inflate(Child.Margin);
_isAnchorElementDirty = true;
return finalSize; return finalSize;
} }
@ -350,7 +368,7 @@ namespace Avalonia.Controls.Presenters
{ {
var logicalUnits = delta.Y / LogicalScrollItemSize; var logicalUnits = delta.Y / LogicalScrollItemSize;
delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize); delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize);
dy = logicalUnits * scrollable.ScrollSize.Height; dy = logicalUnits * scrollable!.ScrollSize.Height;
} }
else else
dy = delta.Y; dy = delta.Y;
@ -368,7 +386,7 @@ namespace Avalonia.Controls.Presenters
{ {
var logicalUnits = delta.X / LogicalScrollItemSize; var logicalUnits = delta.X / LogicalScrollItemSize;
delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize); delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize);
dx = logicalUnits * scrollable.ScrollSize.Width; dx = logicalUnits * scrollable!.ScrollSize.Width;
} }
else else
dx = delta.X; dx = delta.X;
@ -405,7 +423,7 @@ namespace Avalonia.Controls.Presenters
if (Extent.Height > Viewport.Height) if (Extent.Height > Viewport.Height)
{ {
double height = isLogical ? scrollable.ScrollSize.Height : 50; double height = isLogical ? scrollable!.ScrollSize.Height : 50;
y += -e.Delta.Y * height; y += -e.Delta.Y * height;
y = Math.Max(y, 0); y = Math.Max(y, 0);
y = Math.Min(y, Extent.Height - Viewport.Height); y = Math.Min(y, Extent.Height - Viewport.Height);
@ -413,7 +431,7 @@ namespace Avalonia.Controls.Presenters
if (Extent.Width > Viewport.Width) if (Extent.Width > Viewport.Width)
{ {
double width = isLogical ? scrollable.ScrollSize.Width : 50; double width = isLogical ? scrollable!.ScrollSize.Width : 50;
x += -e.Delta.X * width; x += -e.Delta.X * width;
x = Math.Max(x, 0); x = Math.Max(x, 0);
x = Math.Min(x, Extent.Width - Viewport.Width); x = Math.Min(x, Extent.Width - Viewport.Width);
@ -441,7 +459,7 @@ namespace Avalonia.Controls.Presenters
private void ChildChanged(AvaloniaPropertyChangedEventArgs e) private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
{ {
UpdateScrollableSubscription((IControl)e.NewValue); UpdateScrollableSubscription((IControl?)e.NewValue);
if (e.OldValue != null) if (e.OldValue != null)
{ {
@ -449,7 +467,7 @@ namespace Avalonia.Controls.Presenters
} }
} }
private void UpdateScrollableSubscription(IControl child) private void UpdateScrollableSubscription(IControl? child)
{ {
var scrollable = child as ILogicalScrollable; var scrollable = child as ILogicalScrollable;
@ -498,13 +516,17 @@ namespace Avalonia.Controls.Presenters
} }
} }
private (IControl, Rect) CalculateCurrentAnchor() private void EnsureAnchorElementSelection()
{ {
if (_anchorCandidates == null) if (!_isAnchorElementDirty || _anchorCandidates is null)
{ {
return default; return;
} }
_anchorElement = null;
_anchorElementBounds = default;
_isAnchorElementDirty = false;
var bestCandidate = default(IControl); var bestCandidate = default(IControl);
var bestCandidateDistance = double.MaxValue; var bestCandidateDistance = double.MaxValue;
@ -531,10 +553,9 @@ namespace Avalonia.Controls.Presenters
// bounds aren't relative to the ScrollContentPresenter itself, if they change // bounds aren't relative to the ScrollContentPresenter itself, if they change
// then we know it wasn't just due to scrolling. // then we know it wasn't just due to scrolling.
var unscrolledBounds = TranslateBounds(bestCandidate, Child); var unscrolledBounds = TranslateBounds(bestCandidate, Child);
return (bestCandidate, unscrolledBounds); _anchorElement = bestCandidate;
_anchorElementBounds = unscrolledBounds;
} }
return default;
} }
private bool GetViewportBounds(IControl element, out Rect bounds) private bool GetViewportBounds(IControl element, out Rect bounds)

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

@ -267,6 +267,11 @@ namespace Avalonia.Controls
return result; return result;
} }
private protected override void InvalidateMeasureOnChildrenChanged()
{
// Don't invalidate measure when children change.
}
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
if (_isLayoutInProgress) if (_isLayoutInProgress)
@ -364,6 +369,12 @@ namespace Avalonia.Controls
{ {
var newBounds = element.Bounds; var newBounds = element.Bounds;
virtInfo.ArrangeBounds = newBounds; virtInfo.ArrangeBounds = newBounds;
if (!virtInfo.IsRegisteredAsAnchorCandidate)
{
_viewportManager.RegisterScrollAnchorCandidate(element);
virtInfo.IsRegisteredAsAnchorCandidate = true;
}
} }
} }
@ -515,11 +526,14 @@ namespace Avalonia.Controls
return element; return element;
} }
internal void OnElementPrepared(IControl element, int index) internal void OnElementPrepared(IControl element, VirtualizationInfo virtInfo)
{ {
_viewportManager.OnElementPrepared(element); _viewportManager.OnElementPrepared(element, virtInfo);
if (ElementPrepared != null) if (ElementPrepared != null)
{ {
var index = virtInfo.Index;
if (_elementPreparedArgs == null) if (_elementPreparedArgs == null)
{ {
_elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index); _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index);

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

@ -661,7 +661,7 @@ namespace Avalonia.Controls
children.Add(element); children.Add(element);
} }
repeater.OnElementPrepared(element, index); repeater.OnElementPrepared(element, virtInfo);
// Update realized indices // Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);

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

@ -240,9 +240,14 @@ namespace Avalonia.Controls
} }
} }
public void OnElementPrepared(IControl element) public void OnElementPrepared(IControl element, VirtualizationInfo virtInfo)
{ {
_scroller?.RegisterAnchorCandidate(element); // WinUI registers the element as an anchor candidate here, but I feel that's in error:
// at this point the element has not yet been positioned by the arrange pass so it will
// have its previous position, meaning that when the arrange pass moves it into its new
// position, an incorrect scroll anchoring will occur. Instead signal that it's not yet
// registered as a scroll anchor candidate.
virtInfo.IsRegisteredAsAnchorCandidate = false;
} }
public void OnElementCleared(IControl element) public void OnElementCleared(IControl element)
@ -373,6 +378,11 @@ namespace Avalonia.Controls
} }
} }
public void RegisterScrollAnchorCandidate(IControl element)
{
_scroller?.RegisterAnchorCandidate(element);
}
private IControl GetImmediateChildOfRepeater(IControl descendant) private IControl GetImmediateChildOfRepeater(IControl descendant)
{ {
var targetChild = descendant; var targetChild = descendant;

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

@ -38,6 +38,7 @@ namespace Avalonia.Controls
public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool; public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool;
public bool MustClearDataContext { get; set; } public bool MustClearDataContext { get; set; }
public bool KeepAlive { get; set; } public bool KeepAlive { get; set; }
public bool IsRegisteredAsAnchorCandidate { 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; }

2
src/Avalonia.Layout/StackLayout.cs

@ -249,8 +249,8 @@ namespace Avalonia.Layout
realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize) realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize)
{ {
anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize); anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize);
offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent);
anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex)); anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex));
offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent);
} }
} }

Loading…
Cancel
Save