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..384dc67c66
--- /dev/null
+++ b/samples/ControlCatalog/Pages/ScrollSnapPage.xaml.cs
@@ -0,0 +1,68 @@
+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 5042d7823b..fa8714959b 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.
@@ -33,6 +33,5 @@
Source="/Assets/delicate-arch-896885_640.jpg" />
-
diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
index fdab2df0bf..7c1ee13eed 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;
@@ -116,9 +120,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)
@@ -196,6 +197,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
@@ -207,7 +209,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/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index d19a04eb21..db49da85e8 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.
@@ -72,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.
@@ -91,6 +105,8 @@ namespace Avalonia.Controls
private IDataTemplate? _displayMemberItemTemplate;
private Tuple? _containerBeingPrepared;
private ScrollViewer? _scrollViewer;
+ private ItemsPresenter? _itemsPresenter;
+ private IScrollSnapPointsInfo? _scrolSnapPointInfo;
///
/// Initializes a new instance of the class.
@@ -203,6 +219,63 @@ 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;
+ }
+ }
+ }
+
+ ///
+ /// 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.
///
@@ -255,7 +328,6 @@ namespace Avalonia.Controls
///
public IEnumerable GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty();
-
///
/// Creates or a container that can be used to display an item.
///
@@ -355,6 +427,9 @@ namespace Avalonia.Controls
{
base.OnApplyTemplate(e);
_scrollViewer = e.NameScope.Find("PART_ScrollViewer");
+ _itemsPresenter = e.NameScope.Find("PART_ItemsPresenter");
+
+ _scrolSnapPointInfo = _itemsPresenter as IScrollSnapPointsInfo;
}
///
@@ -671,5 +746,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..8594b584fa 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.
@@ -19,8 +21,37 @@ 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.
+ ///
+ 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);
+
static ItemsPresenter()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
@@ -83,12 +114,48 @@ 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;
Size IScrollable.Extent => _logicalScrollable?.Extent ?? default;
Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default;
+ ///
+ /// 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()
{
if (Panel is null && ItemsControl is not null)
@@ -100,14 +167,36 @@ 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
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)
@@ -151,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()
@@ -204,5 +303,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;
+ }
}
}
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 0cfe4bada1..7d5b5e1490 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,42 @@ 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);
+ 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);
+ set => SetValue(VerticalSnapPointsAlignmentProperty, value);
+ }
+
///
/// Gets or sets if scroll chaining is enabled. The default value is true.
///
@@ -424,6 +496,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 +525,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)
@@ -458,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 if(_verticalSnapPoints != null)
+ {
+ 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);
@@ -466,12 +639,37 @@ 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 if(_horizontalSnapPoints != null)
+ {
+ 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);
}
- Vector newOffset = new Vector(x, y);
+ Vector newOffset = SnapOffset(new Vector(x, y));
+
bool offsetChanged = newOffset != Offset;
Offset = newOffset;
@@ -485,10 +683,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 +859,145 @@ 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, VerticalSnapPointsAlignment);
+ }
+ 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;
+
+ var diff = GetAlignedDiff();
+
+ if (VerticalSnapPointsType != SnapPointsType.None)
+ {
+ offset = new Vector(offset.X, offset.Y + diff.Y);
+ 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 - diff.Y);
+ }
+
+ if (HorizontalSnapPointsType != SnapPointsType.None)
+ {
+ offset = new Vector(offset.X + diff.X, offset.Y);
+ 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 - diff.X, offset.Y);
+
+ }
+
+ 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;
+ }
+
+ 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)];
+ }
}
}
diff --git a/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs
new file mode 100644
index 0000000000..7b33db0df2
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/IScrollSnapPointsInfo.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+
+namespace Avalonia.Controls.Primitives
+{
+ ///
+ /// Describes snap point behavior for objects that contain and present items.
+ ///
+ public interface IScrollSnapPointsInfo
+ {
+ ///
+ /// Gets or sets a value that indicates whether the horizontal snap points for the container are equidistant from each other.
+ ///
+ bool AreHorizontalSnapPointsRegular { get; set; }
+
+ ///
+ /// Gets or sets a value that indicates whether the vertical snap points for the container are equidistant from each other.
+ ///
+ bool AreVerticalSnapPointsRegular { get; set; }
+
+ ///
+ /// 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
new file mode 100644
index 0000000000..77b93c50a0
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/SnapPointsAlignment.cs
@@ -0,0 +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
new file mode 100644
index 0000000000..130fb85f77
--- /dev/null
+++ b/src/Avalonia.Controls/Primitives/SnapPointsType.cs
@@ -0,0 +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
+ }
+}
diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs
index d16cd209c0..1c23919d0e 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 AttachedProperty HorizontalSnapPointsAlignmentProperty =
+ AvaloniaProperty.RegisterAttached(
+ nameof(HorizontalSnapPointsAlignment));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AttachedProperty VerticalSnapPointsAlignmentProperty =
+ AvaloniaProperty.RegisterAttached(
+ nameof(VerticalSnapPointsAlignment));
+
///
/// Defines the VerticalScrollBarViewportSize property.
///
@@ -429,6 +457,42 @@ 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);
+ 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.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs
index 964a1055df..aa63ac975e 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,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); }
+ }
+
///
/// Gets the next control in the specified direction.
///
@@ -274,6 +342,8 @@ namespace Avalonia.Controls
ArrangeChild(child, rcChild, finalSize, Orientation);
}
+ RaiseEvent(new RoutedEventArgs(Orientation == Orientation.Horizontal ? HorizontalSnapPointsChangedEvent : VerticalSnapPointsChangedEvent));
+
return finalSize;
}
@@ -285,5 +355,124 @@ 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 + Spacing;
+ }
}
}
diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs
index f2b42b0b7e..c5276741b6 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,167 @@ 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)
+ {
+ var averageElementSize = EstimateElementSizeU();
+ double snapPoint = 0;
+ for (var i = 0; i < Items.Count; i++)
+ {
+ 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
+ {
+ if (snapPoint == 0)
+ {
+ switch (snapPointsAlignment)
+ {
+ case SnapPointsAlignment.Center:
+ snapPoint = averageElementSize / 2;
+ break;
+ case SnapPointsAlignment.Far:
+ snapPoint = averageElementSize;
+ break;
+ }
+ }
+ else
+ snapPoint += averageElementSize;
+ }
+
+ snapPoints.Add(snapPoint);
+ }
+ }
+ break;
+ case Orientation.Vertical:
+ if (AreVerticalSnapPointsRegular)
+ throw new InvalidOperationException();
+ if (Orientation == Orientation.Vertical)
+ {
+ var averageElementSize = EstimateElementSizeU();
+ double snapPoint = 0;
+ for (var i = 0; i < Items.Count; i++)
+ {
+ 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
+ {
+ if (snapPoint == 0)
+ {
+ switch (snapPointsAlignment)
+ {
+ case SnapPointsAlignment.Center:
+ snapPoint = averageElementSize / 2;
+ break;
+ case SnapPointsAlignment.Far:
+ snapPoint = averageElementSize;
+ break;
+ }
+ }
+ else
+ snapPoint += averageElementSize;
+ }
+
+ 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/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 ec0b876c71..4b9fb76b8a 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
@@ -29,11 +29,15 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
index 15cd9428f3..7a8c15bf60 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/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)}">
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}"