From b6c302412cc3ee013f34f3f8c50ea8748599c0d9 Mon Sep 17 00:00:00 2001 From: nickodei Date: Mon, 11 Nov 2024 16:58:40 +0100 Subject: [PATCH 1/3] added improved refresh-capabilities to ScrollViewer in simple theme Added gesture-recognizer for scroll-viewer to improve usability --- .../Pages/RefreshContainerPage.axaml | 20 ++- .../ScrollGestureRecognizer.cs | 86 +++++++++++- .../PullToRefresh/RefreshContainer.cs | 21 +-- .../PullToRefresh/RefreshInfoProvider.cs | 24 +++- .../PullToRefresh/RefreshVisualizer.cs | 132 ++++++++++-------- ...ScrollViewerIRefreshInfoProviderAdapter.cs | 27 +++- .../ScrollablePullGestureRecognizer.cs | 129 +++++++++++++++++ .../Controls/ScrollViewer.xaml | 5 +- .../Controls/ScrollViewer.xaml | 5 +- 9 files changed, 368 insertions(+), 81 deletions(-) create mode 100644 src/Avalonia.Controls/PullToRefresh/ScrollablePullGestureRecognizer.cs 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; @@ -57,6 +60,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. /// @@ -93,6 +120,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); @@ -116,10 +170,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..bc04c2ff54 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs @@ -119,9 +119,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); } } @@ -231,12 +231,15 @@ 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); } } 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..c048275fee 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,13 +277,43 @@ 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. /// public void RequestRefresh() { RefreshVisualizerState = RefreshVisualizerState.Refreshing; - RefreshInfoProvider?.OnRefreshStarted(); + RefreshInfoProvider?.OnRefreshStarted(); RaiseRefreshRequested(); } @@ -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 7ff02711d6..4c437cda5f 100644 --- a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs +++ b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.PullToRefresh private PullDirection _refreshPullDirection; private ScrollViewer? _scrollViewer; private RefreshInfoProvider? _refreshInfoProvider; - private PullGestureRecognizer? _pullGestureRecognizer; + private ScrollablePullGestureRecognizer? _pullGestureRecognizer; private InputElement? _interactionSource; private bool _isVisualizerInteractionSourceAttached; @@ -124,7 +124,7 @@ namespace Avalonia.Controls.PullToRefresh _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, ElementComposition.GetElementVisual(content)); - _pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection); + _pullGestureRecognizer = new ScrollablePullGestureRecognizer(_refreshPullDirection); if (_interactionSource != null) { @@ -261,6 +261,29 @@ 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 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..10cae47c36 --- /dev/null +++ b/src/Avalonia.Controls/PullToRefresh/ScrollablePullGestureRecognizer.cs @@ -0,0 +1,129 @@ +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 ScrollablePullGestureRecognizer(PullDirection pullDirection) + { + PullDirection = pullDirection; + } + + public ScrollablePullGestureRecognizer() { } + + protected override void PointerCaptureLost(IPointer pointer) + { + if (_tracking == pointer) + { + EndPull(); + } + } + + protected override void PointerPressed(PointerPressedEventArgs e) + { + if (Target != null && Target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + { + _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(); + } + + _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}"/> Date: Mon, 14 Apr 2025 14:27:44 +0200 Subject: [PATCH 2/3] added possibility to enable pulltorefresh on desktop too --- samples/ControlCatalog/App.xaml | 3 ++ .../PullToRefresh/RefreshContainer.cs | 32 +++++++++++++++++-- .../PullToRefresh/RefreshVisualizer.cs | 2 +- ...ScrollViewerIRefreshInfoProviderAdapter.cs | 16 ++++++++-- .../ScrollablePullGestureRecognizer.cs | 11 +++++-- 5 files changed, 57 insertions(+), 7 deletions(-) diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 022118d3ab..b04dff6057 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -58,6 +58,9 @@ + diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs index bc04c2ff54..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); } @@ -181,6 +200,10 @@ namespace Avalonia.Controls { OnPullDirectionChanged(); } + else if (change.Property == IsMouseEnabledProperty) + { + OnIsMouseEnabledChanged(); + } } private void OnPullDirectionChanged() @@ -245,6 +268,11 @@ namespace Avalonia.Controls } } + private void OnIsMouseEnabledChanged() + { + _refreshInfoProviderAdapter?.UpdateIsMouseEnabled(IsMouseEnabled); + } + /// /// Initiates an update of the content. /// diff --git a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs index c048275fee..d323965c13 100644 --- a/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs @@ -313,7 +313,7 @@ namespace Avalonia.Controls public void RequestRefresh() { RefreshVisualizerState = RefreshVisualizerState.Refreshing; - RefreshInfoProvider?.OnRefreshStarted(); + RefreshInfoProvider?.OnRefreshStarted(); RaiseRefreshRequested(); } diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs b/src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs index 4c437cda5f..4868079bbe 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 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 ScrollablePullGestureRecognizer(_refreshPullDirection); + _pullGestureRecognizer = new ScrollablePullGestureRecognizer(_refreshPullDirection, _IsMouseEnabled); if (_interactionSource != null) { @@ -276,6 +278,16 @@ namespace Avalonia.Controls.PullToRefresh } } + public void UpdateIsMouseEnabled(bool IsMouseEnabled) + { + _IsMouseEnabled = IsMouseEnabled; + + if (_pullGestureRecognizer != null) + { + _pullGestureRecognizer.IsMouseEnabled = IsMouseEnabled; + } + } + public void UpdateVisualizerSize(Size? refreshVisualizerSize) { if (_refreshInfoProvider != null) diff --git a/src/Avalonia.Controls/PullToRefresh/ScrollablePullGestureRecognizer.cs b/src/Avalonia.Controls/PullToRefresh/ScrollablePullGestureRecognizer.cs index 10cae47c36..2ecaba756e 100644 --- a/src/Avalonia.Controls/PullToRefresh/ScrollablePullGestureRecognizer.cs +++ b/src/Avalonia.Controls/PullToRefresh/ScrollablePullGestureRecognizer.cs @@ -27,9 +27,12 @@ namespace Avalonia.Controls.PullToRefresh set => SetValue(PullDirectionProperty, value); } - public ScrollablePullGestureRecognizer(PullDirection pullDirection) + public bool IsMouseEnabled { get; set; } + + public ScrollablePullGestureRecognizer(PullDirection pullDirection, bool IsMouseEnabled) { PullDirection = pullDirection; + IsMouseEnabled = IsMouseEnabled; } public ScrollablePullGestureRecognizer() { } @@ -44,7 +47,10 @@ namespace Avalonia.Controls.PullToRefresh protected override void PointerPressed(PointerPressedEventArgs e) { - if (Target != null && Target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) + 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); @@ -78,6 +84,7 @@ namespace Avalonia.Controls.PullToRefresh if (_pullInProgress == true) { EndPull(); + e.Pointer.Capture(null); } _tracking = null; From bd214514b8445a696dde62e58c1e84c74bab8f3a Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 5 Aug 2025 14:56:09 +0000 Subject: [PATCH 3/3] fix doc comments in ScrollGestureRecognizer --- .../Input/GestureRecognizers/ScrollGestureRecognizer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 3d5745cb3b..601594314e 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs @@ -67,7 +67,7 @@ namespace Avalonia.Input.GestureRecognizers unsetValue: s_defaultScrollStartDistance); /// - /// Defines the property. + /// Defines the property. /// public static readonly DirectProperty OffsetProperty = AvaloniaProperty.RegisterDirect(nameof(Offset), @@ -75,7 +75,7 @@ namespace Avalonia.Input.GestureRecognizers unsetValue: null); /// - /// Defines the property. + /// Defines the property. /// public static readonly DirectProperty ExtentProperty = AvaloniaProperty.RegisterDirect(nameof(Extent), @@ -83,7 +83,7 @@ namespace Avalonia.Input.GestureRecognizers unsetValue: null); /// - /// Defines the property. + /// Defines the property. /// public static readonly DirectProperty ViewportProperty = AvaloniaProperty.RegisterDirect(nameof(Viewport),