diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs
index 6b5cb2ee25..93f3a0abb8 100644
--- a/src/Avalonia.Controls/IScrollAnchorProvider.cs
+++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs
@@ -1,9 +1,29 @@
namespace Avalonia.Controls
{
+ ///
+ /// Specifies a contract for a scrolling control that supports scroll anchoring.
+ ///
public interface IScrollAnchorProvider
{
+ ///
+ /// The currently chosen anchor element to use for scroll anchoring.
+ ///
IControl CurrentAnchor { get; }
+
+ ///
+ /// Registers a control as a potential scroll anchor candidate.
+ ///
+ ///
+ /// A control within the subtree of the .
+ ///
void RegisterAnchorCandidate(IControl element);
+
+ ///
+ /// Unregisters a control as a potential scroll anchor candidate.
+ ///
+ ///
+ /// A control within the subtree of the .
+ ///
void UnregisterAnchorCandidate(IControl element);
}
}
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 98a5b10023..5dae0043e1 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -14,7 +14,7 @@ namespace Avalonia.Controls.Presenters
///
/// Presents a scrolling view of content inside a .
///
- public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable
+ public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
{
///
/// Defines the property.
@@ -65,6 +65,8 @@ namespace Avalonia.Controls.Presenters
private IDisposable _logicalScrollSubscription;
private Size _viewport;
private Dictionary _activeLogicalGestureScrolls;
+ private List _anchorCandidates;
+ private (IControl control, Rect bounds) _anchor;
///
/// Initializes static members of the class.
@@ -132,6 +134,9 @@ namespace Avalonia.Controls.Presenters
private set { SetAndRaise(ViewportProperty, ref _viewport, value); }
}
+ ///
+ IControl IScrollAnchorProvider.CurrentAnchor => _anchor.Item1;
+
///
/// Attempts to bring a portion of the target visual into view by scrolling the content.
///
@@ -196,6 +201,24 @@ namespace Avalonia.Controls.Presenters
return result;
}
+ ///
+ void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element)
+ {
+ _anchorCandidates ??= new List();
+ _anchorCandidates.Add(element);
+ }
+
+ ///
+ void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
+ {
+ _anchorCandidates?.Remove(element);
+
+ if (_anchor.Item1 == element)
+ {
+ _anchor = default;
+ }
+ }
+
///
protected override Size MeasureOverride(Size availableSize)
{
@@ -223,9 +246,44 @@ namespace Avalonia.Controls.Presenters
var size = new Size(
CanHorizontallyScroll ? Math.Max(Child.DesiredSize.Width, finalSize.Width) : finalSize.Width,
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)
+ {
+ return updatedBounds.Position - _anchor.bounds.Position;
+ }
+
+ return default;
+ }
+
+ // Check whether our previous anchor (if any) has moved relative to Child since the last
+ // arrange, and if so adjust the offset to bring it back into place.
+ Offset += TrackAnchor();
+
+ // Calculate the new anchor element.
+ _anchor = CalculateCurrentAnchor();
+
+ // Do the arrange.
ArrangeOverrideImpl(size, -Offset);
+
+ // If the anchor moved during the arrange, we need to adjust the offset and do another arrange.
+ var postOffset = TrackAnchor();
+
+ if (postOffset != default)
+ {
+ Offset += postOffset;
+ ArrangeOverrideImpl(size, -Offset);
+ }
+
Viewport = finalSize;
Extent = Child.Bounds.Size.Inflate(Child.Margin);
+
return finalSize;
}
@@ -390,5 +448,70 @@ namespace Avalonia.Controls.Presenters
Offset = scrollable.Offset;
}
}
+
+ (IControl, Rect) CalculateCurrentAnchor()
+ {
+ if (_anchorCandidates == null)
+ {
+ return default;
+ }
+
+ var thisBounds = new Rect(Bounds.Size);
+ 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 &&
+ TranslateBounds(element, this, out var bounds) &&
+ bounds.Intersects(thisBounds))
+ {
+ 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 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;
+ }
}
}
diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs
index b1af4f0ecc..7f7b56909e 100644
--- a/src/Avalonia.Controls/Repeater/ViewportManager.cs
+++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs
@@ -236,14 +236,12 @@ namespace Avalonia.Controls
public void OnElementPrepared(IControl element)
{
- // If we have an anchor element, we do not want the
- // scroll anchor provider to start anchoring some other element.
- ////element.CanBeScrollAnchor(true);
+ _scroller.RegisterAnchorCandidate(element);
}
- public void OnElementCleared(ILayoutable element)
+ public void OnElementCleared(IControl element)
{
- ////element.CanBeScrollAnchor(false);
+ _scroller.UnregisterAnchorCandidate(element);
}
public void OnOwnerMeasuring()
diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs
index 058c11c9ee..f2c6f85996 100644
--- a/src/Avalonia.Controls/ScrollViewer.cs
+++ b/src/Avalonia.Controls/ScrollViewer.cs
@@ -316,6 +316,9 @@ namespace Avalonia.Controls
get { return VerticalScrollBarVisibility != ScrollBarVisibility.Disabled; }
}
+ ///
+ public IControl CurrentAnchor => (Presenter as IScrollAnchorProvider)?.CurrentAnchor;
+
///
/// Gets the maximum horizontal scrollbar value.
///
@@ -382,9 +385,6 @@ namespace Avalonia.Controls
get { return _viewport.Height; }
}
- ///
- IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
-
///
/// Scrolls to the top-left corner of the content.
///
@@ -441,14 +441,16 @@ namespace Avalonia.Controls
control.SetValue(VerticalScrollBarVisibilityProperty, value);
}
- void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element)
+ ///
+ public void RegisterAnchorCandidate(IControl element)
{
- // TODO: Implement
+ (Presenter as IScrollAnchorProvider)?.RegisterAnchorCandidate(element);
}
- void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
+ ///
+ public void UnregisterAnchorCandidate(IControl element)
{
- // TODO: Implement
+ (Presenter as IScrollAnchorProvider)?.UnregisterAnchorCandidate(element);
}
protected override bool RegisterContentPresenter(IContentPresenter presenter)