Skip to content
Permalink
Browse files

Initial implementation of IScrollAnchorProvider.

On `ScrollContentPresenter`.
  • Loading branch information
grokys committed Jan 13, 2020
1 parent 155e9d4 commit 7b4431302a7582ab02090b25af00fc8ff3979b17
@@ -1,9 +1,29 @@
namespace Avalonia.Controls
{
/// <summary>
/// Specifies a contract for a scrolling control that supports scroll anchoring.
/// </summary>
public interface IScrollAnchorProvider
{
/// <summary>
/// The currently chosen anchor element to use for scroll anchoring.
/// </summary>
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);

/// <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);
}
}
@@ -15,7 +15,7 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Presents a scrolling view of content inside a <see cref="ScrollViewer"/>.
/// </summary>
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable
public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider
{
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
@@ -66,6 +66,8 @@ public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable
private IDisposable _logicalScrollSubscription;
private Size _viewport;
private Dictionary<int, Vector> _activeLogicalGestureScrolls;
private List<IControl> _anchorCandidates;
private (IControl control, Rect bounds) _anchor;

/// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@@ -133,6 +135,9 @@ public Size Viewport
private set { SetAndRaise(ViewportProperty, ref _viewport, value); }
}

/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => _anchor.Item1;

/// <summary>
/// Attempts to bring a portion of the target visual into view by scrolling the content.
/// </summary>
@@ -197,6 +202,24 @@ public bool BringDescendantIntoView(IVisual target, Rect targetRect)
return result;
}

/// <inheritdoc/>
void IScrollAnchorProvider.RegisterAnchorCandidate(IControl element)
{
_anchorCandidates ??= new List<IControl>();
_anchorCandidates.Add(element);
}

/// <inheritdoc/>
void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element)
{
_anchorCandidates?.Remove(element);

if (_anchor.Item1 == element)
{
_anchor = default;
}
}

/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
@@ -224,9 +247,44 @@ protected override Size ArrangeOverride(Size finalSize)
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;
}

@@ -386,5 +444,70 @@ private void UpdateFromScrollable(ILogicalScrollable scrollable)
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;
}
}
}
@@ -229,14 +229,12 @@ public void OnLayoutChanged(bool isVirtualizing)

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()
@@ -267,6 +267,9 @@ protected bool CanVerticallyScroll
get { return VerticalScrollBarVisibility != ScrollBarVisibility.Disabled; }
}

/// <inheritdoc/>
public IControl CurrentAnchor => (Presenter as IScrollAnchorProvider)?.CurrentAnchor;

/// <summary>
/// Gets the maximum horizontal scrollbar value.
/// </summary>
@@ -333,9 +336,6 @@ protected double VerticalScrollBarViewportSize
get { return _viewport.Height; }
}

/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement

/// <summary>
/// Gets the value of the HorizontalScrollBarVisibility attached property.
/// </summary>
@@ -376,14 +376,16 @@ public static void SetVerticalScrollBarVisibility(Control control, ScrollBarVisi
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);
}

internal static Vector CoerceOffset(Size extent, Size viewport, Vector offset)

0 comments on commit 7b44313

Please sign in to comment.
You can’t perform that action at this time.