Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions samples/ControlCatalog/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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>
Expand Down
20 changes: 18 additions & 2 deletions samples/ControlCatalog/Pages/RefreshContainerPage.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -12,12 +13,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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public class ScrollGestureRecognizer : GestureRecognizer
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;

Expand Down Expand Up @@ -64,6 +67,30 @@ public class ScrollGestureRecognizer : GestureRecognizer
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>
Expand Down Expand Up @@ -100,6 +127,33 @@ public int ScrollStartDistance
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);
Expand All @@ -124,10 +178,34 @@ protected override void PointerMoved(PointerEventArgs e)
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
Expand Down
68 changes: 55 additions & 13 deletions src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public class RefreshContainer : ContentControl
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);
Expand All @@ -54,6 +54,25 @@ internal ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter
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.
Expand Down Expand Up @@ -93,7 +112,7 @@ public event EventHandler<RefreshRequestedEventArgs>? RefreshRequested
public RefreshContainer()
{
_hasDefaultRefreshInfoProviderAdapter = true;
_refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
_refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection, IsMouseEnabled);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
}

Expand All @@ -119,18 +138,29 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)

private void OnVisualizerSizeChanged(Rect obj)
{
if (_hasDefaultRefreshInfoProviderAdapter)
{
_refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
if (_hasDefaultRefreshInfoProviderAdapter && _refreshInfoProviderAdapter != null)
{
_refreshInfoProviderAdapter.UpdateVisualizerSize(obj.Size);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
}
}

private void Visualizer_RefreshRequested(object? sender, RefreshRequestedEventArgs e)
{
// Guarantee the inner deferral is balanced even if a downstream handler
// of RefreshRequestedEvent throws synchronously from RaiseEvent.
// Without this, a synchronously-throwing consumer leaves the visualizer's
// deferral count above zero forever, so RefreshCompleted never fires and
// the spinner stays stuck in Refreshing.
var ev = new RefreshRequestedEventArgs(e.GetDeferral(), RefreshRequestedEvent);
RaiseEvent(ev);
ev.DecrementCount();
try
{
RaiseEvent(ev);
}
finally
{
ev.DecrementCount();
}
}

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
Expand Down Expand Up @@ -181,6 +211,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
{
OnPullDirectionChanged();
}
else if (change.Property == IsMouseEnabledProperty)
{
OnIsMouseEnabledChanged();
}
}

private void OnPullDirectionChanged()
Expand Down Expand Up @@ -231,17 +265,25 @@ private void OnPullDirectionChanged()
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>
Expand Down
37 changes: 34 additions & 3 deletions src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ internal class RefreshInfoProvider : Interactive
{
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;
Expand All @@ -30,6 +30,14 @@ internal class RefreshInfoProvider : Interactive
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>
Expand All @@ -54,6 +62,17 @@ internal set
if (isInteractingForRefresh != _isInteractingForRefresh)
{
SetAndRaise(IsInteractingForRefreshProperty, ref _isInteractingForRefresh, isInteractingForRefresh);

// Keep _entered in sync with IsInteractingForRefresh.
// The flag can be cleared by paths other than PullGestureEnded
// (ScrollViewer_ScrollChanged / ScrollViewer_PointerReleased on the adapter).
// Without this, InteractingStateEntered would short-circuit and never
// re-assert the flag for the rest of the gesture, leaving the visualizer
// stuck in Idle and the spinner invisible until the next gesture.
if (!isInteractingForRefresh)
{
_entered = false;
}
}
}
}
Expand All @@ -64,6 +83,18 @@ public double InteractionRatio
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;
Expand Down Expand Up @@ -130,7 +161,7 @@ internal void ValuesChanged(Vector value)
break;
case PullDirection.LeftToRight:
case PullDirection.RightToLeft:
InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.X / _refreshVisualizerSize.Width);
InteractionRatio = _refreshVisualizerSize.Width == 0 ? 1 : Math.Min(1, value.X / _refreshVisualizerSize.Width);
break;
}
}
Expand Down
Loading