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/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs
index 1726028a3f..a9bf150ff9 100644
--- a/samples/ControlCatalog/MainView.xaml.cs
+++ b/samples/ControlCatalog/MainView.xaml.cs
@@ -60,7 +60,7 @@ namespace ControlCatalog
{
if (flowDirections.SelectedItem is FlowDirection flowDirection)
{
- this.FlowDirection = flowDirection;
+ TopLevel.GetTopLevel(this).FlowDirection = flowDirection;
}
};
diff --git a/samples/ControlCatalog/Pages/GesturePage.cs b/samples/ControlCatalog/Pages/GesturePage.cs
index ee10f21317..0bb8f38219 100644
--- a/samples/ControlCatalog/Pages/GesturePage.cs
+++ b/samples/ControlCatalog/Pages/GesturePage.cs
@@ -6,6 +6,7 @@ using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Rendering.Composition;
+using Avalonia.Utilities;
namespace ControlCatalog.Pages
{
@@ -53,6 +54,7 @@ namespace ControlCatalog.Pages
{
_currentScale = 1;
compositionVisual.Scale = new Vector3(1,1,1);
+ compositionVisual.Offset = default;
image.InvalidateMeasure();
}
};
@@ -100,13 +102,19 @@ namespace ControlCatalog.Pages
{
InitComposition(control!);
- isZooming = true;
-
if(compositionVisual != null)
{
var scale = _currentScale * (float)e.Scale;
+ if (scale <= 1)
+ {
+ scale = 1;
+ compositionVisual.Offset = default;
+ }
+
compositionVisual.Scale = new(scale, scale, 1);
+
+ e.Handled = true;
}
});
@@ -114,8 +122,6 @@ namespace ControlCatalog.Pages
{
InitComposition(control!);
- isZooming = false;
-
if (compositionVisual != null)
{
_currentScale = compositionVisual.Scale.X;
@@ -126,11 +132,19 @@ namespace ControlCatalog.Pages
{
InitComposition(control!);
- if (compositionVisual != null && !isZooming)
+ if (compositionVisual != null && _currentScale != 1)
{
- currentOffset -= new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0);
+ currentOffset += new Vector3((float)e.Delta.X, (float)e.Delta.Y, 0);
+
+ var currentSize = control.Bounds.Size * _currentScale;
+
+ currentOffset = new Vector3((float)MathUtilities.Clamp(currentOffset.X, 0, currentSize.Width - control.Bounds.Width),
+ (float)MathUtilities.Clamp(currentOffset.Y, 0, currentSize.Height - control.Bounds.Height),
+ 0);
- compositionVisual.Offset = currentOffset;
+ compositionVisual.Offset = currentOffset * -1;
+
+ e.Handled = true;
}
});
}
@@ -173,6 +187,8 @@ namespace ControlCatalog.Pages
if (ballCompositionVisual != null)
{
ballCompositionVisual.Offset = defaultOffset + new System.Numerics.Vector3((float)e.Delta.X * 0.4f, (float)e.Delta.Y * 0.4f, 0) * (inverse ? -1 : 1);
+
+ e.Handled = true;
}
});
@@ -187,11 +203,6 @@ namespace ControlCatalog.Pages
void InitComposition(Control control)
{
- if (ballCompositionVisual != null)
- {
- return;
- }
-
ballCompositionVisual = ElementComposition.GetElementVisual(ball);
if (ballCompositionVisual != null)
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 1903e50ed7..fa8714959b 100644
--- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml
+++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml
@@ -3,12 +3,13 @@
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.
+
@@ -24,6 +25,7 @@
@@ -31,6 +33,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 dcd7a88a56..a097f1f951 100644
--- a/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ScrollViewerPage.xaml.cs
@@ -9,6 +9,7 @@ namespace ControlCatalog.Pages
public class ScrollViewerPageViewModel : ViewModelBase
{
private bool _allowAutoHide;
+ private bool _enableInertia;
private ScrollBarVisibility _horizontalScrollVisibility;
private ScrollBarVisibility _verticalScrollVisibility;
@@ -25,6 +26,7 @@ namespace ControlCatalog.Pages
HorizontalScrollVisibility = ScrollBarVisibility.Auto;
VerticalScrollVisibility = ScrollBarVisibility.Auto;
AllowAutoHide = true;
+ EnableInertia = true;
}
public bool AllowAutoHide
@@ -33,6 +35,12 @@ namespace ControlCatalog.Pages
set => this.RaiseAndSetIfChanged(ref _allowAutoHide, value);
}
+ public bool EnableInertia
+ {
+ get => _enableInertia;
+ set => this.RaiseAndSetIfChanged(ref _enableInertia, value);
+ }
+
public ScrollBarVisibility HorizontalScrollVisibility
{
get => _horizontalScrollVisibility;
diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs
index eea7c3b7d1..3b83d0cb87 100644
--- a/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs
+++ b/src/Avalonia.Base/Input/GestureRecognizers/PinchGestureRecognizer.cs
@@ -57,7 +57,10 @@ namespace Avalonia.Input
var scale = distance / _initialDistance;
- _target?.RaiseEvent(new PinchEventArgs(scale, _origin));
+ var pinchEventArgs = new PinchEventArgs(scale, _origin);
+ _target?.RaiseEvent(pinchEventArgs);
+
+ e.Handled = pinchEventArgs.Handled;
}
}
}
diff --git a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
index 23bab13fc8..991694cc60 100644
--- a/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
+++ b/src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics;
using Avalonia.Input.GestureRecognizers;
namespace Avalonia.Input
@@ -88,7 +89,10 @@ namespace Avalonia.Input
}
_pullInProgress = true;
- _target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection));
+ var pullEventArgs = new PullGestureEventArgs(_gestureId, delta, PullDirection);
+ _target?.RaiseEvent(pullEventArgs);
+
+ e.Handled = pullEventArgs.Handled;
}
}
diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
index 790439245a..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;
@@ -23,7 +27,8 @@ namespace Avalonia.Input.GestureRecognizers
// Movement per second
private Vector _inertia;
private ulong? _lastMoveTimestamp;
-
+ private bool _isScrollInertiaEnabled;
+
///
/// Defines the property.
///
@@ -42,6 +47,15 @@ namespace Avalonia.Input.GestureRecognizers
o => o.CanVerticallyScroll,
(o, v) => o.CanVerticallyScroll = v);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty IsScrollInertiaEnabledProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(IsScrollInertiaEnabled),
+ o => o.IsScrollInertiaEnabled,
+ (o, v) => o.IsScrollInertiaEnabled = v);
+
///
/// Defines the property.
///
@@ -68,6 +82,15 @@ namespace Avalonia.Input.GestureRecognizers
get => _canVerticallyScroll;
set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
}
+
+ ///
+ /// Gets or sets whether the gesture should include inertia in it's behavior.
+ ///
+ public bool IsScrollInertiaEnabled
+ {
+ get => _isScrollInertiaEnabled;
+ set => SetAndRaise(IsScrollInertiaEnabledProperty, ref _isScrollInertiaEnabled, value);
+ }
///
/// Gets or sets a value indicating the distance the pointer moves before scrolling is started
@@ -97,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)
@@ -168,7 +188,8 @@ namespace Avalonia.Input.GestureRecognizers
if (_inertia == default
|| e.Timestamp == 0
|| _lastMoveTimestamp == 0
- || e.Timestamp - _lastMoveTimestamp > 200)
+ || e.Timestamp - _lastMoveTimestamp > 200
+ || !IsScrollInertiaEnabled)
EndGesture();
else
{
@@ -176,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
@@ -187,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.Base/Media/Brush.cs b/src/Avalonia.Base/Media/Brush.cs
index b9a560ad8f..accabce145 100644
--- a/src/Avalonia.Base/Media/Brush.cs
+++ b/src/Avalonia.Base/Media/Brush.cs
@@ -11,7 +11,7 @@ namespace Avalonia.Media
/// Describes how an area is painted.
///
[TypeConverter(typeof(BrushConverter))]
- public abstract class Brush : Animatable
+ public abstract class Brush : Animatable, IBrush
{
///
/// Defines the property.
diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs
index 65575617d0..0ec7152359 100644
--- a/src/Avalonia.Base/Media/GlyphRun.cs
+++ b/src/Avalonia.Base/Media/GlyphRun.cs
@@ -13,14 +13,22 @@ namespace Avalonia.Media
///
public sealed class GlyphRun : IDisposable
{
+ private readonly static IPlatformRenderInterface s_renderInterface;
+
private IRef? _platformImpl;
private double _fontRenderingEmSize;
private int _biDiLevel;
private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlyMemory _characters;
private IReadOnlyList _glyphInfos;
+ private Point? _baselineOrigin;
private bool _hasOneCharPerCluster; // if true, character index and cluster are similar
+ static GlyphRun()
+ {
+ s_renderInterface = AvaloniaLocator.Current.GetRequiredService();
+ }
+
///
/// Initializes a new instance of the class by specifying properties of the class.
///
@@ -28,15 +36,17 @@ namespace Avalonia.Media
/// The rendering em size.
/// The characters.
/// The glyph indices.
+ /// The baseline origin of the run.
/// The bidi level.
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlyMemory characters,
IReadOnlyList glyphIndices,
+ Point? baselineOrigin = null,
int biDiLevel = 0)
: this(glyphTypeface, fontRenderingEmSize, characters,
- CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), biDiLevel)
+ CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), baselineOrigin, biDiLevel)
{
_hasOneCharPerCluster = true;
}
@@ -48,12 +58,14 @@ namespace Avalonia.Media
/// The rendering em size.
/// The characters.
/// The list of glyphs used.
+ /// The baseline origin of the run.
/// The bidi level.
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlyMemory characters,
IReadOnlyList glyphInfos,
+ Point? baselineOrigin = null,
int biDiLevel = 0)
{
GlyphTypeface = glyphTypeface;
@@ -64,6 +76,8 @@ namespace Avalonia.Media
_glyphInfos = glyphInfos;
+ _baselineOrigin = baselineOrigin;
+
_biDiLevel = biDiLevel;
}
@@ -72,6 +86,7 @@ namespace Avalonia.Media
_glyphInfos = Array.Empty();
GlyphTypeface = Typeface.Default.GlyphTypeface;
_platformImpl = platformImpl;
+ _baselineOrigin = platformImpl.Item.BaselineOrigin;
}
private static IReadOnlyList CreateGlyphInfos(IReadOnlyList glyphIndices,
@@ -147,9 +162,13 @@ namespace Avalonia.Media
=> _glyphRunMetrics ??= CreateGlyphRunMetrics();
///
- /// Gets the baseline origin of the.
+ /// Gets or sets the baseline origin of the.
///
- public Point BaselineOrigin => PlatformImpl.Item.BaselineOrigin;
+ public Point BaselineOrigin
+ {
+ get => _baselineOrigin ?? default;
+ set => Set(ref _baselineOrigin, value);
+ }
///
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the .
@@ -204,9 +223,7 @@ namespace Avalonia.Media
/// The geometry returned contains the combined geometry of all glyphs in the glyph run.
public Geometry BuildGeometry()
{
- var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService();
-
- var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this);
+ var geometryImpl = s_renderInterface.BuildGlyphRunGeometry(this);
return new PlatformGeometry(geometryImpl);
}
@@ -802,9 +819,11 @@ namespace Avalonia.Media
private IRef CreateGlyphRunImpl()
{
- var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService();
-
- var platformImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos);
+ var platformImpl = s_renderInterface.CreateGlyphRun(
+ GlyphTypeface,
+ FontRenderingEmSize,
+ GlyphInfos,
+ _baselineOrigin ?? new Point(0, -GlyphTypeface.Metrics.Ascent * Scale));
_platformImpl = RefCountable.Create(platformImpl);
diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
index ac196bf7e0..7f23ac98b4 100644
--- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
+++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
@@ -185,7 +185,7 @@ namespace Avalonia.Media.TextFormatting
ShapedBuffer.FontRenderingEmSize,
Text,
ShapedBuffer,
- BidiLevel);
+ biDiLevel: BidiLevel);
}
public void Dispose()
diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
index e2160f21d2..41e792d58e 100644
--- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
+++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
@@ -168,8 +168,9 @@ namespace Avalonia.Platform
/// The glyph typeface.
/// The font rendering em size.
/// The list of glyphs.
+ /// The baseline origin of the run. Can be null.
/// An .
- IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos);
+ IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin);
///
/// Creates a backend-specific object using a low-level API graphics context
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 503187e2d3..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.
///
@@ -193,6 +221,14 @@ namespace Avalonia.Controls
nameof(IsScrollChainingEnabled),
defaultValue: true);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly AttachedProperty IsScrollInertiaEnabledProperty =
+ AvaloniaProperty.RegisterAttached(
+ nameof(IsScrollInertiaEnabled),
+ defaultValue: true);
+
///
/// Defines the event.
///
@@ -421,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.
///
@@ -444,6 +516,15 @@ namespace Avalonia.Controls
set => SetValue(IsScrollChainingEnabledProperty, value);
}
+ ///
+ /// Gets or sets whether scroll gestures should include inertia in their behavior and value.
+ ///
+ public bool IsScrollInertiaEnabled
+ {
+ get => GetValue(IsScrollInertiaEnabledProperty);
+ set => SetValue(IsScrollInertiaEnabledProperty, value);
+ }
+
///
/// Scrolls the content up one line.
///
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/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index ff241dce7a..3bd566c622 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -581,12 +581,21 @@ namespace Avalonia.Controls
/// The event args.
private void HandleInput(RawInputEventArgs e)
{
- if (e is RawPointerEventArgs pointerArgs)
+ if (PlatformImpl != null)
{
- pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position);
- }
+ if (e is RawPointerEventArgs pointerArgs)
+ {
+ pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position);
+ }
- _inputManager?.ProcessInput(e);
+ _inputManager?.ProcessInput(e);
+ }
+ else
+ {
+ Logger.TryGet(LogEventLevel.Warning, LogArea.Control)?.Log(
+ this,
+ "PlatformImpl is null, couldn't handle input.");
+ }
}
private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e)
@@ -606,6 +615,13 @@ namespace Avalonia.Controls
KeyboardDevice.Instance?.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
}
+ protected override bool BypassFlowDirectionPolicies => true;
+
+ public override void InvalidateMirrorTransform()
+ {
+ // Do nothing becuase TopLevel should't apply MirrorTransform on himself.
+ }
+
ITextInputMethodImpl? ITextInputMethodRoot.InputMethod =>
(PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod;
}
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.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
index 572ff1c876..514d3b3e07 100644
--- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
+++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
@@ -120,7 +120,11 @@ namespace Avalonia.Headless
return new HeadlessGeometryStub(new Rect(glyphRun.Size));
}
- public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos)
+ public IGlyphRunImpl CreateGlyphRun(
+ IGlyphTypeface glyphTypeface,
+ double fontRenderingEmSize,
+ IReadOnlyList glyphInfos,
+ Point baselineOrigin)
{
return new HeadlessGlyphRunStub();
}
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 71df4d419f..7a8c15bf60 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
@@ -34,12 +34,17 @@
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}">
+ CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
+ IsScrollInertiaEnabled="{TemplateBinding IsScrollInertiaEnabled}"/>
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}"
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index 6630f0707e..b4297a7c33 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -201,7 +201,11 @@ namespace Avalonia.Skia
return new WriteableBitmapImpl(size, dpi, format, alphaFormat);
}
- public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos)
+ public IGlyphRunImpl CreateGlyphRun(
+ IGlyphTypeface glyphTypeface,
+ double fontRenderingEmSize,
+ IReadOnlyList glyphInfos,
+ Point baselineOrigin)
{
if (glyphTypeface == null)
{
@@ -252,7 +256,6 @@ namespace Avalonia.Skia
var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
var height = glyphTypeface.Metrics.LineSpacing * scale;
- var baselineOrigin = new Point(0, -glyphTypeface.Metrics.Ascent * scale);
return new GlyphRunImpl(builder.Build(), new Size(width, height), baselineOrigin);
}
diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
index 461950b728..fbf8097ece 100644
--- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
+++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
@@ -158,7 +158,8 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
- public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos)
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize,
+ IReadOnlyList glyphInfos, Point baselineOrigin)
{
var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface;
@@ -207,7 +208,6 @@ namespace Avalonia.Direct2D1
var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
var height = glyphTypeface.Metrics.LineSpacing * scale;
- var baselineOrigin = new Point(0, -glyphTypeface.Metrics.Ascent * scale);
return new GlyphRunImpl(run, new Size(width, height), baselineOrigin);
}
@@ -257,7 +257,7 @@ namespace Avalonia.Direct2D1
sink.Close();
}
- var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin;
+ var (baselineOriginX, baselineOriginY) = glyphRun.PlatformImpl.Item.BaselineOrigin;
var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry(
Direct2D1Factory,
diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
index a05bfbea4c..43feb75c08 100644
--- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
@@ -188,7 +188,7 @@ namespace Avalonia.Base.UnitTests.Media
glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]);
}
- return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, bidiLevel);
+ return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel);
}
}
}
diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
index ee501a86c1..76c7fe97fc 100644
--- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
+++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
@@ -77,7 +77,8 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException();
}
- public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos)
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize,
+ IReadOnlyList glyphInfos, Point baselineOrigin)
{
throw new NotImplementedException();
}
diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
index 9b148c798b..37b79855db 100644
--- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
+++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
@@ -123,7 +123,8 @@ namespace Avalonia.Benchmarks
return new MockStreamGeometryImpl();
}
- public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos)
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize,
+ IReadOnlyList glyphInfos, Point baselineOrigin)
{
return new MockGlyphRun(glyphInfos);
}
diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
index 59c7ac3786..bfe03030c6 100644
--- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
+++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs
@@ -217,7 +217,7 @@ namespace Avalonia.Skia.UnitTests.Media
shapedBuffer.FontRenderingEmSize,
shapedBuffer.Text,
shapedBuffer.GlyphInfos,
- shapedBuffer.BidiLevel);
+ biDiLevel: shapedBuffer.BidiLevel);
if(shapedBuffer.BidiLevel == 1)
{
diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
index 30f949ccb8..93073faefb 100644
--- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
+++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
@@ -149,7 +149,8 @@ namespace Avalonia.UnitTests
throw new NotImplementedException();
}
- public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos)
+ public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize,
+ IReadOnlyList glyphInfos, Point baselineOrigin)
{
return new MockGlyphRun(glyphInfos);
}