diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 799b9dc992..e66d6e8ee5 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -8,13 +8,14 @@ using Avalonia.Utilities;
using Avalonia.VisualTree;
using System.Linq;
using Avalonia.Layout;
+using System.Xml.Linq;
namespace Avalonia.Controls.Presenters
{
///
/// Presents a scrolling view of content inside a .
///
- public class ScrollContentPresenter : ContentPresenter, IScrollable, IScrollAnchorProvider
+ public class ScrollContentPresenter : ContentPresenter, IScrollable, IScrollAnchorProvider, IScrollSnapPointAnchorProvider
{
private const double EdgeDetectionTolerance = 0.1;
@@ -87,6 +88,7 @@ namespace Avalonia.Controls.Presenters
private Dictionary? _activeLogicalGestureScrolls;
private Dictionary? _scrollGestureSnapPoints;
private HashSet? _anchorCandidates;
+ private HashSet? _scrollSnapPointsCandidates;
private Control? _anchorElement;
private Rect _anchorElementBounds;
private bool _isAnchorElementDirty;
@@ -606,7 +608,7 @@ namespace Avalonia.Controls.Presenters
if (Content is ItemsControl itemsControl)
scrollable = itemsControl.Presenter?.Panel;
- if (scrollable is not IScrollSnapPointsInfo)
+ if (scrollable is not IScrollSnapPointsInfo && _scrollSnapPointsCandidates?.Any() == false)
return;
if (_scrollGestureSnapPoints == null)
@@ -921,6 +923,58 @@ namespace Avalonia.Controls.Presenters
_horizontalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Layout.Orientation.Horizontal, HorizontalSnapPointsAlignment, out _horizontalSnapPointOffset);
}
}
+ else if (_scrollSnapPointsCandidates is { } candidates && candidates.Count > 0 && this.GetVisualChildren().FirstOrDefault() is { } contentVisual)
+ {
+ var sources = candidates.Select(x => x as Visual).ToList();
+ var horizontalSnapPoints = new List();
+ var verticalSnapPoints = new List();
+
+ _horizontalSnapPoints = horizontalSnapPoints;
+ _verticalSnapPoints = verticalSnapPoints;
+ foreach (var source in sources)
+ {
+ if (source is not IScrollSnapPointsInfo info)
+ continue;
+ var transform = source.TransformToVisual(contentVisual) ?? Matrix.Identity;
+
+ var visualEdge = transform.Transform(source.Bounds.BottomRight);
+ if (!info.AreVerticalSnapPointsRegular)
+ {
+ verticalSnapPoints.AddRange(info.GetIrregularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment).Select(y => transform.Transform(new Point(0, y)).Y).ToList());
+ }
+ else
+ {
+ var interval = info.GetRegularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment, out var offset);
+ if (interval == 0)
+ continue;
+ var snapPoint = transform.Transform(new Point(0, offset)).Y;
+ while (snapPoint < visualEdge.Y)
+ {
+ verticalSnapPoints.Add(snapPoint);
+ offset += interval;
+ snapPoint = transform.Transform(new Point(0, offset)).Y;
+ }
+ }
+
+ if (!info.AreHorizontalSnapPointsRegular)
+ {
+ horizontalSnapPoints.AddRange(info.GetIrregularSnapPoints(Layout.Orientation.Horizontal, HorizontalSnapPointsAlignment).Select(x => transform.Transform(new Point(x, 0)).X).ToList());
+ }
+ else
+ {
+ var interval = info.GetRegularSnapPoints(Layout.Orientation.Horizontal, HorizontalSnapPointsAlignment, out var offset);
+ if (interval == 0)
+ continue;
+ var snapPoint = transform.Transform(new Point(offset, 0)).X;
+ while (snapPoint < visualEdge.Y)
+ {
+ horizontalSnapPoints.Add(snapPoint);
+ offset += interval;
+ snapPoint = transform.Transform(new Point(offset, 0)).X;
+ }
+ }
+ }
+ }
else
{
_horizontalSnapPoints = new List();
@@ -932,7 +986,7 @@ namespace Avalonia.Controls.Presenters
{
var scrollable = GetScrollSnapPointsInfo(Content);
- if (scrollable is null || (VerticalSnapPointsType == SnapPointsType.None && HorizontalSnapPointsType == SnapPointsType.None))
+ if ((scrollable is null && _scrollSnapPointsCandidates?.Count == 0) || (VerticalSnapPointsType == SnapPointsType.None && HorizontalSnapPointsType == SnapPointsType.None))
return offset;
var diff = GetAlignmentDiff();
@@ -1065,5 +1119,34 @@ namespace Avalonia.Controls.Presenters
return snapPointsInfo;
}
+
+ public void RegisterScrollSnapPointsInfoSource(IScrollSnapPointsInfo scrollSnapPointsInfo)
+ {
+ if(scrollSnapPointsInfo is not Visual visual)
+ {
+ throw new InvalidOperationException("ScrollSnapPointsInfo must be a visual");
+ }
+
+ if (!this.IsVisualAncestorOf(visual))
+ {
+ throw new InvalidOperationException(
+ "A ScrollSnapPointsInfo source must be a visual descendent of the ScrollContentPresenter.");
+ }
+
+ _scrollSnapPointsCandidates ??= new();
+ _scrollSnapPointsCandidates.Add(scrollSnapPointsInfo);
+ scrollSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
+ scrollSnapPointsInfo.VerticalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged;
+ }
+
+ public void UnregisterScrollSnapPointsInfoSource(IScrollSnapPointsInfo scrollSnapPointsInfo)
+ {
+ if (_scrollSnapPointsCandidates?.Contains(scrollSnapPointsInfo) == true)
+ {
+ scrollSnapPointsInfo.HorizontalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged;
+ scrollSnapPointsInfo.VerticalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged;
+ _scrollSnapPointsCandidates?.Remove(scrollSnapPointsInfo);
+ }
+ }
}
}
diff --git a/src/Avalonia.Controls/Primitives/IScrollSnapPointAnchorProvider.cs b/src/Avalonia.Controls/Primitives/IScrollSnapPointAnchorProvider.cs
new file mode 100644
index 0000000000..714169d8f0
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/IScrollSnapPointAnchorProvider.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Avalonia.Controls.Primitives
+{
+ internal interface IScrollSnapPointAnchorProvider
+ {
+ void RegisterScrollSnapPointsInfoSource(IScrollSnapPointsInfo scrollSnapPointsInfo);
+ void UnregisterScrollSnapPointsInfoSource(IScrollSnapPointsInfo scrollSnapPointsInfo);
+ }
+}
diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs
index 3152eec2db..dc7258501b 100644
--- a/src/Avalonia.Controls/ScrollViewer.cs
+++ b/src/Avalonia.Controls/ScrollViewer.cs
@@ -6,6 +6,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
+using System.Xml.Linq;
namespace Avalonia.Controls
{
@@ -14,7 +15,7 @@ namespace Avalonia.Controls
///
[TemplatePart("PART_HorizontalScrollBar", typeof(ScrollBar))]
[TemplatePart("PART_VerticalScrollBar", typeof(ScrollBar))]
- public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider, IInternalScroller
+ public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider, IInternalScroller, IScrollSnapPointAnchorProvider
{
///
/// Defines the property.
@@ -873,5 +874,15 @@ namespace Avalonia.Controls
_oldViewport = Viewport;
}
}
+
+ public void RegisterScrollSnapPointsInfoSource(IScrollSnapPointsInfo scrollSnapPointsInfo)
+ {
+ (Presenter as IScrollSnapPointAnchorProvider)?.RegisterScrollSnapPointsInfoSource(scrollSnapPointsInfo);
+ }
+
+ public void UnregisterScrollSnapPointsInfoSource(IScrollSnapPointsInfo scrollSnapPointsInfo)
+ {
+ (Presenter as IScrollSnapPointAnchorProvider)?.UnregisterScrollSnapPointsInfoSource(scrollSnapPointsInfo);
+ }
}
}
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
index adeebf97d9..db28cc0e99 100644
--- a/src/Avalonia.Controls/VirtualizingStackPanel.cs
+++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs
@@ -73,6 +73,7 @@ namespace Avalonia.Controls
private int _focusedIndex = -1;
private Control? _realizingElement;
private int _realizingIndex = -1;
+ private IScrollSnapPointAnchorProvider? _attachedSnapPointProvider;
public VirtualizingStackPanel()
{
@@ -247,11 +248,18 @@ namespace Avalonia.Controls
{
base.OnAttachedToVisualTree(e);
_scrollAnchorProvider = this.FindAncestorOfType();
+ if (this.FindAncestorOfType() is { } scrollSnapPointsAnchor)
+ {
+ _attachedSnapPointProvider = scrollSnapPointsAnchor;
+ scrollSnapPointsAnchor.RegisterScrollSnapPointsInfoSource(this);
+ }
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
+ _attachedSnapPointProvider?.UnregisterScrollSnapPointsInfoSource(this);
+ _attachedSnapPointProvider = null;
_scrollAnchorProvider = null;
}