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; }