Browse Source

Merge bd214514b8 into 3068850405

pull/18617/merge
Alexander Marek 1 day ago
committed by GitHub
parent
commit
e051ea5101
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      samples/ControlCatalog/App.xaml
  2. 20
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  3. 86
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  4. 53
      src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs
  5. 24
      src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs
  6. 130
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
  7. 41
      src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs
  8. 136
      src/Avalonia.Controls/PullToRefresh/ScrollablePullGestureRecognizer.cs
  9. 5
      src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml
  10. 5
      src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml

3
samples/ControlCatalog/App.xaml

@ -58,6 +58,9 @@
<Style Selector="Label.h3">
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="RefreshContainer">
<Setter Property="IsMouseEnabled" Value="true" />
</Style>
</Application.Styles>
<NativeDock.Menu>
<NativeMenu>

20
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 @@
<DockPanel HorizontalAlignment="Stretch"
Height="600"
VerticalAlignment="Top">
<Label DockPanel.Dock="Top">A control that supports pull to refresh</Label>
<StackPanel DockPanel.Dock="Top">
<Label>A control that supports pull to refresh on touch inputs</Label>
<StackPanel Orientation="Horizontal" Margin="3" Spacing="5">
<TextBlock VerticalAlignment="Center">Used pull direction:</TextBlock>
<ComboBox x:Name="PullDirections" SelectedIndex="0">
<ComboBox.ItemsSource>
<generic:List x:TypeArguments="PullDirection">
<PullDirection>TopToBottom</PullDirection>
<PullDirection>LeftToRight</PullDirection>
<PullDirection>RightToLeft</PullDirection>
<PullDirection>BottomToTop</PullDirection>
</generic:List>
</ComboBox.ItemsSource>
</ComboBox>
</StackPanel>
</StackPanel>
<RefreshContainer Name="Refresh"
DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
PullDirection="TopToBottom"
PullDirection="{Binding #PullDirections.SelectedItem}"
RefreshRequested="RefreshContainerPage_RefreshRequested"
Margin="5">
<ListBox HorizontalAlignment="Stretch"

86
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -16,6 +16,9 @@ namespace Avalonia.Input.GestureRecognizers
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private bool _isScrollInertiaEnabled;
private Vector? _offset;
private Size? _viewport;
private Size? _extent;
private readonly static int s_defaultScrollStartDistance = (int)((AvaloniaLocator.Current?.GetService<IPlatformSettings>()?.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);
/// <summary>
/// Defines the <see cref="Offset"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, Vector?> OffsetProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, Vector?>(nameof(Offset),
o => o.Offset, (o, v) => o.Offset = v,
unsetValue: null);
/// <summary>
/// Defines the <see cref="Extent"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, Size?> ExtentProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, Size?>(nameof(Extent),
o => o.Extent, (o, v) => o.Extent = v,
unsetValue: null);
/// <summary>
/// Defines the <see cref="Viewport"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, Size?> ViewportProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, Size?>(nameof(Viewport),
o => o.Viewport, (o, v) => o.Viewport = v,
unsetValue: null);
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
@ -99,6 +126,33 @@ namespace Avalonia.Input.GestureRecognizers
set => SetAndRaise(ScrollStartDistanceProperty, ref _scrollStartDistance, value);
}
/// <summary>
/// Gets the extent of the scrollable content.
/// </summary>
public Size? Extent
{
get => _extent;
private set => SetAndRaise(ExtentProperty, ref _extent, value);
}
/// <summary>
/// Gets or sets the current scroll offset.
/// </summary>
public Vector? Offset
{
get => _offset;
private set => SetAndRaise(OffsetProperty, ref _offset, value);
}
/// <summary>
/// Gets the size of the viewport on the scrollable content.
/// </summary>
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

53
src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs

@ -41,7 +41,7 @@ namespace Avalonia.Controls
s => s.Visualizer, (s, o) => s.Visualizer = o);
/// <summary>
/// Defines the <see cref="PullDirection"/> event.
/// Defines the <see cref="PullDirection"/> property.
/// </summary>
public static readonly StyledProperty<PullDirection> PullDirectionProperty =
AvaloniaProperty.Register<RefreshContainer, PullDirection>(nameof(PullDirection), PullDirection.TopToBottom);
@ -54,6 +54,25 @@ namespace Avalonia.Controls
SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value);
}
}
/// <summary>
/// Defines the <see cref="IsMouseEnabled"/> property.
/// </summary>
/// <remarks>
/// Allows to enable the pull 2 refresh gesture for devices using a mouse. Disabled by default
/// </remarks>
public static readonly StyledProperty<bool> IsMouseEnabledProperty =
AvaloniaProperty.Register<RefreshContainer, bool>(nameof(IsMouseEnabled), false);
/// <summary>
/// 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
/// </summary>
public bool IsMouseEnabled
{
get => GetValue(IsMouseEnabledProperty);
set => SetValue(IsMouseEnabledProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="RefreshVisualizer"/> 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);
}
/// <summary>
/// Initiates an update of the content.
/// </summary>

24
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<RefreshInfoProvider, double>(nameof(InteractionRatio),
s => s.InteractionRatio, (s, o) => s.InteractionRatio = o);
public static readonly DirectProperty<RefreshInfoProvider, PullDirection> PullDirectionProperty =
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, PullDirection>(nameof(PullDirection),
s => s.PullDirection, (s, o) => s.PullDirection = o);
public static readonly DirectProperty<RefreshInfoProvider, Size> RefreshVisualizerSizeProperty =
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, Size>(nameof(RefreshVisualizerSize),
s => s.RefreshVisualizerSize, (s, o) => s.RefreshVisualizerSize = o);
/// <summary>
/// Defines the <see cref="RefreshStarted"/> event.
/// </summary>
@ -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;

130
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 };
}
}
/// <summary>
/// Initiates an update of the content.
/// </summary>
@ -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)

41
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)

136
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;
/// <summary>
/// Defines the <see cref="PullDirection"/> property.
/// </summary>
public static readonly StyledProperty<PullDirection> PullDirectionProperty =
AvaloniaProperty.Register<ScrollablePullGestureRecognizer, PullDirection>(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,
};
}
}

5
src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml

@ -37,7 +37,10 @@
<ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll="{Binding CanHorizontallyScroll, ElementName=PART_ContentPresenter}"
CanVerticallyScroll="{Binding CanVerticallyScroll, ElementName=PART_ContentPresenter}"
IsScrollInertiaEnabled="{Binding (ScrollViewer.IsScrollInertiaEnabled), 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}"/>
</ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter>
<ScrollBar Name="PART_HorizontalScrollBar"

5
src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml

@ -20,7 +20,10 @@
<ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer CanHorizontallyScroll="{Binding CanHorizontallyScroll, ElementName=PART_ContentPresenter}"
CanVerticallyScroll="{Binding CanVerticallyScroll, ElementName=PART_ContentPresenter}"
IsScrollInertiaEnabled="{Binding (ScrollViewer.IsScrollInertiaEnabled), 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}"/>
</ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter>
<ScrollBar Name="PART_HorizontalScrollBar"

Loading…
Cancel
Save