diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml
index 179f64233e..94e81c520c 100644
--- a/samples/ControlCatalog/App.xaml
+++ b/samples/ControlCatalog/App.xaml
@@ -58,6 +58,9 @@
+
diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml
index f6ea26a84c..677bac26ce 100644
--- a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml
+++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml
@@ -3,6 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:ControlCatalog.ViewModels"
+ xmlns:generic="clr-namespace:System.Collections.Generic;assembly=netstandard"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
@@ -11,12 +12,27 @@
-
+
+
+
+ Used pull direction:
+
+
+
+ TopToBottom
+ LeftToRight
+ RightToLeft
+ BottomToTop
+
+
+
+
+
()?.GetTapSize(PointerType.Touch).Height ?? 10) / 2);
private int _scrollStartDistance = s_defaultScrollStartDistance;
@@ -63,6 +66,30 @@ namespace Avalonia.Input.GestureRecognizers
o => o.ScrollStartDistance, (o, v) => o.ScrollStartDistance = v,
unsetValue: s_defaultScrollStartDistance);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty OffsetProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Offset),
+ o => o.Offset, (o, v) => o.Offset = v,
+ unsetValue: null);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty ExtentProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Extent),
+ o => o.Extent, (o, v) => o.Extent = v,
+ unsetValue: null);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly DirectProperty ViewportProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Viewport),
+ o => o.Viewport, (o, v) => o.Viewport = v,
+ unsetValue: null);
+
///
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
///
@@ -99,6 +126,33 @@ namespace Avalonia.Input.GestureRecognizers
set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value);
}
+ ///
+ /// Gets the extent of the scrollable content.
+ ///
+ public Size? Extent
+ {
+ get => _extent;
+ private set => SetAndRaise(ExtentProperty, ref _extent, value);
+ }
+
+ ///
+ /// Gets or sets the current scroll offset.
+ ///
+ public Vector? Offset
+ {
+ get => _offset;
+ private set => SetAndRaise(OffsetProperty, ref _offset, value);
+ }
+
+ ///
+ /// Gets the size of the viewport on the scrollable content.
+ ///
+ public Size? Viewport
+ {
+ get => _viewport;
+ private set => SetAndRaise(ViewportProperty, ref _viewport, value);
+ }
+
protected override void PointerPressed(PointerPressedEventArgs e)
{
var point = e.GetCurrentPoint(null);
@@ -123,10 +177,34 @@ namespace Avalonia.Input.GestureRecognizers
var rootPoint = e.GetPosition(null);
if (!_scrolling)
{
- if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance)
- _scrolling = true;
- if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance)
- _scrolling = true;
+ if (CanVerticallyScroll)
+ {
+ double delta = _trackedRootPoint.Y - rootPoint.Y;
+
+ if (Offset?.Y == 0 && delta < 0)
+ return;
+
+ if (Offset?.Y + Viewport?.Height - Extent?.Height == 0 && delta > 0)
+ return;
+
+ if (Math.Abs(delta) > ScrollStartDistance)
+ _scrolling = true;
+ }
+
+ if (CanHorizontallyScroll)
+ {
+ double delta = _trackedRootPoint.X - rootPoint.X;
+
+ if (Offset?.X == 0 && delta < 0)
+ return;
+
+ if (Offset?.X + Viewport?.Width - Extent?.Width == 0 && delta > 0)
+ return;
+
+ if (Math.Abs(delta) > ScrollStartDistance)
+ _scrolling = true;
+ }
+
if (_scrolling)
{
// Correct _trackedRootPoint with ScrollStartDistance, so scrolling does not start with a skip of ScrollStartDistance
diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs
index 2c29b19d48..b67bbe5fd3 100644
--- a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs
+++ b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs
@@ -41,7 +41,7 @@ namespace Avalonia.Controls
s => s.Visualizer, (s, o) => s.Visualizer = o);
///
- /// Defines the event.
+ /// Defines the property.
///
public static readonly StyledProperty PullDirectionProperty =
AvaloniaProperty.Register(nameof(PullDirection), PullDirection.TopToBottom);
@@ -54,6 +54,25 @@ namespace Avalonia.Controls
SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value);
}
}
+
+ ///
+ /// Defines the property.
+ ///
+ ///
+ /// Allows to enable the pull 2 refresh gesture for devices using a mouse. Disabled by default
+ ///
+ public static readonly StyledProperty IsMouseEnabledProperty =
+ AvaloniaProperty.Register(nameof(IsMouseEnabled), false);
+
+ ///
+ /// Gets or sets a value that indicates whether the pull-to-refresh gesture is enabled for desktop devices.
+ /// Allows to enable the pull 2 refresh gesture for devices using a mouse. Disabled by default
+ ///
+ public bool IsMouseEnabled
+ {
+ get => GetValue(IsMouseEnabledProperty);
+ set => SetValue(IsMouseEnabledProperty, value);
+ }
///
/// Gets or sets the for this container.
@@ -93,7 +112,7 @@ namespace Avalonia.Controls
public RefreshContainer()
{
_hasDefaultRefreshInfoProviderAdapter = true;
- _refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
+ _refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection, IsMouseEnabled);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
}
@@ -119,9 +138,9 @@ namespace Avalonia.Controls
private void OnVisualizerSizeChanged(Rect obj)
{
- if (_hasDefaultRefreshInfoProviderAdapter)
- {
- _refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
+ if (_hasDefaultRefreshInfoProviderAdapter && _refreshInfoProviderAdapter != null)
+ {
+ _refreshInfoProviderAdapter.UpdateVisualizerSize(obj.Size);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
}
}
@@ -181,6 +200,10 @@ namespace Avalonia.Controls
{
OnPullDirectionChanged();
}
+ else if (change.Property == IsMouseEnabledProperty)
+ {
+ OnIsMouseEnabledChanged();
+ }
}
private void OnPullDirectionChanged()
@@ -231,17 +254,25 @@ namespace Avalonia.Controls
break;
}
- if (_hasDefaultRefreshInfoProviderAdapter &&
- _hasDefaultRefreshVisualizer &&
- _refreshVisualizer.Bounds.Height == DefaultPullDimensionSize &&
- _refreshVisualizer.Bounds.Width == DefaultPullDimensionSize)
- {
- _refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
+ if (_hasDefaultRefreshInfoProviderAdapter)
+ {
+ if (_hasDefaultRefreshVisualizer)
+ {
+ var size = new Size(_refreshVisualizer.Width, _refreshVisualizer.Height);
+ _refreshInfoProviderAdapter?.UpdateVisualizerSize(size);
+ }
+
+ _refreshInfoProviderAdapter?.UpdatePullDirection(PullDirection);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
}
}
}
+ private void OnIsMouseEnabledChanged()
+ {
+ _refreshInfoProviderAdapter?.UpdateIsMouseEnabled(IsMouseEnabled);
+ }
+
///
/// Initiates an update of the content.
///
diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs
index db7b493fb7..7c0bfdcc0e 100644
--- a/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs
+++ b/src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs
@@ -9,8 +9,8 @@ namespace Avalonia.Controls.PullToRefresh
{
internal const double DefaultExecutionRatio = 0.8;
- private readonly PullDirection _refreshPullDirection;
- private readonly Size _refreshVisualizerSize;
+ private PullDirection _refreshPullDirection;
+ private Size _refreshVisualizerSize;
private readonly CompositionVisual? _visual;
private bool _isInteractingForRefresh;
@@ -30,6 +30,14 @@ namespace Avalonia.Controls.PullToRefresh
AvaloniaProperty.RegisterDirect(nameof(InteractionRatio),
s => s.InteractionRatio, (s, o) => s.InteractionRatio = o);
+ public static readonly DirectProperty PullDirectionProperty =
+ AvaloniaProperty.RegisterDirect(nameof(PullDirection),
+ s => s.PullDirection, (s, o) => s.PullDirection = o);
+
+ public static readonly DirectProperty RefreshVisualizerSizeProperty =
+ AvaloniaProperty.RegisterDirect(nameof(RefreshVisualizerSize),
+ s => s.RefreshVisualizerSize, (s, o) => s.RefreshVisualizerSize = o);
+
///
/// Defines the event.
///
@@ -64,6 +72,18 @@ namespace Avalonia.Controls.PullToRefresh
set => SetAndRaise(InteractionRatioProperty, ref _interactionRatio, value);
}
+ public Size RefreshVisualizerSize
+ {
+ get => _refreshVisualizerSize;
+ set => SetAndRaise(RefreshVisualizerSizeProperty, ref _refreshVisualizerSize, value);
+ }
+
+ public PullDirection PullDirection
+ {
+ get => _refreshPullDirection;
+ set => SetAndRaise(PullDirectionProperty, ref _refreshPullDirection, value);
+ }
+
public double ExecutionRatio
{
get => DefaultExecutionRatio;
diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
index f92151e02a..d323965c13 100644
--- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
+++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
@@ -18,6 +18,8 @@ namespace Avalonia.Controls
private const float ParallaxPositionRatio = 0.5f;
private double _executingRatio = 0.8;
+ private Vector3D _initialVisualOffset = default;
+
private RefreshVisualizerState _refreshVisualizerState;
private RefreshInfoProvider? _refreshInfoProvider;
private IDisposable? _isInteractingSubscription;
@@ -211,47 +213,32 @@ namespace Avalonia.Controls
_rotateAnimation.IterationBehavior = AnimationIterationBehavior.Count;
_rotateAnimation = null;
}
+
contentVisual.Opacity = MinimumIndicatorOpacity;
contentVisual.RotationAngle = _startingRotationAngle;
- visualizerVisual.Offset = IsPullDirectionVertical ?
- new Vector3D(visualizerVisual.Offset.X, 0, 0) :
- new Vector3D(0, visualizerVisual.Offset.Y, 0);
- visual.Offset = visualizerVisual.Offset;
+
+ if (visualizerVisual.Offset.X != 0 || visualizerVisual.Offset.Y != 0)
+ {
+ visual.Offset = _initialVisualOffset;
+ visualizerVisual.Offset = new Vector3D(0, 0, 0);
+ }
+
_content.InvalidateMeasure();
+
break;
case RefreshVisualizerState.Interacting:
_played = false;
contentVisual.Opacity = MinimumIndicatorOpacity;
contentVisual.RotationAngle = (float)(_startingRotationAngle + _interactionRatio * 2 * Math.PI);
- Vector3D offset = default;
- if (IsPullDirectionVertical)
- {
- offset = new Vector3D(0, (_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
- }
- else
- {
- offset = new Vector3D((_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
- }
- visual.Offset = offset;
- visualizerVisual.Offset = IsPullDirectionVertical ?
- new Vector3D(visualizerVisual.Offset.X, offset.Y, 0) :
- new Vector3D(offset.X, visualizerVisual.Offset.Y, 0);
+
+ CalculateAndSetOffsets(root, visual, visualizerVisual);
+
break;
case RefreshVisualizerState.Pending:
contentVisual.Opacity = 1;
contentVisual.RotationAngle = _startingRotationAngle + (float)(2 * Math.PI);
- if (IsPullDirectionVertical)
- {
- offset = new Vector3D(0, (_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
- }
- else
- {
- offset = new Vector3D((_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
- }
- visual.Offset = offset;
- visualizerVisual.Offset = IsPullDirectionVertical ?
- new Vector3D(visualizerVisual.Offset.X, offset.Y, 0) :
- new Vector3D(offset.X, visualizerVisual.Offset.Y, 0);
+
+ CalculateAndSetOffsets(root, visual, visualizerVisual);
if (!_played)
{
@@ -278,20 +265,8 @@ namespace Avalonia.Controls
contentVisual.Opacity = 1;
float translationRatio = (float)(_refreshInfoProvider != null ? (1.0f - _refreshInfoProvider.ExecutionRatio) * ParallaxPositionRatio : 1.0f)
* (IsPullDirectionFar ? -1f : 1f);
- if (IsPullDirectionVertical)
- {
- offset = new Vector3D(0, (_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
- }
- else
- {
- offset = new Vector3D((_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
- }
- visual.Offset = offset;
- contentVisual.Offset += IsPullDirectionVertical ? new Vector3D(0, (translationRatio * root.Bounds.Height), 0) :
- new Vector3D((translationRatio * root.Bounds.Width), 0, 0);
- visualizerVisual.Offset = IsPullDirectionVertical ?
- new Vector3D(visualizerVisual.Offset.X, offset.Y, 0) :
- new Vector3D(offset.X, visualizerVisual.Offset.Y, 0);
+
+ CalculateAndSetOffsets(root, visual, visualizerVisual);
break;
case RefreshVisualizerState.Peeking:
contentVisual.Opacity = 1;
@@ -302,6 +277,36 @@ namespace Avalonia.Controls
}
}
+ private void CalculateAndSetOffsets(Grid root, CompositionVisual visual, CompositionVisual indicatorVisualizer)
+ {
+ if (IsPullDirectionVertical)
+ {
+ // As long as the indicator-container isn't visible,
+ // the initial offset is the current offset
+ if (indicatorVisualizer.Offset.Y == 0)
+ {
+ _initialVisualOffset = visual.Offset;
+ }
+
+ var offset = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height;
+ indicatorVisualizer.Offset = indicatorVisualizer.Offset with { Y = offset };
+
+ visual.Offset = _initialVisualOffset with { Y = _initialVisualOffset.Y + offset };
+ }
+ else
+ {
+ if (indicatorVisualizer.Offset.X == 0)
+ {
+ _initialVisualOffset = visual.Offset;
+ }
+
+ var offset = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width;
+ indicatorVisualizer.Offset = indicatorVisualizer.Offset with { X = offset };
+
+ visual.Offset = _initialVisualOffset with { X = _initialVisualOffset.X + offset };
+ }
+ }
+
///
/// Initiates an update of the content.
///
@@ -371,32 +376,39 @@ namespace Avalonia.Controls
}
else if (change.Property == BoundsProperty)
{
- switch (PullDirection)
- {
- case PullDirection.TopToBottom:
- RenderTransform = new TranslateTransform(0, -Bounds.Height);
- break;
- case PullDirection.BottomToTop:
- RenderTransform = new TranslateTransform(0, Bounds.Height);
- break;
- case PullDirection.LeftToRight:
- RenderTransform = new TranslateTransform(-Bounds.Width, 0);
- break;
- case PullDirection.RightToLeft:
- RenderTransform = new TranslateTransform(Bounds.Width, 0);
- break;
- }
+ OnBoundsChanged();
UpdateContent();
}
- else if(change.Property == PullDirectionProperty)
+ else if (change.Property == PullDirectionProperty)
{
OnOrientationChanged();
+ OnBoundsChanged();
+
UpdateContent();
}
}
+ private void OnBoundsChanged()
+ {
+ switch (PullDirection)
+ {
+ case PullDirection.TopToBottom:
+ RenderTransform = new TranslateTransform(0, -Bounds.Height);
+ break;
+ case PullDirection.BottomToTop:
+ RenderTransform = new TranslateTransform(0, Bounds.Height);
+ break;
+ case PullDirection.LeftToRight:
+ RenderTransform = new TranslateTransform(-Bounds.Width, 0);
+ break;
+ case PullDirection.RightToLeft:
+ RenderTransform = new TranslateTransform(Bounds.Width, 0);
+ break;
+ }
+ }
+
private void OnOrientationChanged()
{
switch (_orientation)
diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs
index 48063d90f8..5df9dda922 100644
--- a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs
+++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs
@@ -12,15 +12,17 @@ namespace Avalonia.Controls.PullToRefresh
private const int InitialOffsetThreshold = 1;
private PullDirection _refreshPullDirection;
+ private bool _IsMouseEnabled;
private ScrollViewer? _scrollViewer;
private RefreshInfoProvider? _refreshInfoProvider;
- private PullGestureRecognizer? _pullGestureRecognizer;
+ private ScrollablePullGestureRecognizer? _pullGestureRecognizer;
private InputElement? _interactionSource;
private bool _isVisualizerInteractionSourceAttached;
- public ScrollViewerIRefreshInfoProviderAdapter(PullDirection pullDirection)
+ public ScrollViewerIRefreshInfoProviderAdapter(PullDirection pullDirection, bool IsMouseEnabled)
{
_refreshPullDirection = pullDirection;
+ _IsMouseEnabled = IsMouseEnabled;
}
public RefreshInfoProvider? AdaptFromTree(Visual root, Size? refreshVIsualizerSize)
@@ -124,7 +126,7 @@ namespace Avalonia.Controls.PullToRefresh
_refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, ElementComposition.GetElementVisual(content));
- _pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection);
+ _pullGestureRecognizer = new ScrollablePullGestureRecognizer(_refreshPullDirection, _IsMouseEnabled);
if (_interactionSource != null)
{
@@ -261,6 +263,39 @@ namespace Avalonia.Controls.PullToRefresh
return false;
}
+ public void UpdatePullDirection(PullDirection pullDirection)
+ {
+ _refreshPullDirection = pullDirection;
+
+ if (_refreshInfoProvider != null)
+ {
+ _refreshInfoProvider.PullDirection = pullDirection;
+ }
+
+ if (_pullGestureRecognizer != null)
+ {
+ _pullGestureRecognizer.PullDirection = pullDirection;
+ }
+ }
+
+ public void UpdateIsMouseEnabled(bool IsMouseEnabled)
+ {
+ _IsMouseEnabled = IsMouseEnabled;
+
+ if (_pullGestureRecognizer != null)
+ {
+ _pullGestureRecognizer.IsMouseEnabled = IsMouseEnabled;
+ }
+ }
+
+ public void UpdateVisualizerSize(Size? refreshVisualizerSize)
+ {
+ if (_refreshInfoProvider != null)
+ {
+ _refreshInfoProvider.RefreshVisualizerSize = refreshVisualizerSize ?? default;
+ }
+ }
+
private void CleanUpScrollViewer()
{
if (_scrollViewer != null)
diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollablePullGestureRecognizer.cs b/src/Avalonia.Controls/PullToRefresh/ScrollablePullGestureRecognizer.cs
new file mode 100644
index 0000000000..2ecaba756e
--- /dev/null
+++ b/src/Avalonia.Controls/PullToRefresh/ScrollablePullGestureRecognizer.cs
@@ -0,0 +1,136 @@
+using System;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Input.GestureRecognizers;
+
+namespace Avalonia.Controls.PullToRefresh
+{
+ internal class ScrollablePullGestureRecognizer : GestureRecognizer
+ {
+ private int _gestureId;
+ private bool _pullInProgress;
+
+ private double _delta = 1;
+
+ private Point _initialPosition;
+ private IPointer? _tracking;
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PullDirectionProperty =
+ AvaloniaProperty.Register(nameof(PullDirection));
+
+ public PullDirection PullDirection
+ {
+ get => GetValue(PullDirectionProperty);
+ set => SetValue(PullDirectionProperty, value);
+ }
+
+ public bool IsMouseEnabled { get; set; }
+
+ public ScrollablePullGestureRecognizer(PullDirection pullDirection, bool IsMouseEnabled)
+ {
+ PullDirection = pullDirection;
+ IsMouseEnabled = IsMouseEnabled;
+ }
+
+ public ScrollablePullGestureRecognizer() { }
+
+ protected override void PointerCaptureLost(IPointer pointer)
+ {
+ if (_tracking == pointer)
+ {
+ EndPull();
+ }
+ }
+
+ protected override void PointerPressed(PointerPressedEventArgs e)
+ {
+ var isEnabledOnPlatform = (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen) // either it is a touch device
+ || IsMouseEnabled; // or desktop is enabled
+
+ if (Target != null && Target is Visual visual && isEnabledOnPlatform)
+ {
+ _tracking = e.Pointer;
+ _initialPosition = e.GetPosition(visual);
+ }
+ }
+
+ protected override void PointerMoved(PointerEventArgs e)
+ {
+ if (_tracking != e.Pointer)
+ return;
+
+ if (Target is Visual visual && visual is IScrollable scrollable && CanPull(scrollable))
+ {
+ var currentPosition = e.GetPosition(visual);
+
+ var delta = CalculateDelta(currentPosition);
+
+ bool pulling = delta.Y > 0 || delta.X > 0;
+ _pullInProgress = (_pullInProgress, pulling) switch
+ {
+ (false, false) => false,
+ (false, true) => BeginPull(e, delta),
+ (true, true) => HandlePull(e, delta),
+ (true, false) => EndPull(),
+ };
+ }
+ }
+
+ protected override void PointerReleased(PointerReleasedEventArgs e)
+ {
+ if (_pullInProgress == true)
+ {
+ EndPull();
+ e.Pointer.Capture(null);
+ }
+
+ _tracking = null;
+ _initialPosition = default;
+ _pullInProgress = false;
+ }
+
+ private bool BeginPull(PointerEventArgs e, Vector delta)
+ {
+ _gestureId = PullGestureEventArgs.GetNextFreeId();
+ return HandlePull(e, delta);
+ }
+
+ private bool HandlePull(PointerEventArgs e, Vector delta)
+ {
+ Capture(e.Pointer);
+
+ var pullEventArgs = new PullGestureEventArgs(_gestureId, delta, PullDirection);
+ Target?.RaiseEvent(pullEventArgs);
+
+ e.Handled = pullEventArgs.Handled;
+ return true;
+ }
+
+ private bool EndPull()
+ {
+ Target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection));
+ return false;
+ }
+
+ private Vector CalculateDelta(Point currentPosition) => PullDirection switch
+ {
+ PullDirection.TopToBottom => new Vector(0, currentPosition.Y - _initialPosition.Y),
+ PullDirection.BottomToTop => new Vector(0, _initialPosition.Y - currentPosition.Y),
+ PullDirection.LeftToRight => new Vector(currentPosition.X - _initialPosition.X, 0),
+ PullDirection.RightToLeft => new Vector(_initialPosition.X - currentPosition.X, 0),
+ _ => default,
+ };
+
+ private bool CanPull(IScrollable scrollable) => PullDirection switch
+ {
+ PullDirection.TopToBottom => scrollable.Offset.Y < _delta,
+ PullDirection.BottomToTop => Math.Abs(scrollable.Offset.Y + scrollable.Viewport.Height - scrollable.Extent.Height) <= _delta,
+ PullDirection.LeftToRight => scrollable.Offset.X < _delta,
+ PullDirection.RightToLeft => Math.Abs(scrollable.Offset.X + scrollable.Viewport.Width - scrollable.Extent.Width) <= _delta,
+ _ => false,
+ };
+ }
+}
diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
index 611c7dc49b..ef861532ae 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
@@ -37,7 +37,10 @@
+ IsScrollInertiaEnabled="{Binding (ScrollViewer.IsScrollInertiaEnabled), ElementName=PART_ContentPresenter}"
+ Offset="{Binding Offset, ElementName=PART_ContentPresenter}"
+ Viewport="{Binding Viewport, ElementName=PART_ContentPresenter}"
+ Extent="{Binding Extent, ElementName=PART_ContentPresenter}"/>
+ IsScrollInertiaEnabled="{Binding (ScrollViewer.IsScrollInertiaEnabled), ElementName=PART_ContentPresenter}"
+ Offset="{Binding Offset, ElementName=PART_ContentPresenter}"
+ Viewport="{Binding Viewport, ElementName=PART_ContentPresenter}"
+ Extent="{Binding Extent, ElementName=PART_ContentPresenter}"/>