Browse Source

Tweak ScrollContentPresenter anchoring.

Using WinUI's `ScrollPresenter` implementation to guide us.
pull/4814/head
Steven Kirk 6 years ago
parent
commit
e9c990d908
  1. 110
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

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

@ -16,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>
@ -70,7 +72,9 @@ namespace Avalonia.Controls.Presenters
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.
@ -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;
} }
} }
@ -266,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;
} }
@ -493,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;
@ -526,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)

Loading…
Cancel
Save