From 4390f197d4e48d18f17d92d275d684a539da895f Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 12 Jan 2023 11:40:14 +0000 Subject: [PATCH 01/13] Add support for snap points to scroll viewer --- .../ScrollGestureRecognizer.cs | 10 +- src/Avalonia.Base/Input/Gestures.cs | 4 + .../Input/ScrollGestureEventArgs.cs | 12 + .../Presenters/ScrollContentPresenter.cs | 271 +++++++++++++++++- .../Primitives/IScrollSnapPointsInfo.cs | 19 ++ .../Primitives/SnapPointsAlignment.cs | 9 + .../Primitives/SnapPointsType.cs | 9 + src/Avalonia.Controls/ScrollViewer.cs | 52 ++++ .../Controls/ScrollViewer.xaml | 4 + .../Controls/ScrollViewer.xaml | 6 +- 10 files changed, 390 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs create mode 100644 src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs create mode 100644 src/Avalonia.Controls/Primitives/SnapPointsType.cs diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 790439245a..63ba869f1f 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -8,6 +8,10 @@ namespace Avalonia.Input.GestureRecognizers : StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise IGestureRecognizer { + // Pixels per second speed that is considered to be the stop of inertial scroll + internal const double InertialScrollSpeedEnd = 5; + public const double InertialResistance = 0.15; + private bool _scrolling; private Point _trackedRootPoint; private IPointer? _tracking; @@ -97,9 +101,6 @@ namespace Avalonia.Input.GestureRecognizers } } - // Pixels per second speed that is considered to be the stop of inertial scroll - private const double InertialScrollSpeedEnd = 5; - public void PointerMoved(PointerEventArgs e) { if (e.Pointer == _tracking) @@ -176,6 +177,7 @@ namespace Avalonia.Input.GestureRecognizers var savedGestureId = _gestureId; var st = Stopwatch.StartNew(); var lastTime = TimeSpan.Zero; + _target!.RaiseEvent(new ScrollGestureInertiaStartingEventArgs(_gestureId, _inertia)); DispatcherTimer.Run(() => { // Another gesture has started, finish the current one @@ -187,7 +189,7 @@ namespace Avalonia.Input.GestureRecognizers var elapsedSinceLastTick = st.Elapsed - lastTime; lastTime = st.Elapsed; - var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds); + var speed = _inertia * Math.Pow(InertialResistance, st.Elapsed.TotalSeconds); var distance = speed * elapsedSinceLastTick.TotalSeconds; var scrollGestureEventArgs = new ScrollGestureEventArgs(_gestureId, distance); _target!.RaiseEvent(scrollGestureEventArgs); diff --git a/src/Avalonia.Base/Input/Gestures.cs b/src/Avalonia.Base/Input/Gestures.cs index a9e42c2374..167f61eead 100644 --- a/src/Avalonia.Base/Input/Gestures.cs +++ b/src/Avalonia.Base/Input/Gestures.cs @@ -45,6 +45,10 @@ namespace Avalonia.Input RoutedEvent.Register( "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); + public static readonly RoutedEvent ScrollGestureInertiaStartingEvent = + RoutedEvent.Register( + "ScrollGestureInertiaStarting", RoutingStrategies.Bubble, typeof(Gestures)); + public static readonly RoutedEvent ScrollGestureEndedEvent = RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); diff --git a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs index f1a0887b60..55aaadff71 100644 --- a/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs +++ b/src/Avalonia.Base/Input/ScrollGestureEventArgs.cs @@ -30,4 +30,16 @@ namespace Avalonia.Input Id = id; } } + + public class ScrollGestureInertiaStartingEventArgs : RoutedEventArgs + { + public int Id { get; } + public Vector Inertia { get; } + + internal ScrollGestureInertiaStartingEventArgs(int id, Vector inertia) : base(Gestures.ScrollGestureInertiaStartingEvent) + { + Id = id; + Inertia = inertia; + } + } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 0cfe4bada1..49c6fdbaaa 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Reactive; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Input.GestureRecognizers; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -14,6 +15,7 @@ namespace Avalonia.Controls.Presenters public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider { private const double EdgeDetectionTolerance = 0.1; + private const int ProximityPoints = 10; /// /// Defines the property. @@ -57,6 +59,30 @@ namespace Avalonia.Controls.Presenters o => o.Viewport, (o, v) => o.Viewport = v); + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalSnapPointsTypeProperty = + ScrollViewer.HorizontalSnapPointsTypeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalSnapPointsTypeProperty = + ScrollViewer.VerticalSnapPointsTypeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalSnapPointsAlignmentProperty = + ScrollViewer.HorizontalSnapPointsAlignmentProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalSnapPointsAlignmentProperty = + ScrollViewer.VerticalSnapPointsAlignmentProperty.AddOwner(); + /// /// Defines the property. /// @@ -71,10 +97,19 @@ namespace Avalonia.Controls.Presenters private IDisposable? _logicalScrollSubscription; private Size _viewport; private Dictionary? _activeLogicalGestureScrolls; + private Dictionary? _scrollGestureSnapPoints; private List? _anchorCandidates; private Control? _anchorElement; private Rect _anchorElementBounds; private bool _isAnchorElementDirty; + private bool _areVerticalSnapPointsRegular; + private bool _areHorizontalSnapPointsRegular; + private IReadOnlyList? _horizontalSnapPoints; + private double _horizontalSnapPoint; + private IReadOnlyList? _verticalSnapPoints; + private double _verticalSnapPoint; + private double _verticalSnapPointOffset; + private double _horizontalSnapPointOffset; /// /// Initializes static members of the class. @@ -93,6 +128,7 @@ namespace Avalonia.Controls.Presenters AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture); AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + AddHandler(Gestures.ScrollGestureInertiaStartingEvent, OnScrollGestureInertiaStartingEnded); this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } @@ -142,6 +178,30 @@ namespace Avalonia.Controls.Presenters private set { SetAndRaise(ViewportProperty, ref _viewport, value); } } + public SnapPointsType HorizontalSnapPointsType + { + get => GetValue(HorizontalSnapPointsTypeProperty); + set => SetValue(HorizontalSnapPointsTypeProperty, value); + } + + public SnapPointsType VerticalSnapPointsType + { + get => GetValue(VerticalSnapPointsTypeProperty); + set => SetValue(VerticalSnapPointsTypeProperty, value); + } + + public SnapPointsAlignment HorizontalSnapPointsAlignment + { + get => GetValue(HorizontalSnapPointsAlignmentProperty); + set => SetValue(HorizontalSnapPointsAlignmentProperty, value); + } + + public SnapPointsAlignment VerticalSnapPointsAlignment + { + get => GetValue(VerticalSnapPointsAlignmentProperty); + set => SetValue(VerticalSnapPointsAlignmentProperty, value); + } + /// /// Gets or sets if scroll chaining is enabled. The default value is true. /// @@ -424,6 +484,25 @@ namespace Avalonia.Controls.Presenters } Vector newOffset = new Vector(x, y); + + if (_scrollGestureSnapPoints?.TryGetValue(e.Id, out var snapPoint) == true) + { + double xOffset = x; + double yOffset = y; + + if (HorizontalSnapPointsType != SnapPointsType.None) + { + xOffset = delta.X < 0 ? Math.Max(snapPoint.X, newOffset.X) : Math.Min(snapPoint.X, newOffset.X); + } + + if (VerticalSnapPointsType != SnapPointsType.None) + { + yOffset = delta.Y < 0 ? Math.Max(snapPoint.Y, newOffset.Y) : Math.Min(snapPoint.Y, newOffset.Y); + } + + newOffset = new Vector(xOffset, yOffset); + } + bool offsetChanged = newOffset != Offset; Offset = newOffset; @@ -434,7 +513,65 @@ namespace Avalonia.Controls.Presenters } private void OnScrollGestureEnded(object? sender, ScrollGestureEndedEventArgs e) - => _activeLogicalGestureScrolls?.Remove(e.Id); + { + _activeLogicalGestureScrolls?.Remove(e.Id); + _scrollGestureSnapPoints?.Remove(e.Id); + + Offset = SnapOffset(Offset); + } + + private void OnScrollGestureInertiaStartingEnded(object? sender, ScrollGestureInertiaStartingEventArgs e) + { + if (Content is not IScrollSnapPointsInfo) + return; + + if (_scrollGestureSnapPoints == null) + _scrollGestureSnapPoints = new Dictionary(); + + var offset = Offset; + + if (HorizontalSnapPointsType != SnapPointsType.None && VerticalSnapPointsType != SnapPointsType.None) + { + return; + } + + double xDistance = 0; + double yDistance = 0; + + if (HorizontalSnapPointsType != SnapPointsType.None) + { + xDistance = HorizontalSnapPointsType == SnapPointsType.Mandatory ? GetDistance(e.Inertia.X) : 0; + } + + if (VerticalSnapPointsType != SnapPointsType.None) + { + yDistance = VerticalSnapPointsType == SnapPointsType.Mandatory ? GetDistance(e.Inertia.Y) : 0; + } + + offset = new Vector(offset.X + xDistance, offset.Y + yDistance); + + System.Diagnostics.Debug.WriteLine($"{offset}"); + + _scrollGestureSnapPoints.Add(e.Id, SnapOffset(offset)); + + double GetDistance(double speed) + { + var time = Math.Log(ScrollGestureRecognizer.InertialScrollSpeedEnd / Math.Abs(speed)) / Math.Log(ScrollGestureRecognizer.InertialResistance); + + double timeElapsed = 0, distance = 0, step = 0; + + while (timeElapsed <= time) + { + double s = speed * Math.Pow(ScrollGestureRecognizer.InertialResistance, timeElapsed); + distance += (s * step); + + timeElapsed += 0.016f; + step = 0.016f; + } + + return distance; + } + } /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) @@ -472,6 +609,7 @@ namespace Avalonia.Controls.Presenters } Vector newOffset = new Vector(x, y); + bool offsetChanged = newOffset != Offset; Offset = newOffset; @@ -485,10 +623,36 @@ namespace Avalonia.Controls.Presenters { InvalidateArrange(); } + else if (change.Property == ContentProperty) + { + if (change.OldValue is IScrollSnapPointsInfo oldSnapPointsInfo) + { + oldSnapPointsInfo.VerticalSnapPointsChanged -= ScrollSnapPointsInfoSnapPointsChanged; + oldSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged; + } + + if (Content is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.VerticalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged; + scrollSnapPointsInfo.HorizontalSnapPointsChanged += ScrollSnapPointsInfoSnapPointsChanged; + } + + UpdateSnapPoints(); + } + else if (change.Property == HorizontalSnapPointsAlignmentProperty || + change.Property == VerticalSnapPointsAlignmentProperty) + { + UpdateSnapPoints(); + } base.OnPropertyChanged(change); } + private void ScrollSnapPointsInfoSnapPointsChanged(object? sender, Interactivity.RoutedEventArgs e) + { + UpdateSnapPoints(); + } + private void BringIntoViewRequested(object? sender, RequestBringIntoViewEventArgs e) { if (e.TargetObject is not null) @@ -635,5 +799,110 @@ namespace Avalonia.Controls.Presenters bounds = p.HasValue ? new Rect(p.Value, control.Bounds.Size) : default; return p.HasValue; } + + private void UpdateSnapPoints() + { + if (Content is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + _areVerticalSnapPointsRegular = scrollSnapPointsInfo.AreVerticalSnapPointsRegular; + _areHorizontalSnapPointsRegular = scrollSnapPointsInfo.AreHorizontalSnapPointsRegular; + + if (!_areVerticalSnapPointsRegular) + { + _verticalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Vertical, HorizontalSnapPointsAlignment); + } + else + { + _verticalSnapPoints = new List(); + _verticalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment, out _verticalSnapPointOffset); + + } + + if (!_areHorizontalSnapPointsRegular) + { + _horizontalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Horizontal, HorizontalSnapPointsAlignment); + } + else + { + _horizontalSnapPoints = new List(); + _horizontalSnapPoint = scrollSnapPointsInfo.GetRegularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment, out _horizontalSnapPointOffset); + } + } + else + { + _horizontalSnapPoints = new List(); + _verticalSnapPoints = new List(); + } + } + + private Vector SnapOffset(Vector offset) + { + if(Content is not IScrollSnapPointsInfo) + return offset; + + if (VerticalSnapPointsType != SnapPointsType.None) + { + double nearestSnapPoint = offset.Y; + + if (_areVerticalSnapPointsRegular) + { + var minSnapPoint = (int)(offset.Y / _verticalSnapPoint) * _verticalSnapPoint + _verticalSnapPointOffset; + var maxSnapPoint = minSnapPoint + _verticalSnapPoint; + var midPoint = (minSnapPoint + maxSnapPoint) / 2; + + nearestSnapPoint = offset.Y < midPoint ? minSnapPoint : maxSnapPoint; + } + else if (_verticalSnapPoints != null && _verticalSnapPoints.Count > 0) + { + var higherSnapPoint = FindNearestSnapPoint(_verticalSnapPoints, offset.Y, out var lowerSnapPoint); + var midPoint = (lowerSnapPoint + higherSnapPoint) / 2; + + nearestSnapPoint = offset.Y < midPoint ? lowerSnapPoint : higherSnapPoint; + } + + offset = new Vector(offset.X, nearestSnapPoint); + } + + if (HorizontalSnapPointsType != SnapPointsType.None) + { + double nearestSnapPoint = offset.X; + + if (_areHorizontalSnapPointsRegular) + { + var minSnapPoint = (int)(offset.X / _horizontalSnapPoint) * _horizontalSnapPoint + _horizontalSnapPointOffset; + var maxSnapPoint = minSnapPoint + _horizontalSnapPoint; + var midPoint = (minSnapPoint + maxSnapPoint) / 2; + + nearestSnapPoint = offset.X < midPoint ? minSnapPoint : maxSnapPoint; + } + else if (_horizontalSnapPoints != null && _horizontalSnapPoints.Count > 0) + { + var higherSnapPoint = FindNearestSnapPoint(_horizontalSnapPoints, offset.X, out var lowerSnapPoint); + var midPoint = (lowerSnapPoint + higherSnapPoint) / 2; + + nearestSnapPoint = offset.X < midPoint ? lowerSnapPoint : higherSnapPoint; + } + + offset = new Vector(nearestSnapPoint, offset.Y); + + } + + double FindNearestSnapPoint(IReadOnlyList snapPoints, double value, out double lowerSnapPoint) + { + var point = snapPoints.BinarySearch(value, Comparer.Default); + + lowerSnapPoint = 0; + + if (point < 0) + { + point = ~point; + } + + lowerSnapPoint = snapPoints[Math.Max(0, point - 1)]; + return snapPoints[Math.Min(point, snapPoints.Count - 1)]; + } + + return offset; + } } } diff --git a/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs new file mode 100644 index 0000000000..2aa2382dc0 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using Avalonia.Interactivity; +using Avalonia.Layout; + +namespace Avalonia.Controls.Primitives +{ + public interface IScrollSnapPointsInfo + { + bool AreHorizontalSnapPointsRegular { get; } + bool AreVerticalSnapPointsRegular { get; } + + IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment); + double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset); + + event EventHandler HorizontalSnapPointsChanged; + event EventHandler VerticalSnapPointsChanged; + } +} diff --git a/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs b/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs new file mode 100644 index 0000000000..9f0125f1c4 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Controls.Primitives +{ + public enum SnapPointsAlignment + { + Near, + Center, + Far + } +} diff --git a/src/Avalonia.Controls/Primitives/SnapPointsType.cs b/src/Avalonia.Controls/Primitives/SnapPointsType.cs new file mode 100644 index 0000000000..7e43f4c191 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/SnapPointsType.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Controls.Primitives +{ + public enum SnapPointsType + { + None, + Mandatory, + MandatorySingle + } +} diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 503187e2d3..215e2dd395 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -151,6 +151,34 @@ namespace Avalonia.Controls o => o.VerticalScrollBarValue, (o, v) => o.VerticalScrollBarValue = v); + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalSnapPointsTypeProperty = + AvaloniaProperty.Register( + nameof(HorizontalSnapPointsType)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalSnapPointsTypeProperty = + AvaloniaProperty.Register( + nameof(VerticalSnapPointsType)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalSnapPointsAlignmentProperty = + AvaloniaProperty.Register( + nameof(HorizontalSnapPointsAlignment)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalSnapPointsAlignmentProperty = + AvaloniaProperty.Register( + nameof(VerticalSnapPointsAlignment)); + /// /// Defines the VerticalScrollBarViewportSize property. /// @@ -421,6 +449,30 @@ namespace Avalonia.Controls private set => SetAndRaise(ScrollBar.IsExpandedProperty, ref _isExpanded, value); } + public SnapPointsType HorizontalSnapPointsType + { + get => GetValue(HorizontalSnapPointsTypeProperty); + set => SetValue(HorizontalSnapPointsTypeProperty, value); + } + + public SnapPointsType VerticalSnapPointsType + { + get => GetValue(VerticalSnapPointsTypeProperty); + set => SetValue(VerticalSnapPointsTypeProperty, value); + } + + public SnapPointsAlignment HorizontalSnapPointsAlignment + { + get => GetValue(HorizontalSnapPointsAlignmentProperty); + set => SetValue(HorizontalSnapPointsAlignmentProperty, value); + } + + public SnapPointsAlignment VerticalSnapPointsAlignment + { + get => GetValue(VerticalSnapPointsAlignmentProperty); + set => SetValue(VerticalSnapPointsAlignmentProperty, value); + } + /// /// Gets a value that indicates whether scrollbars can hide itself when user is not interacting with it. /// diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml index 71df4d419f..f13f775695 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml @@ -34,6 +34,10 @@ Content="{TemplateBinding Content}" Extent="{TemplateBinding Extent, Mode=TwoWay}" Padding="{TemplateBinding Padding}" + HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}" + VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}" + HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}" + VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}" Offset="{TemplateBinding Offset, Mode=TwoWay}" Viewport="{TemplateBinding Viewport, Mode=TwoWay}" IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"> diff --git a/src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml index e11fbb6f15..7029792467 100644 --- a/src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml @@ -13,7 +13,11 @@ Background="{TemplateBinding Background}" CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}" CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" - Content="{TemplateBinding Content}" + Content="{TemplateBinding Content}" + HorizontalSnapPointsType="{TemplateBinding HorizontalSnapPointsType}" + VerticalSnapPointsType="{TemplateBinding VerticalSnapPointsType}" + HorizontalSnapPointsAlignment="{TemplateBinding HorizontalSnapPointsAlignment}" + VerticalSnapPointsAlignment="{TemplateBinding VerticalSnapPointsAlignment}" Extent="{TemplateBinding Extent, Mode=TwoWay}" IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}" From d102536017e009660a41b3861e0c7355213c36bf Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Thu, 12 Jan 2023 11:40:35 +0000 Subject: [PATCH 02/13] implement IScrollSnapPointInfo in StackPanel --- src/Avalonia.Controls/StackPanel.cs | 178 +++++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 964a1055df..89afb50b38 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -4,7 +4,11 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; namespace Avalonia.Controls @@ -12,7 +16,7 @@ namespace Avalonia.Controls /// /// A panel which lays out its children horizontally or vertically. /// - public class StackPanel : Panel, INavigableContainer + public class StackPanel : Panel, INavigableContainer, IScrollSnapPointsInfo { /// /// Defines the property. @@ -26,6 +30,34 @@ namespace Avalonia.Controls public static readonly StyledProperty OrientationProperty = StackLayout.OrientationProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent HorizontalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(HorizontalSnapPointsChanged), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent VerticalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(VerticalSnapPointsChanged), + RoutingStrategies.Bubble); + /// /// Initializes static members of the class. /// @@ -53,6 +85,31 @@ namespace Avalonia.Controls set { SetValue(OrientationProperty, value); } } + public event EventHandler? HorizontalSnapPointsChanged + { + add => AddHandler(HorizontalSnapPointsChangedEvent, value); + remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value); + } + + + public event EventHandler? VerticalSnapPointsChanged + { + add => AddHandler(VerticalSnapPointsChangedEvent, value); + remove => RemoveHandler(VerticalSnapPointsChangedEvent, value); + } + + public bool AreHorizontalSnapPointsRegular + { + get { return GetValue(AreHorizontalSnapPointsRegularProperty); } + set { SetValue(AreHorizontalSnapPointsRegularProperty, value); } + } + + public bool AreVerticalSnapPointsRegular + { + get { return GetValue(AreVerticalSnapPointsRegularProperty); } + set { SetValue(AreVerticalSnapPointsRegularProperty, value); } + } + /// /// Gets the next control in the specified direction. /// @@ -274,6 +331,8 @@ namespace Avalonia.Controls ArrangeChild(child, rcChild, finalSize, Orientation); } + RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent)); + return finalSize; } @@ -285,5 +344,122 @@ namespace Avalonia.Controls { child.Arrange(rect); } + + public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) + { + var snapPoints = new List(); + + switch (orientation) + { + case Orientation.Horizontal: + if (AreHorizontalSnapPointsRegular) + throw new InvalidOperationException(); + if (Orientation == Orientation.Horizontal) + { + foreach(var child in VisualChildren) + { + double snapPoint = 0; + + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + snapPoint = child.Bounds.Left; + break; + case SnapPointsAlignment.Center: + snapPoint = child.Bounds.Center.X; + break; + case SnapPointsAlignment.Far: + snapPoint = child.Bounds.Right; + break; + } + + snapPoints.Add(snapPoint); + } + } + break; + case Orientation.Vertical: + if (AreVerticalSnapPointsRegular) + throw new InvalidOperationException(); + if (Orientation == Orientation.Vertical) + { + foreach (var child in VisualChildren) + { + double snapPoint = 0; + + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + snapPoint = child.Bounds.Top; + break; + case SnapPointsAlignment.Center: + snapPoint = child.Bounds.Center.Y; + break; + case SnapPointsAlignment.Far: + snapPoint = child.Bounds.Bottom; + break; + } + + snapPoints.Add(snapPoint); + } + } + break; + } + + return snapPoints; + } + + public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) + { + offset = 0f; + var firstChild = VisualChildren.FirstOrDefault(); + + if(firstChild == null) + { + return 0; + } + + double snapPoint = 0; + + switch (Orientation) + { + case Orientation.Horizontal: + if (!AreHorizontalSnapPointsRegular) + throw new InvalidOperationException(); + + snapPoint = firstChild.Bounds.Width; + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + offset = firstChild.Bounds.Left; + break; + case SnapPointsAlignment.Center: + offset = firstChild.Bounds.Center.X; + break; + case SnapPointsAlignment.Far: + offset = firstChild.Bounds.Right; + break; + } + break; + case Orientation.Vertical: + if (!AreVerticalSnapPointsRegular) + throw new InvalidOperationException(); + snapPoint = firstChild.Bounds.Height; + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + offset = firstChild.Bounds.Top; + break; + case SnapPointsAlignment.Center: + offset = firstChild.Bounds.Center.Y; + break; + case SnapPointsAlignment.Far: + offset = firstChild.Bounds.Bottom; + break; + } + break; + } + + return snapPoint; + } } } From 87b0325bf134a0ad517a38b6ef55764beb309d6c Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 16 Jan 2023 16:35:01 +0000 Subject: [PATCH 03/13] add comments --- .../Presenters/ScrollContentPresenter.cs | 18 +++++++++++++++--- src/Avalonia.Controls/ScrollViewer.cs | 12 ++++++++++++ src/Avalonia.Controls/StackPanel.cs | 13 ++++++++++++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 49c6fdbaaa..15f51822d6 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -178,24 +178,36 @@ namespace Avalonia.Controls.Presenters private set { SetAndRaise(ViewportProperty, ref _viewport, value); } } + /// + /// Gets or sets how scroll gesture reacts to the snap points along the horizontal axis. + /// public SnapPointsType HorizontalSnapPointsType { - get => GetValue(HorizontalSnapPointsTypeProperty); + get => GetValue(HorizontalSnapPointsTypeProperty); set => SetValue(HorizontalSnapPointsTypeProperty, value); } + /// + /// Gets or sets how scroll gesture reacts to the snap points along the vertical axis. + /// public SnapPointsType VerticalSnapPointsType { - get => GetValue(VerticalSnapPointsTypeProperty); + get => GetValue(VerticalSnapPointsTypeProperty); set => SetValue(VerticalSnapPointsTypeProperty, value); } + /// + /// Gets or sets how the existing snap points are horizontally aligned versus the initial viewport. + /// public SnapPointsAlignment HorizontalSnapPointsAlignment { - get => GetValue(HorizontalSnapPointsAlignmentProperty); + get => GetValue(HorizontalSnapPointsAlignmentProperty); set => SetValue(HorizontalSnapPointsAlignmentProperty, value); } + /// + /// Gets or sets how the existing snap points are vertically aligned versus the initial viewport. + /// public SnapPointsAlignment VerticalSnapPointsAlignment { get => GetValue(VerticalSnapPointsAlignmentProperty); diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 215e2dd395..fb40006b4c 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -449,24 +449,36 @@ namespace Avalonia.Controls private set => SetAndRaise(ScrollBar.IsExpandedProperty, ref _isExpanded, value); } + /// + /// Gets or sets how scroll gesture reacts to the snap points along the horizontal axis. + /// public SnapPointsType HorizontalSnapPointsType { get => GetValue(HorizontalSnapPointsTypeProperty); set => SetValue(HorizontalSnapPointsTypeProperty, value); } + /// + /// Gets or sets how scroll gesture reacts to the snap points along the vertical axis. + /// public SnapPointsType VerticalSnapPointsType { get => GetValue(VerticalSnapPointsTypeProperty); set => SetValue(VerticalSnapPointsTypeProperty, value); } + /// + /// Gets or sets how the existing snap points are horizontally aligned versus the initial viewport. + /// public SnapPointsAlignment HorizontalSnapPointsAlignment { get => GetValue(HorizontalSnapPointsAlignmentProperty); set => SetValue(HorizontalSnapPointsAlignmentProperty, value); } + /// + /// Gets or sets how the existing snap points are vertically aligned versus the initial viewport. + /// public SnapPointsAlignment VerticalSnapPointsAlignment { get => GetValue(VerticalSnapPointsAlignmentProperty); diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 89afb50b38..b9524a515f 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -85,25 +85,36 @@ namespace Avalonia.Controls set { SetValue(OrientationProperty, value); } } + /// + /// Occurs when the measurements for horizontal snap points change. + /// public event EventHandler? HorizontalSnapPointsChanged { add => AddHandler(HorizontalSnapPointsChangedEvent, value); remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value); } - + /// + /// Occurs when the measurements for vertical snap points change. + /// public event EventHandler? VerticalSnapPointsChanged { add => AddHandler(VerticalSnapPointsChangedEvent, value); remove => RemoveHandler(VerticalSnapPointsChangedEvent, value); } + /// + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// public bool AreHorizontalSnapPointsRegular { get { return GetValue(AreHorizontalSnapPointsRegularProperty); } set { SetValue(AreHorizontalSnapPointsRegularProperty, value); } } + /// + /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// public bool AreVerticalSnapPointsRegular { get { return GetValue(AreVerticalSnapPointsRegularProperty); } From e7891da6ad660ab559a9cb92b785adb96b782f24 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 17 Jan 2023 13:20:57 +0000 Subject: [PATCH 04/13] snap on pointer wheel. fix center and far snap alignment --- .../Presenters/ScrollContentPresenter.cs | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 15f51822d6..0f8a8bd965 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -620,7 +620,7 @@ namespace Avalonia.Controls.Presenters x = Math.Min(x, Extent.Width - Viewport.Width); } - Vector newOffset = new Vector(x, y); + Vector newOffset = SnapOffset(new Vector(x, y)); bool offsetChanged = newOffset != Offset; Offset = newOffset; @@ -852,8 +852,11 @@ namespace Avalonia.Controls.Presenters if(Content is not IScrollSnapPointsInfo) return offset; + var diff = GetAlignedDiff(); + if (VerticalSnapPointsType != SnapPointsType.None) { + offset = new Vector(offset.X, offset.Y + diff.Y); double nearestSnapPoint = offset.Y; if (_areVerticalSnapPointsRegular) @@ -872,11 +875,12 @@ namespace Avalonia.Controls.Presenters nearestSnapPoint = offset.Y < midPoint ? lowerSnapPoint : higherSnapPoint; } - offset = new Vector(offset.X, nearestSnapPoint); + offset = new Vector(offset.X, nearestSnapPoint - diff.Y); } if (HorizontalSnapPointsType != SnapPointsType.None) { + offset = new Vector(offset.X + diff.X, offset.Y); double nearestSnapPoint = offset.X; if (_areHorizontalSnapPointsRegular) @@ -895,7 +899,7 @@ namespace Avalonia.Controls.Presenters nearestSnapPoint = offset.X < midPoint ? lowerSnapPoint : higherSnapPoint; } - offset = new Vector(nearestSnapPoint, offset.Y); + offset = new Vector(nearestSnapPoint - diff.X, offset.Y); } @@ -903,8 +907,6 @@ namespace Avalonia.Controls.Presenters { var point = snapPoints.BinarySearch(value, Comparer.Default); - lowerSnapPoint = 0; - if (point < 0) { point = ~point; @@ -914,6 +916,33 @@ namespace Avalonia.Controls.Presenters return snapPoints[Math.Min(point, snapPoints.Count - 1)]; } + Vector GetAlignedDiff() + { + var vector = offset; + + switch (VerticalSnapPointsAlignment) + { + case SnapPointsAlignment.Center: + vector += new Vector(0, Viewport.Height / 2); + break; + case SnapPointsAlignment.Far: + vector += new Vector(0, Viewport.Height); + break; + } + + switch (HorizontalSnapPointsAlignment) + { + case SnapPointsAlignment.Center: + vector += new Vector(Viewport.Width / 2, 0); + break; + case SnapPointsAlignment.Far: + vector += new Vector(Viewport.Width, 0); + break; + } + + return vector - offset; + } + return offset; } } From 93f6d3196650c2f27c29b24530db7a038f8ef705 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 17 Jan 2023 13:48:30 +0000 Subject: [PATCH 05/13] support single scroll snap on pointer wheel --- .../Presenters/ScrollContentPresenter.cs | 80 ++++++++++++++++--- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 0f8a8bd965..2326699c36 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -607,6 +607,30 @@ namespace Avalonia.Controls.Presenters if (Extent.Height > Viewport.Height) { double height = isLogical ? scrollable!.ScrollSize.Height : 50; + if(VerticalSnapPointsType == SnapPointsType.MandatorySingle && Content is IScrollSnapPointsInfo) + { + if(_areVerticalSnapPointsRegular) + { + height = _verticalSnapPoint; + } + else + { + double yOffset = Offset.Y; + switch (VerticalSnapPointsAlignment) + { + case SnapPointsAlignment.Center: + yOffset += Viewport.Height / 2; + break; + case SnapPointsAlignment.Far: + yOffset += Viewport.Height; + break; + } + + var snapPoint = FindNearestSnapPoint(_verticalSnapPoints, yOffset, out var lowerSnapPoint); + + height = snapPoint - lowerSnapPoint; + } + } y += -delta.Y * height; y = Math.Max(y, 0); y = Math.Min(y, Extent.Height - Viewport.Height); @@ -615,6 +639,30 @@ namespace Avalonia.Controls.Presenters if (Extent.Width > Viewport.Width) { double width = isLogical ? scrollable!.ScrollSize.Width : 50; + if (HorizontalSnapPointsType == SnapPointsType.MandatorySingle && Content is IScrollSnapPointsInfo) + { + if (_areHorizontalSnapPointsRegular) + { + width = _horizontalSnapPoint; + } + else + { + double xOffset = Offset.X; + switch (VerticalSnapPointsAlignment) + { + case SnapPointsAlignment.Center: + xOffset += Viewport.Width / 2; + break; + case SnapPointsAlignment.Far: + xOffset += Viewport.Width; + break; + } + + var snapPoint = FindNearestSnapPoint(_horizontalSnapPoints, xOffset, out var lowerSnapPoint); + + width = snapPoint - lowerSnapPoint; + } + } x += -delta.X * width; x = Math.Max(x, 0); x = Math.Min(x, Extent.Width - Viewport.Width); @@ -903,19 +951,6 @@ namespace Avalonia.Controls.Presenters } - double FindNearestSnapPoint(IReadOnlyList snapPoints, double value, out double lowerSnapPoint) - { - var point = snapPoints.BinarySearch(value, Comparer.Default); - - if (point < 0) - { - point = ~point; - } - - lowerSnapPoint = snapPoints[Math.Max(0, point - 1)]; - return snapPoints[Math.Min(point, snapPoints.Count - 1)]; - } - Vector GetAlignedDiff() { var vector = offset; @@ -945,5 +980,24 @@ namespace Avalonia.Controls.Presenters return offset; } + + private static double FindNearestSnapPoint(IReadOnlyList snapPoints, double value, out double lowerSnapPoint) + { + var point = snapPoints.BinarySearch(value, Comparer.Default); + + if (point < 0) + { + point = ~point; + + lowerSnapPoint = snapPoints[Math.Max(0, point - 1)]; + } + else + { + lowerSnapPoint = snapPoints[point]; + + point += 1; + } + return snapPoints[Math.Min(point, snapPoints.Count - 1)]; + } } } From bde380ebfa630f1d8df216025baf49904073eef8 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 17 Jan 2023 20:08:22 +0000 Subject: [PATCH 06/13] Add snapping sample to scroll viewer page --- .../Pages/ScrollViewerPage.xaml | 139 +++++++++++++++++- .../Pages/ScrollViewerPage.xaml.cs | 37 +++++ .../Presenters/ScrollContentPresenter.cs | 4 +- 3 files changed, 176 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml index 1903e50ed7..0eceb9e07d 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml @@ -3,8 +3,8 @@ xmlns:pages="using:ControlCatalog.Pages" x:Class="ControlCatalog.Pages.ScrollViewerPage" x:DataType="pages:ScrollViewerPageViewModel"> - - Allows for horizontal and vertical content scrolling. + + Allows for horizontal and vertical content scrolling. Supports snapping on touch and pointer wheel scrolling. @@ -31,6 +31,141 @@ Source="/Assets/delicate-arch-896885_640.jpg" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs index dcd7a88a56..10244af5f3 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs @@ -11,6 +11,9 @@ namespace ControlCatalog.Pages private bool _allowAutoHide; private ScrollBarVisibility _horizontalScrollVisibility; private ScrollBarVisibility _verticalScrollVisibility; + private SnapPointsType _verticalSnapPointsType; + private SnapPointsAlignment _verticalSnapPointsAlignment; + private bool _areSnapPointsRegular; public ScrollViewerPageViewModel() { @@ -22,6 +25,20 @@ namespace ControlCatalog.Pages ScrollBarVisibility.Disabled, }; + AvailableSnapPointsType = new List() + { + SnapPointsType.None, + SnapPointsType.Mandatory, + SnapPointsType.MandatorySingle + }; + + AvailableSnapPointsAlignment = new List() + { + SnapPointsAlignment.Near, + SnapPointsAlignment.Center, + SnapPointsAlignment.Far, + }; + HorizontalScrollVisibility = ScrollBarVisibility.Auto; VerticalScrollVisibility = ScrollBarVisibility.Auto; AllowAutoHide = true; @@ -33,6 +50,12 @@ namespace ControlCatalog.Pages set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value); } + public bool AreSnapPointsRegular + { + get => _areSnapPointsRegular; + set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); + } + public ScrollBarVisibility HorizontalScrollVisibility { get => _horizontalScrollVisibility; @@ -45,7 +68,21 @@ namespace ControlCatalog.Pages set => this.RaiseAndSetIfChanged(ref _verticalScrollVisibility, value); } + public SnapPointsType VerticalSnapPointsType + { + get => _verticalSnapPointsType; + set => this.RaiseAndSetIfChanged(ref _verticalSnapPointsType, value); + } + + public SnapPointsAlignment VerticalSnapPointsAlignment + { + get => _verticalSnapPointsAlignment; + set => this.RaiseAndSetIfChanged(ref _verticalSnapPointsAlignment, value); + } + public List AvailableVisibility { get; } + public List AvailableSnapPointsType { get; } + public List AvailableSnapPointsAlignment { get; } } public class ScrollViewerPage : UserControl diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 2326699c36..3b291841fe 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -613,7 +613,7 @@ namespace Avalonia.Controls.Presenters { height = _verticalSnapPoint; } - else + else if(_verticalSnapPoints != null) { double yOffset = Offset.Y; switch (VerticalSnapPointsAlignment) @@ -645,7 +645,7 @@ namespace Avalonia.Controls.Presenters { width = _horizontalSnapPoint; } - else + else if(_horizontalSnapPoints != null) { double xOffset = Offset.X; switch (VerticalSnapPointsAlignment) From fce16337474b7389d976680d285c00ddcc5f8ffc Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Wed, 18 Jan 2023 10:41:53 +0000 Subject: [PATCH 07/13] add doc comments --- .../Primitives/IScrollSnapPointsInfo.cs | 31 +++++++++++++++++++ .../Primitives/SnapPointsAlignment.cs | 14 +++++++++ .../Primitives/SnapPointsType.cs | 14 +++++++++ 3 files changed, 59 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs index 2aa2382dc0..d0462aff9e 100644 --- a/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs +++ b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs @@ -5,15 +5,46 @@ using Avalonia.Layout; namespace Avalonia.Controls.Primitives { + /// + /// Describes snap point behavior for objects that contain and present items. + /// public interface IScrollSnapPointsInfo { + /// + /// Gets a value that indicates whether the horizontal snap points for the container are equidistant from each other. + /// bool AreHorizontalSnapPointsRegular { get; } + + /// + /// Gets a value that indicates whether the vertical snap points for the container are equidistant from each other. + /// bool AreVerticalSnapPointsRegular { get; } + /// + /// Returns the set of distances between irregular snap points for a specified orientation and alignment. + /// + /// The orientation for the desired snap point set. + /// The alignment to use when applying the snap points. + /// The read-only collection of snap point distances. Returns an empty collection when no snap points are present. IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment); + + /// + /// Gets the distance between regular snap points for a specified orientation and alignment. + /// + /// The orientation for the desired snap point set. + /// The alignment to use when applying the snap points. + /// Out parameter. The offset of the first snap point. + /// The distance between the equidistant snap points. Returns 0 when no snap points are present. double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset); + /// + /// Occurs when the measurements for horizontal snap points change. + /// event EventHandler HorizontalSnapPointsChanged; + + /// + /// Occurs when the measurements for vertical snap points change. + /// event EventHandler VerticalSnapPointsChanged; } } diff --git a/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs b/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs index 9f0125f1c4..77b93c50a0 100644 --- a/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs +++ b/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs @@ -1,9 +1,23 @@ namespace Avalonia.Controls.Primitives { + /// + /// Specify options for snap point alignment relative to an edge. Which edge depends on the orientation of the object where the alignment is applied + /// public enum SnapPointsAlignment { + /// + /// Use snap points grouped closer to the orientation edge. + /// Near, + + /// + /// Use snap points that are centered in the orientation. + /// Center, + + /// + /// Use snap points grouped farther from the orientation edge. + /// Far } } diff --git a/src/Avalonia.Controls/Primitives/SnapPointsType.cs b/src/Avalonia.Controls/Primitives/SnapPointsType.cs index 7e43f4c191..130fb85f77 100644 --- a/src/Avalonia.Controls/Primitives/SnapPointsType.cs +++ b/src/Avalonia.Controls/Primitives/SnapPointsType.cs @@ -1,9 +1,23 @@ namespace Avalonia.Controls.Primitives { + /// + /// Specify how panning snap points are processed for gesture input. + /// public enum SnapPointsType { + /// + /// No snapping behavior. + /// None, + + /// + /// Content always stops at the snap point closest to where inertia would naturally stop along the direction of inertia. + /// Mandatory, + + /// + /// Content always stops at the snap point closest to the release point along the direction of inertia. + /// MandatorySingle } } From b1b63cba2abcd10776f6141921a7cf29c8cb7c24 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 20 Jan 2023 12:54:22 +0000 Subject: [PATCH 08/13] wip --- src/Avalonia.Controls/ItemsControl.cs | 60 ++++++++++++++++- .../Presenters/ItemsPresenter.cs | 67 ++++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index d19a04eb21..1948fda928 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -12,6 +12,8 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Styling; @@ -23,7 +25,7 @@ namespace Avalonia.Controls /// Displays a collection of items. /// [PseudoClasses(":empty", ":singleitem")] - public class ItemsControl : TemplatedControl, IChildIndexProvider + public class ItemsControl : TemplatedControl, IChildIndexProvider, IScrollSnapPointsInfo { /// /// The default value for the property. @@ -91,6 +93,7 @@ namespace Avalonia.Controls private IDataTemplate? _displayMemberItemTemplate; private Tuple? _containerBeingPrepared; private ScrollViewer? _scrollViewer; + private ItemsPresenter? _itemsPresenter; /// /// Initializes a new instance of the class. @@ -203,6 +206,45 @@ namespace Avalonia.Controls remove => _childIndexChanged -= value; } + + public event EventHandler HorizontalSnapPointsChanged + { + add + { + if (_itemsPresenter != null) + { + _itemsPresenter.HorizontalSnapPointsChanged += value; + } + } + + remove + { + if (_itemsPresenter != null) + { + _itemsPresenter.HorizontalSnapPointsChanged -= value; + } + } + } + + public event EventHandler VerticalSnapPointsChanged + { + add + { + if (_itemsPresenter != null) + { + _itemsPresenter.VerticalSnapPointsChanged += value; + } + } + + remove + { + if (_itemsPresenter != null) + { + _itemsPresenter.VerticalSnapPointsChanged -= value; + } + } + } + /// /// Returns the container for the item at the specified index. /// @@ -254,7 +296,8 @@ namespace Avalonia.Controls /// Gets the currently realized containers. /// public IEnumerable GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty(); - + public bool AreHorizontalSnapPointsRegular => _itemsPresenter?.AreHorizontalSnapPointsRegular ?? false; + public bool AreVerticalSnapPointsRegular => _itemsPresenter?.AreVerticalSnapPointsRegular ?? false; /// /// Creates or a container that can be used to display an item. @@ -355,6 +398,7 @@ namespace Avalonia.Controls { base.OnApplyTemplate(e); _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); + _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); } /// @@ -671,5 +715,17 @@ namespace Avalonia.Controls count = ItemsView.Count; return true; } + + public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) + { + return _itemsPresenter?.GetIrregularSnapPoints(orientation, snapPointsAlignment) ?? new List(); + } + + public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) + { + offset = 0; + + return _itemsPresenter?.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset) ?? 0; + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index e0252feed5..08de6bf5a8 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -3,13 +3,15 @@ using System.Collections.Generic; using System.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; namespace Avalonia.Controls.Presenters { /// /// Presents items inside an . /// - public class ItemsPresenter : Control, ILogicalScrollable + public class ItemsPresenter : Control, ILogicalScrollable, IScrollSnapPointsInfo { /// /// Defines the property. @@ -21,6 +23,44 @@ namespace Avalonia.Controls.Presenters private ILogicalScrollable? _logicalScrollable; private EventHandler? _scrollInvalidated; + public event EventHandler HorizontalSnapPointsChanged + { + add + { + if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.HorizontalSnapPointsChanged += value; + } + } + + remove + { + if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.HorizontalSnapPointsChanged -= value; + } + } + } + + public event EventHandler VerticalSnapPointsChanged + { + add + { + if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.VerticalSnapPointsChanged += value; + } + } + + remove + { + if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.VerticalSnapPointsChanged -= value; + } + } + } + static ItemsPresenter() { KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( @@ -89,6 +129,9 @@ namespace Avalonia.Controls.Presenters Size IScrollable.Extent => _logicalScrollable?.Extent ?? default; Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; + public bool AreHorizontalSnapPointsRegular => Panel is IScrollSnapPointsInfo scrollSnapPointsInfo ? scrollSnapPointsInfo.AreHorizontalSnapPointsRegular : false; + public bool AreVerticalSnapPointsRegular => Panel is IScrollSnapPointsInfo scrollSnapPointsInfo ? scrollSnapPointsInfo.AreVerticalSnapPointsRegular : false; + public override sealed void ApplyTemplate() { if (Panel is null && ItemsControl is not null) @@ -204,5 +247,27 @@ namespace Avalonia.Controls.Presenters } private void OnLogicalScrollInvalidated(object? sender, EventArgs e) => _scrollInvalidated?.Invoke(this, e); + + public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) + { + if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + return scrollSnapPointsInfo.GetIrregularSnapPoints(orientation, snapPointsAlignment); + } + + return new List(); + } + + public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) + { + if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + return scrollSnapPointsInfo.GetRegularSnapPoints(orientation, snapPointsAlignment, out offset); + } + + offset = 0; + + return 0; + } } } From e6c60ddfef1a23b5225056bab610758afdd98ff0 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Fri, 20 Jan 2023 17:52:55 +0000 Subject: [PATCH 09/13] add regular snap points to virutalizing panel --- src/Avalonia.Controls/ListBox.cs | 5 +- .../Presenters/ItemsPresenter.cs | 83 ++++---- src/Avalonia.Controls/ScrollViewer.cs | 8 +- src/Avalonia.Controls/StackPanel.cs | 4 +- .../VirtualizingStackPanel.cs | 190 +++++++++++++++++- .../Controls/ListBox.xaml | 3 + 6 files changed, 250 insertions(+), 43 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 8b1a307182..775e0ae0cb 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -21,7 +21,10 @@ namespace Avalonia.Controls /// The default value for the property. /// private static readonly FuncTemplate DefaultPanel = - new FuncTemplate(() => new VirtualizingStackPanel()); + new FuncTemplate(() => new VirtualizingStackPanel() + { + AreVerticalSnapPointsRegular= true, + }); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 08de6bf5a8..e3332ef3a2 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -23,43 +23,21 @@ namespace Avalonia.Controls.Presenters private ILogicalScrollable? _logicalScrollable; private EventHandler? _scrollInvalidated; - public event EventHandler HorizontalSnapPointsChanged - { - add - { - if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) - { - scrollSnapPointsInfo.HorizontalSnapPointsChanged += value; - } - } - - remove - { - if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) - { - scrollSnapPointsInfo.HorizontalSnapPointsChanged -= value; - } - } - } - - public event EventHandler VerticalSnapPointsChanged - { - add - { - if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) - { - scrollSnapPointsInfo.VerticalSnapPointsChanged += value; - } - } + /// + /// Defines the event. + /// + public static readonly RoutedEvent HorizontalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(HorizontalSnapPointsChanged), + RoutingStrategies.Bubble); - remove - { - if (Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) - { - scrollSnapPointsInfo.VerticalSnapPointsChanged -= value; - } - } - } + /// + /// Defines the event. + /// + public static readonly RoutedEvent VerticalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(VerticalSnapPointsChanged), + RoutingStrategies.Bubble); static ItemsPresenter() { @@ -123,6 +101,24 @@ namespace Avalonia.Controls.Presenters } } + /// + /// Occurs when the measurements for horizontal snap points change. + /// + public event EventHandler? HorizontalSnapPointsChanged + { + add => AddHandler(HorizontalSnapPointsChangedEvent, value); + remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value); + } + + /// + /// Occurs when the measurements for vertical snap points change. + /// + public event EventHandler? VerticalSnapPointsChanged + { + add => AddHandler(VerticalSnapPointsChangedEvent, value); + remove => RemoveHandler(VerticalSnapPointsChangedEvent, value); + } + bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false; Size ILogicalScrollable.ScrollSize => _logicalScrollable?.ScrollSize ?? default; Size ILogicalScrollable.PageScrollSize => _logicalScrollable?.PageScrollSize ?? default; @@ -151,6 +147,21 @@ namespace Avalonia.Controls.Presenters else CreateSimplePanelGenerator(); + if(Panel is IScrollSnapPointsInfo scrollSnapPointsInfo) + { + scrollSnapPointsInfo.VerticalSnapPointsChanged += (s, e) => + { + e.RoutedEvent = VerticalSnapPointsChangedEvent; + RaiseEvent(e); + }; + + scrollSnapPointsInfo.HorizontalSnapPointsChanged += (s, e) => + { + e.RoutedEvent = HorizontalSnapPointsChangedEvent; + RaiseEvent(e); + }; + } + _logicalScrollable = Panel as ILogicalScrollable; if (_logicalScrollable is not null) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index fb40006b4c..1340de3af9 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -168,15 +168,15 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty HorizontalSnapPointsAlignmentProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty HorizontalSnapPointsAlignmentProperty = + AvaloniaProperty.RegisterAttached( nameof(HorizontalSnapPointsAlignment)); /// /// Defines the property. /// - public static readonly StyledProperty VerticalSnapPointsAlignmentProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty VerticalSnapPointsAlignmentProperty = + AvaloniaProperty.RegisterAttached( nameof(VerticalSnapPointsAlignment)); /// diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index b9524a515f..aa63ac975e 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -356,6 +356,7 @@ namespace Avalonia.Controls child.Arrange(rect); } + /// public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) { var snapPoints = new List(); @@ -419,6 +420,7 @@ namespace Avalonia.Controls return snapPoints; } + /// public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) { offset = 0f; @@ -470,7 +472,7 @@ namespace Avalonia.Controls break; } - return snapPoint; + return snapPoint + Spacing; } } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 3f539ce198..f60c67b577 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -3,8 +3,11 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; +using System.Reflection; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Utilities; using Avalonia.VisualTree; @@ -14,7 +17,7 @@ namespace Avalonia.Controls /// /// Arranges and virtualizes content on a single line that is oriented either horizontally or vertically. /// - public class VirtualizingStackPanel : VirtualizingPanel + public class VirtualizingStackPanel : VirtualizingPanel, IScrollSnapPointsInfo { /// /// Defines the property. @@ -22,6 +25,34 @@ namespace Avalonia.Controls public static readonly StyledProperty OrientationProperty = StackLayout.OrientationProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent HorizontalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(HorizontalSnapPointsChanged), + RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent VerticalSnapPointsChangedEvent = + RoutedEvent.Register( + nameof(VerticalSnapPointsChanged), + RoutingStrategies.Bubble); + private static readonly AttachedProperty ItemIsOwnContainerProperty = AvaloniaProperty.RegisterAttached("ItemIsOwnContainer"); @@ -62,6 +93,42 @@ namespace Avalonia.Controls set => SetValue(OrientationProperty, value); } + /// + /// Occurs when the measurements for horizontal snap points change. + /// + public event EventHandler? HorizontalSnapPointsChanged + { + add => AddHandler(HorizontalSnapPointsChangedEvent, value); + remove => RemoveHandler(HorizontalSnapPointsChangedEvent, value); + } + + /// + /// Occurs when the measurements for vertical snap points change. + /// + public event EventHandler? VerticalSnapPointsChanged + { + add => AddHandler(VerticalSnapPointsChangedEvent, value); + remove => RemoveHandler(VerticalSnapPointsChangedEvent, value); + } + + /// + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// + public bool AreHorizontalSnapPointsRegular + { + get { return GetValue(AreHorizontalSnapPointsRegularProperty); } + set { SetValue(AreHorizontalSnapPointsRegularProperty, value); } + } + + /// + /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// + public bool AreVerticalSnapPointsRegular + { + get { return GetValue(AreVerticalSnapPointsRegularProperty); } + set { SetValue(AreVerticalSnapPointsRegularProperty, value); } + } + protected override Size MeasureOverride(Size availableSize) { if (!IsEffectivelyVisible) @@ -145,6 +212,8 @@ namespace Avalonia.Controls finally { _isInLayout = false; + + RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent)); } } @@ -622,6 +691,125 @@ namespace Avalonia.Controls Invalidate(c); } + /// + public IReadOnlyList GetIrregularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment) + { + var snapPoints = new List(); + + switch (orientation) + { + case Orientation.Horizontal: + if (AreHorizontalSnapPointsRegular) + throw new InvalidOperationException(); + if (Orientation == Orientation.Horizontal) + { + foreach (var child in VisualChildren) + { + double snapPoint = 0; + + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + snapPoint = child.Bounds.Left; + break; + case SnapPointsAlignment.Center: + snapPoint = child.Bounds.Center.X; + break; + case SnapPointsAlignment.Far: + snapPoint = child.Bounds.Right; + break; + } + + snapPoints.Add(snapPoint); + } + } + break; + case Orientation.Vertical: + if (AreVerticalSnapPointsRegular) + throw new InvalidOperationException(); + if (Orientation == Orientation.Vertical) + { + foreach (var child in VisualChildren) + { + double snapPoint = 0; + + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + snapPoint = child.Bounds.Top; + break; + case SnapPointsAlignment.Center: + snapPoint = child.Bounds.Center.Y; + break; + case SnapPointsAlignment.Far: + snapPoint = child.Bounds.Bottom; + break; + } + + snapPoints.Add(snapPoint); + } + } + break; + } + + return snapPoints; + } + + /// + public double GetRegularSnapPoints(Orientation orientation, SnapPointsAlignment snapPointsAlignment, out double offset) + { + offset = 0f; + var firstRealizedChild = _realizedElements?.Elements.FirstOrDefault(); + + if (firstRealizedChild == null) + { + return 0; + } + + double snapPoint = 0; + + switch (Orientation) + { + case Orientation.Horizontal: + if (!AreHorizontalSnapPointsRegular) + throw new InvalidOperationException(); + + snapPoint = firstRealizedChild.Bounds.Width; + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + offset = 0; + break; + case SnapPointsAlignment.Center: + offset = (firstRealizedChild.Bounds.Right - firstRealizedChild.Bounds.Left) / 2; + break; + case SnapPointsAlignment.Far: + offset = firstRealizedChild.Bounds.Width; + break; + } + break; + case Orientation.Vertical: + if (!AreVerticalSnapPointsRegular) + throw new InvalidOperationException(); + snapPoint = firstRealizedChild.Bounds.Height; + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + offset = 0; + break; + case SnapPointsAlignment.Center: + offset = (firstRealizedChild.Bounds.Bottom - firstRealizedChild.Bounds.Top) / 2; + break; + case SnapPointsAlignment.Far: + offset = firstRealizedChild.Bounds.Height; + break; + } + break; + } + + return snapPoint; + } + /// /// Stores the realized element state for a . /// diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml index ec0b876c71..dc18d65797 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml @@ -19,6 +19,7 @@ + @@ -29,6 +30,8 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> Date: Mon, 23 Jan 2023 12:20:14 +0000 Subject: [PATCH 10/13] add irregular snap points to virtualizing stack panel --- src/Avalonia.Controls/ListBox.cs | 5 +- .../VirtualizingStackPanel.cs | 68 ++++++++++++------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 775e0ae0cb..8b1a307182 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -21,10 +21,7 @@ namespace Avalonia.Controls /// The default value for the property. /// private static readonly FuncTemplate DefaultPanel = - new FuncTemplate(() => new VirtualizingStackPanel() - { - AreVerticalSnapPointsRegular= true, - }); + new FuncTemplate(() => new VirtualizingStackPanel()); /// /// Defines the property. diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index f60c67b577..a59fffa704 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -703,21 +703,29 @@ namespace Avalonia.Controls throw new InvalidOperationException(); if (Orientation == Orientation.Horizontal) { - foreach (var child in VisualChildren) + var averageElementSize = EstimateElementSizeU(); + double snapPoint = 0; + for (var i = 0; i < Items.Count; i++) { - double snapPoint = 0; - - switch (snapPointsAlignment) + var container = ContainerFromIndex(i); + if (container != null) + { + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + snapPoint = container.Bounds.Left; + break; + case SnapPointsAlignment.Center: + snapPoint = container.Bounds.Center.X; + break; + case SnapPointsAlignment.Far: + snapPoint = container.Bounds.Right; + break; + } + } + else { - case SnapPointsAlignment.Near: - snapPoint = child.Bounds.Left; - break; - case SnapPointsAlignment.Center: - snapPoint = child.Bounds.Center.X; - break; - case SnapPointsAlignment.Far: - snapPoint = child.Bounds.Right; - break; + snapPoint += averageElementSize; } snapPoints.Add(snapPoint); @@ -729,21 +737,29 @@ namespace Avalonia.Controls throw new InvalidOperationException(); if (Orientation == Orientation.Vertical) { - foreach (var child in VisualChildren) + var averageElementSize = EstimateElementSizeU(); + double snapPoint = 0; + for (var i = 0; i < Items.Count; i++) { - double snapPoint = 0; - - switch (snapPointsAlignment) + var container = ContainerFromIndex(i); + if (container != null) + { + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Near: + snapPoint = container.Bounds.Top; + break; + case SnapPointsAlignment.Center: + snapPoint = container.Bounds.Center.Y; + break; + case SnapPointsAlignment.Far: + snapPoint = container.Bounds.Bottom; + break; + } + } + else { - case SnapPointsAlignment.Near: - snapPoint = child.Bounds.Top; - break; - case SnapPointsAlignment.Center: - snapPoint = child.Bounds.Center.Y; - break; - case SnapPointsAlignment.Far: - snapPoint = child.Bounds.Bottom; - break; + snapPoint += averageElementSize; } snapPoints.Add(snapPoint); From feddc7e1c48c3d478bc94873e08be8fe2aea3f10 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 23 Jan 2023 13:46:46 +0000 Subject: [PATCH 11/13] make AreVerticalSnapPointsRegular and AreHorizontalSnapPointsRegular styled properties --- src/Avalonia.Controls/ItemsControl.cs | 35 ++++++++++++- .../Presenters/ItemsPresenter.cs | 49 ++++++++++++++++++- .../Primitives/IScrollSnapPointsInfo.cs | 8 +-- .../VirtualizingStackPanel.cs | 32 ++++++++++-- .../Controls/ItemsControl.xaml | 2 + .../Controls/ListBox.xaml | 3 +- .../Controls/ItemsControl.xaml | 2 + .../Controls/ListBox.xaml | 6 ++- 8 files changed, 124 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1948fda928..db49da85e8 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -74,6 +74,18 @@ namespace Avalonia.Controls /// public static readonly StyledProperty DisplayMemberBindingProperty = AvaloniaProperty.Register(nameof(DisplayMemberBinding)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); /// /// Gets or sets the to use for binding to the display member of each item. @@ -94,6 +106,7 @@ namespace Avalonia.Controls private Tuple? _containerBeingPrepared; private ScrollViewer? _scrollViewer; private ItemsPresenter? _itemsPresenter; + private IScrollSnapPointsInfo? _scrolSnapPointInfo; /// /// Initializes a new instance of the class. @@ -245,6 +258,24 @@ namespace Avalonia.Controls } } + /// + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// + public bool AreHorizontalSnapPointsRegular + { + get { return GetValue(AreHorizontalSnapPointsRegularProperty); } + set { SetValue(AreHorizontalSnapPointsRegularProperty, value); } + } + + /// + /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// + public bool AreVerticalSnapPointsRegular + { + get { return GetValue(AreVerticalSnapPointsRegularProperty); } + set { SetValue(AreVerticalSnapPointsRegularProperty, value); } + } + /// /// Returns the container for the item at the specified index. /// @@ -296,8 +327,6 @@ namespace Avalonia.Controls /// Gets the currently realized containers. /// public IEnumerable GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty(); - public bool AreHorizontalSnapPointsRegular => _itemsPresenter?.AreHorizontalSnapPointsRegular ?? false; - public bool AreVerticalSnapPointsRegular => _itemsPresenter?.AreVerticalSnapPointsRegular ?? false; /// /// Creates or a container that can be used to display an item. @@ -399,6 +428,8 @@ namespace Avalonia.Controls base.OnApplyTemplate(e); _scrollViewer = e.NameScope.Find("PART_ScrollViewer"); _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter"); + + _scrolSnapPointInfo = _itemsPresenter as IScrollSnapPointsInfo; } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index e3332ef3a2..8594b584fa 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -21,8 +21,21 @@ namespace Avalonia.Controls.Presenters private PanelContainerGenerator? _generator; private ILogicalScrollable? _logicalScrollable; + private IScrollSnapPointsInfo? _scrollSnapPointsInfo; private EventHandler? _scrollInvalidated; + /// + /// Defines the property. + /// + public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); + /// /// Defines the event. /// @@ -125,8 +138,23 @@ namespace Avalonia.Controls.Presenters Size IScrollable.Extent => _logicalScrollable?.Extent ?? default; Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; - public bool AreHorizontalSnapPointsRegular => Panel is IScrollSnapPointsInfo scrollSnapPointsInfo ? scrollSnapPointsInfo.AreHorizontalSnapPointsRegular : false; - public bool AreVerticalSnapPointsRegular => Panel is IScrollSnapPointsInfo scrollSnapPointsInfo ? scrollSnapPointsInfo.AreVerticalSnapPointsRegular : false; + /// + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// + public bool AreHorizontalSnapPointsRegular + { + get { return GetValue(AreHorizontalSnapPointsRegularProperty); } + set { SetValue(AreHorizontalSnapPointsRegularProperty, value); } + } + + /// + /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// + public bool AreVerticalSnapPointsRegular + { + get { return GetValue(AreVerticalSnapPointsRegularProperty); } + set { SetValue(AreVerticalSnapPointsRegularProperty, value); } + } public override sealed void ApplyTemplate() { @@ -139,9 +167,16 @@ namespace Avalonia.Controls.Presenters Panel = ItemsPanel.Build(); Panel.SetValue(TemplatedParentProperty, TemplatedParent); + _scrollSnapPointsInfo = Panel as IScrollSnapPointsInfo; LogicalChildren.Add(Panel); VisualChildren.Add(Panel); + if (_scrollSnapPointsInfo != null) + { + _scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular; + _scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular; + } + if (Panel is VirtualizingPanel v) v.Attach(ItemsControl); else @@ -205,6 +240,16 @@ namespace Avalonia.Controls.Presenters ResetState(); InvalidateMeasure(); } + else if(change.Property == AreHorizontalSnapPointsRegularProperty) + { + if (_scrollSnapPointsInfo != null) + _scrollSnapPointsInfo.AreHorizontalSnapPointsRegular = AreHorizontalSnapPointsRegular; + } + else if (change.Property == AreVerticalSnapPointsRegularProperty) + { + if (_scrollSnapPointsInfo != null) + _scrollSnapPointsInfo.AreVerticalSnapPointsRegular = AreVerticalSnapPointsRegular; + } } internal void Refresh() diff --git a/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs index d0462aff9e..7b33db0df2 100644 --- a/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs +++ b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs @@ -11,14 +11,14 @@ namespace Avalonia.Controls.Primitives public interface IScrollSnapPointsInfo { /// - /// Gets a value that indicates whether the horizontal snap points for the container are equidistant from each other. + /// Gets or sets a value that indicates whether the horizontal snap points for the container are equidistant from each other. /// - bool AreHorizontalSnapPointsRegular { get; } + bool AreHorizontalSnapPointsRegular { get; set; } /// - /// Gets a value that indicates whether the vertical snap points for the container are equidistant from each other. + /// Gets or sets a value that indicates whether the vertical snap points for the container are equidistant from each other. /// - bool AreVerticalSnapPointsRegular { get; } + bool AreVerticalSnapPointsRegular { get; set; } /// /// Returns the set of distances between irregular snap points for a specified orientation and alignment. diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index a59fffa704..6bcf3dcf5d 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); /// /// Defines the event. @@ -725,7 +725,20 @@ namespace Avalonia.Controls } else { - snapPoint += averageElementSize; + if (snapPoint == 0) + { + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Center: + snapPoint = averageElementSize / 2; + break; + case SnapPointsAlignment.Far: + snapPoint = averageElementSize; + break; + } + } + else + snapPoint += averageElementSize; } snapPoints.Add(snapPoint); @@ -759,7 +772,20 @@ namespace Avalonia.Controls } else { - snapPoint += averageElementSize; + if (snapPoint == 0) + { + switch (snapPointsAlignment) + { + case SnapPointsAlignment.Center: + snapPoint = averageElementSize / 2; + break; + case SnapPointsAlignment.Far: + snapPoint = averageElementSize; + break; + } + } + else + snapPoint += averageElementSize; } snapPoints.Add(snapPoint); diff --git a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml index 1306985f5f..19a29b9466 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ItemsControl.xaml @@ -9,6 +9,8 @@ CornerRadius="{TemplateBinding CornerRadius}" Padding="{TemplateBinding Padding}"> diff --git a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml index dc18d65797..4b9fb76b8a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml @@ -19,7 +19,6 @@ - @@ -37,6 +36,8 @@ IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}" AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"> diff --git a/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml b/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml index d85cf193fa..8cf4e0be08 100644 --- a/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ItemsControl.xaml @@ -10,6 +10,8 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> diff --git a/src/Avalonia.Themes.Simple/Controls/ListBox.xaml b/src/Avalonia.Themes.Simple/Controls/ListBox.xaml index e1257ef10f..eaa1f914ca 100644 --- a/src/Avalonia.Themes.Simple/Controls/ListBox.xaml +++ b/src/Avalonia.Themes.Simple/Controls/ListBox.xaml @@ -20,9 +20,13 @@ Background="{TemplateBinding Background}" HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}" IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}" - VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"> + VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}" + VerticalSnapPointsType="{TemplateBinding (ScrollViewer.VerticalSnapPointsType)}" + HorizontalSnapPointsType="{TemplateBinding (ScrollViewer.HorizontalSnapPointsType)}"> From 23ab4294b1122befb80d5da20bb8db1357846a64 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 24 Jan 2023 12:46:23 +0000 Subject: [PATCH 12/13] move snapping samples to separate page --- samples/ControlCatalog/MainView.xaml | 5 +- .../ControlCatalog/Pages/ScrollSnapPage.xaml | 222 ++++++++++++++++++ .../Pages/ScrollSnapPage.xaml.cs | 67 ++++++ .../Pages/ScrollViewerPage.xaml | 136 ----------- .../Pages/ScrollViewerPage.xaml.cs | 37 --- 5 files changed, 293 insertions(+), 174 deletions(-) create mode 100644 samples/ControlCatalog/Pages/ScrollSnapPage.xaml create mode 100644 samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 166b98436e..83776ec2c1 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -144,9 +144,12 @@ - + + + + diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml new file mode 100644 index 0000000000..e7e1060f31 --- /dev/null +++ b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml @@ -0,0 +1,222 @@ + + + Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen, or using the pointer wheel. + + + + + + + + + + + + + + + + Vertical Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Horizontal Snapping + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs new file mode 100644 index 0000000000..97aeb6bcdb --- /dev/null +++ b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Markup.Xaml; +using MiniMvvm; + +namespace ControlCatalog.Pages +{ + public class ScrollSnapPageViewModel : ViewModelBase + { + private SnapPointsType _snapPointsType; + private SnapPointsAlignment _snapPointsAlignment; + private bool _areSnapPointsRegular; + + public ScrollSnapPageViewModel() + { + + AvailableSnapPointsType = new List() + { + SnapPointsType.None, + SnapPointsType.Mandatory, + SnapPointsType.MandatorySingle + }; + + AvailableSnapPointsAlignment = new List() + { + SnapPointsAlignment.Near, + SnapPointsAlignment.Center, + SnapPointsAlignment.Far, + }; + } + + public bool AreSnapPointsRegular + { + get => _areSnapPointsRegular; + set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); + } + + public SnapPointsType SnapPointsType + { + get => _snapPointsType; + set => this.RaiseAndSetIfChanged(ref _snapPointsType, value); + } + + public SnapPointsAlignment SnapPointsAlignment + { + get => _snapPointsAlignment; + set => this.RaiseAndSetIfChanged(ref _snapPointsAlignment, value); + } + public List AvailableSnapPointsType { get; } + public List AvailableSnapPointsAlignment { get; } + } + public class ScrollSnapPage : UserControl + { + public ScrollSnapPage() + { + this.InitializeComponent(); + + DataContext = new ScrollSnapPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml index 0eceb9e07d..1741c796db 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml @@ -31,141 +31,5 @@ Source="/Assets/delicate-arch-896885_640.jpg" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs index 10244af5f3..dcd7a88a56 100644 --- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs @@ -11,9 +11,6 @@ namespace ControlCatalog.Pages private bool _allowAutoHide; private ScrollBarVisibility _horizontalScrollVisibility; private ScrollBarVisibility _verticalScrollVisibility; - private SnapPointsType _verticalSnapPointsType; - private SnapPointsAlignment _verticalSnapPointsAlignment; - private bool _areSnapPointsRegular; public ScrollViewerPageViewModel() { @@ -25,20 +22,6 @@ namespace ControlCatalog.Pages ScrollBarVisibility.Disabled, }; - AvailableSnapPointsType = new List() - { - SnapPointsType.None, - SnapPointsType.Mandatory, - SnapPointsType.MandatorySingle - }; - - AvailableSnapPointsAlignment = new List() - { - SnapPointsAlignment.Near, - SnapPointsAlignment.Center, - SnapPointsAlignment.Far, - }; - HorizontalScrollVisibility = ScrollBarVisibility.Auto; VerticalScrollVisibility = ScrollBarVisibility.Auto; AllowAutoHide = true; @@ -50,12 +33,6 @@ namespace ControlCatalog.Pages set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value); } - public bool AreSnapPointsRegular - { - get => _areSnapPointsRegular; - set => this.RaiseAndSetIfChanged(ref _areSnapPointsRegular, value); - } - public ScrollBarVisibility HorizontalScrollVisibility { get => _horizontalScrollVisibility; @@ -68,21 +45,7 @@ namespace ControlCatalog.Pages set => this.RaiseAndSetIfChanged(ref _verticalScrollVisibility, value); } - public SnapPointsType VerticalSnapPointsType - { - get => _verticalSnapPointsType; - set => this.RaiseAndSetIfChanged(ref _verticalSnapPointsType, value); - } - - public SnapPointsAlignment VerticalSnapPointsAlignment - { - get => _verticalSnapPointsAlignment; - set => this.RaiseAndSetIfChanged(ref _verticalSnapPointsAlignment, value); - } - public List AvailableVisibility { get; } - public List AvailableSnapPointsType { get; } - public List AvailableSnapPointsAlignment { get; } } public class ScrollViewerPage : UserControl From baab50e94cfd2ca10b134ed71be87bcc8217a257 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 24 Jan 2023 13:08:53 +0000 Subject: [PATCH 13/13] fix center snap alignment --- samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs | 1 + src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs index 97aeb6bcdb..384dc67c66 100644 --- a/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs @@ -50,6 +50,7 @@ namespace ControlCatalog.Pages public List AvailableSnapPointsType { get; } public List AvailableSnapPointsAlignment { get; } } + public class ScrollSnapPage : UserControl { public ScrollSnapPage() diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 3b291841fe..7d5b5e1490 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -869,7 +869,7 @@ namespace Avalonia.Controls.Presenters if (!_areVerticalSnapPointsRegular) { - _verticalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Vertical, HorizontalSnapPointsAlignment); + _verticalSnapPoints = scrollSnapPointsInfo.GetIrregularSnapPoints(Layout.Orientation.Vertical, VerticalSnapPointsAlignment); } else {