Browse Source

clean up animation and add comments

pull/9437/head
Emmanuel Hansen 4 years ago
parent
commit
1a80d5fd5a
  1. 1
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  2. 2
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  3. 2
      samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
  4. 2
      src/Avalonia.Base/Input/PullGestureEventArgs.cs
  5. 59
      src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs
  6. 17
      src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs
  7. 217
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
  8. 2
      src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs
  9. 32
      src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml
  10. 49
      src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml

1
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

@ -16,6 +16,7 @@
DockPanel.Dock="Bottom" DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
PullDirection="TopToBottom"
RefreshRequested="RefreshContainerPage_RefreshRequested" RefreshRequested="RefreshContainerPage_RefreshRequested"
Margin="5"> Margin="5">
<ListBox HorizontalAlignment="Stretch" <ListBox HorizontalAlignment="Stretch"

2
samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs

@ -21,7 +21,7 @@ namespace ControlCatalog.Pages
private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e) private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e)
{ {
var deferral = e.GetRefreshCompletionDeferral(); var deferral = e.GetDeferral();
await _viewModel.AddToTop(); await _viewModel.AddToTop();

2
samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs

@ -19,7 +19,7 @@ namespace ControlCatalog.ViewModels
public async Task AddToTop() public async Task AddToTop()
{ {
await Task.Delay(1000); await Task.Delay(3000);
Items.Insert(0, $"Item {200 - Items.Count}"); Items.Insert(0, $"Item {200 - Items.Count}");
} }
} }

2
src/Avalonia.Base/Input/PullGestureEventArgs.cs

@ -11,7 +11,7 @@ namespace Avalonia.Input
private static int _nextId = 1; private static int _nextId = 1;
public static int GetNextFreeId() => _nextId++; internal static int GetNextFreeId() => _nextId++;
public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent) public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent)
{ {

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

@ -6,18 +6,25 @@ using Avalonia.Interactivity;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary>
/// Represents a container control that provides a <see cref="RefreshVisualizer"/> and pull-to-refresh functionality for scrollable content.
/// </summary>
public class RefreshContainer : ContentControl public class RefreshContainer : ContentControl
{ {
internal const int DefaultPullDimensionSize = 100; internal const int DefaultPullDimensionSize = 100;
private readonly bool _hasDefaultRefreshInfoProviderAdapter; private bool _hasDefaultRefreshInfoProviderAdapter;
private ScrollViewerIRefreshInfoProviderAdapter? _refreshInfoProviderAdapter; private ScrollViewerIRefreshInfoProviderAdapter? _refreshInfoProviderAdapter;
private RefreshInfoProvider? _refreshInfoProvider; private RefreshInfoProvider? _refreshInfoProvider;
private IDisposable? _visualizerSizeSubscription; private IDisposable? _visualizerSizeSubscription;
private Grid? _visualizerPresenter; private Grid? _visualizerPresenter;
private RefreshVisualizer? _refreshVisualizer; private RefreshVisualizer? _refreshVisualizer;
private bool _hasDefaultRefreshVisualizer;
/// <summary>
/// Defines the <see cref="RefreshRequested"/> event.
/// </summary>
public static readonly RoutedEvent<RefreshRequestedEventArgs> RefreshRequestedEvent = public static readonly RoutedEvent<RefreshRequestedEventArgs> RefreshRequestedEvent =
RoutedEvent.Register<RefreshContainer, RefreshRequestedEventArgs>(nameof(RefreshRequested), RoutingStrategies.Bubble); RoutedEvent.Register<RefreshContainer, RefreshRequestedEventArgs>(nameof(RefreshRequested), RoutingStrategies.Bubble);
@ -25,24 +32,32 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<RefreshContainer, ScrollViewerIRefreshInfoProviderAdapter?>(nameof(RefreshInfoProviderAdapter), AvaloniaProperty.RegisterDirect<RefreshContainer, ScrollViewerIRefreshInfoProviderAdapter?>(nameof(RefreshInfoProviderAdapter),
(s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o); (s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o);
public static readonly DirectProperty<RefreshContainer, RefreshVisualizer?> RefreshVisualizerProperty = /// <summary>
AvaloniaProperty.RegisterDirect<RefreshContainer, RefreshVisualizer?>(nameof(RefreshVisualizer), /// Defines the <see cref="Visualizer"/> event.
s => s.RefreshVisualizer, (s, o) => s.RefreshVisualizer = o); /// </summary>
public static readonly DirectProperty<RefreshContainer, RefreshVisualizer?> VisualizerProperty =
AvaloniaProperty.RegisterDirect<RefreshContainer, RefreshVisualizer?>(nameof(Visualizer),
s => s.Visualizer, (s, o) => s.Visualizer = o);
/// <summary>
/// Defines the <see cref="PullDirection"/> event.
/// </summary>
public static readonly StyledProperty<PullDirection> PullDirectionProperty = public static readonly StyledProperty<PullDirection> PullDirectionProperty =
AvaloniaProperty.Register<RefreshContainer, PullDirection>(nameof(PullDirection), PullDirection.TopToBottom); AvaloniaProperty.Register<RefreshContainer, PullDirection>(nameof(PullDirection), PullDirection.TopToBottom);
public ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter internal ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter
{ {
get => _refreshInfoProviderAdapter; set get => _refreshInfoProviderAdapter; set
{ {
_hasDefaultRefreshInfoProviderAdapter = false;
SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value); SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value);
} }
} }
private bool _hasDefaultRefreshVisualizer; /// <summary>
/// Gets or sets the <see cref="RefreshVisualizer"/> for this container.
public RefreshVisualizer? RefreshVisualizer /// </summary>
public RefreshVisualizer? Visualizer
{ {
get => _refreshVisualizer; set get => _refreshVisualizer; set
{ {
@ -52,16 +67,22 @@ namespace Avalonia.Controls
_refreshVisualizer.RefreshRequested -= Visualizer_RefreshRequested; _refreshVisualizer.RefreshRequested -= Visualizer_RefreshRequested;
} }
SetAndRaise(RefreshVisualizerProperty, ref _refreshVisualizer, value); SetAndRaise(VisualizerProperty, ref _refreshVisualizer, value);
} }
} }
/// <summary>
/// Gets or sets a value that specifies the direction to pull to initiate a refresh.
/// </summary>
public PullDirection PullDirection public PullDirection PullDirection
{ {
get => GetValue(PullDirectionProperty); get => GetValue(PullDirectionProperty);
set => SetValue(PullDirectionProperty, value); set => SetValue(PullDirectionProperty, value);
} }
/// <summary>
/// Occurs when an update of the content has been initiated.
/// </summary>
public event EventHandler<RefreshRequestedEventArgs>? RefreshRequested public event EventHandler<RefreshRequestedEventArgs>? RefreshRequested
{ {
add => AddHandler(RefreshRequestedEvent, value); add => AddHandler(RefreshRequestedEvent, value);
@ -71,7 +92,8 @@ namespace Avalonia.Controls
public RefreshContainer() public RefreshContainer()
{ {
_hasDefaultRefreshInfoProviderAdapter = true; _hasDefaultRefreshInfoProviderAdapter = true;
RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); _refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
} }
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@ -83,12 +105,12 @@ namespace Avalonia.Controls
if (_refreshVisualizer == null) if (_refreshVisualizer == null)
{ {
_hasDefaultRefreshVisualizer = true; _hasDefaultRefreshVisualizer = true;
RefreshVisualizer = new RefreshVisualizer(); Visualizer = new RefreshVisualizer();
} }
else else
{ {
_hasDefaultRefreshVisualizer = false; _hasDefaultRefreshVisualizer = false;
RaisePropertyChanged(RefreshVisualizerProperty, default, _refreshVisualizer); RaisePropertyChanged(VisualizerProperty, default, _refreshVisualizer);
} }
OnPullDirectionChanged(); OnPullDirectionChanged();
@ -98,13 +120,14 @@ namespace Avalonia.Controls
{ {
if (_hasDefaultRefreshInfoProviderAdapter) if (_hasDefaultRefreshInfoProviderAdapter)
{ {
RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); _refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
} }
} }
private void Visualizer_RefreshRequested(object? sender, RefreshRequestedEventArgs e) private void Visualizer_RefreshRequested(object? sender, RefreshRequestedEventArgs e)
{ {
var ev = new RefreshRequestedEventArgs(e.GetRefreshCompletionDeferral(), RefreshRequestedEvent); var ev = new RefreshRequestedEventArgs(e.GetDeferral(), RefreshRequestedEvent);
RaiseEvent(ev); RaiseEvent(ev);
ev.DecrementCount(); ev.DecrementCount();
} }
@ -136,7 +159,7 @@ namespace Avalonia.Controls
} }
} }
} }
else if (change.Property == RefreshVisualizerProperty) else if (change.Property == VisualizerProperty)
{ {
if (_visualizerPresenter != null) if (_visualizerPresenter != null)
{ {
@ -212,11 +235,15 @@ namespace Avalonia.Controls
_refreshVisualizer.Bounds.Height == DefaultPullDimensionSize && _refreshVisualizer.Bounds.Height == DefaultPullDimensionSize &&
_refreshVisualizer.Bounds.Width == DefaultPullDimensionSize) _refreshVisualizer.Bounds.Width == DefaultPullDimensionSize)
{ {
RefreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection); _refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
} }
} }
} }
/// <summary>
/// Initiates an update of the content.
/// </summary>
public void RequestRefresh() public void RequestRefresh()
{ {
_refreshVisualizer?.RequestRefresh(); _refreshVisualizer?.RequestRefresh();

17
src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs

@ -5,9 +5,9 @@ using Avalonia.Rendering.Composition;
namespace Avalonia.Controls.PullToRefresh namespace Avalonia.Controls.PullToRefresh
{ {
public class RefreshInfoProvider : Interactive internal class RefreshInfoProvider : Interactive
{ {
internal const double ExecutionRatio = 0.8; internal const double DefaultExecutionRatio = 0.8;
private readonly PullDirection _refreshPullDirection; private readonly PullDirection _refreshPullDirection;
private readonly Size _refreshVisualizerSize; private readonly Size _refreshVisualizerSize;
@ -21,6 +21,11 @@ namespace Avalonia.Controls.PullToRefresh
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, bool>(nameof(IsInteractingForRefresh), AvaloniaProperty.RegisterDirect<RefreshInfoProvider, bool>(nameof(IsInteractingForRefresh),
s => s.IsInteractingForRefresh, (s, o) => s.IsInteractingForRefresh = o); s => s.IsInteractingForRefresh, (s, o) => s.IsInteractingForRefresh = o);
public DirectProperty<RefreshInfoProvider, double> ExecutionRatioProperty =
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, double>(nameof(ExecutionRatio),
s => s.ExecutionRatio);
public DirectProperty<RefreshInfoProvider, double> InteractionRatioProperty = public DirectProperty<RefreshInfoProvider, double> InteractionRatioProperty =
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, double>(nameof(InteractionRatio), AvaloniaProperty.RegisterDirect<RefreshInfoProvider, double>(nameof(InteractionRatio),
s => s.InteractionRatio, (s, o) => s.InteractionRatio = o); s => s.InteractionRatio, (s, o) => s.InteractionRatio = o);
@ -41,7 +46,8 @@ namespace Avalonia.Controls.PullToRefresh
public bool IsInteractingForRefresh public bool IsInteractingForRefresh
{ {
get => _isInteractingForRefresh; internal set get => _isInteractingForRefresh;
internal set
{ {
var isInteractingForRefresh = value && !PeekingMode; var isInteractingForRefresh = value && !PeekingMode;
@ -61,6 +67,11 @@ namespace Avalonia.Controls.PullToRefresh
} }
} }
public double ExecutionRatio
{
get => DefaultExecutionRatio;
}
internal CompositionVisual? Visual => _visual; internal CompositionVisual? Visual => _visual;
public event EventHandler<RoutedEventArgs> RefreshStarted public event EventHandler<RoutedEventArgs> RefreshStarted

217
src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs

@ -3,12 +3,14 @@ using System.Numerics;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading; using System.Threading;
using Avalonia.Animation; using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.PullToRefresh; using Avalonia.Controls.PullToRefresh;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Animations;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -16,7 +18,7 @@ namespace Avalonia.Controls
{ {
private const int DefaultIndicatorSize = 24; private const int DefaultIndicatorSize = 24;
private const float MinimumIndicatorOpacity = 0.4f; private const float MinimumIndicatorOpacity = 0.4f;
private const string ArrowPathData = "M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z"; private const float ParallaxPositionRatio = 0.5f;
private double _executingRatio = 0.8; private double _executingRatio = 0.8;
private RefreshVisualizerState _refreshVisualizerState; private RefreshVisualizerState _refreshVisualizerState;
@ -29,30 +31,49 @@ namespace Avalonia.Controls
private RefreshVisualizerOrientation _orientation; private RefreshVisualizerOrientation _orientation;
private float _startingRotationAngle; private float _startingRotationAngle;
private double _interactionRatio; private double _interactionRatio;
private bool _played;
private ScalarKeyFrameAnimation? _rotateAnimation;
private bool IsPullDirectionVertical => PullDirection == PullDirection.TopToBottom || PullDirection == PullDirection.BottomToTop; private bool IsPullDirectionVertical => PullDirection == PullDirection.TopToBottom || PullDirection == PullDirection.BottomToTop;
private bool IsPullDirectionFar => PullDirection == PullDirection.BottomToTop || PullDirection == PullDirection.RightToLeft; private bool IsPullDirectionFar => PullDirection == PullDirection.BottomToTop || PullDirection == PullDirection.RightToLeft;
/// <summary>
/// Defines the <see cref="PullDirection"/> property.
/// </summary>
public static readonly StyledProperty<PullDirection> PullDirectionProperty = public static readonly StyledProperty<PullDirection> PullDirectionProperty =
AvaloniaProperty.Register<RefreshVisualizer, PullDirection>(nameof(PullDirection), PullDirection.TopToBottom); AvaloniaProperty.Register<RefreshVisualizer, PullDirection>(nameof(PullDirection), PullDirection.TopToBottom);
/// <summary>
/// Defines the <see cref="RefreshRequested"/> event.
/// </summary>
public static readonly RoutedEvent<RefreshRequestedEventArgs> RefreshRequestedEvent = public static readonly RoutedEvent<RefreshRequestedEventArgs> RefreshRequestedEvent =
RoutedEvent.Register<RefreshVisualizer, RefreshRequestedEventArgs>(nameof(RefreshRequested), RoutingStrategies.Bubble); RoutedEvent.Register<RefreshVisualizer, RefreshRequestedEventArgs>(nameof(RefreshRequested), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="RefreshVisualizerState"/> property.
/// </summary>
public static readonly DirectProperty<RefreshVisualizer, RefreshVisualizerState> RefreshVisualizerStateProperty = public static readonly DirectProperty<RefreshVisualizer, RefreshVisualizerState> RefreshVisualizerStateProperty =
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshVisualizerState>(nameof(RefreshVisualizerState), AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshVisualizerState>(nameof(RefreshVisualizerState),
s => s.RefreshVisualizerState); s => s.RefreshVisualizerState);
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly DirectProperty<RefreshVisualizer, RefreshVisualizerOrientation> OrientationProperty = public static readonly DirectProperty<RefreshVisualizer, RefreshVisualizerOrientation> OrientationProperty =
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshVisualizerOrientation>(nameof(Orientation), AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshVisualizerOrientation>(nameof(Orientation),
s => s.Orientation, (s, o) => s.Orientation = o); s => s.Orientation, (s, o) => s.Orientation = o);
public DirectProperty<RefreshVisualizer, RefreshInfoProvider?> RefreshInfoProviderProperty = /// <summary>
/// Defines the <see cref="RefreshInfoProvider"/> property.
/// </summary>
internal DirectProperty<RefreshVisualizer, RefreshInfoProvider?> RefreshInfoProviderProperty =
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshInfoProvider?>(nameof(RefreshInfoProvider), AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshInfoProvider?>(nameof(RefreshInfoProvider),
s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o); s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o);
private Vector3 _defaultOffset;
private bool _played;
public RefreshVisualizerState RefreshVisualizerState /// <summary>
/// Gets or sets a value that indicates the refresh state of the visualizer.
/// </summary>
protected RefreshVisualizerState RefreshVisualizerState
{ {
get get
{ {
@ -65,6 +86,9 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Gets or sets a value that indicates the orientation of the visualizer.
/// </summary>
public RefreshVisualizerOrientation Orientation public RefreshVisualizerOrientation Orientation
{ {
get get
@ -90,9 +114,10 @@ namespace Avalonia.Controls
} }
} }
public RefreshInfoProvider? RefreshInfoProvider internal RefreshInfoProvider? RefreshInfoProvider
{ {
get => _refreshInfoProvider; internal set get => _refreshInfoProvider;
set
{ {
if (_refreshInfoProvider != null) if (_refreshInfoProvider != null)
{ {
@ -102,6 +127,9 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Occurs when an update of the content has been initiated.
/// </summary>
public event EventHandler<RefreshRequestedEventArgs>? RefreshRequested public event EventHandler<RefreshRequestedEventArgs>? RefreshRequested
{ {
add => AddHandler(RefreshRequestedEvent, value); add => AddHandler(RefreshRequestedEvent, value);
@ -119,14 +147,57 @@ namespace Avalonia.Controls
if (_root != null) if (_root != null)
{ {
_content = Content as Control; _content = Content as Control;
if (_content == null) if (_content == null)
{ {
Content = new PathIcon() _content = new PathIcon()
{ {
Data = PathGeometry.Parse(ArrowPathData),
Height = DefaultIndicatorSize, Height = DefaultIndicatorSize,
Width = DefaultIndicatorSize Width = DefaultIndicatorSize,
Name = "PART_Icon"
};
_content.Loaded += (s, e) =>
{
var composition = ElementComposition.GetElementVisual(_content);
var compositor = composition!.Compositor;
composition.Opacity = 0;
var smoothRotationAnimation
= compositor.CreateScalarKeyFrameAnimation();
smoothRotationAnimation.Target = "RotationAngle";
smoothRotationAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
smoothRotationAnimation.Duration = TimeSpan.FromMilliseconds(100);
var opacityAnimation
= compositor.CreateScalarKeyFrameAnimation();
opacityAnimation.Target = "Opacity";
opacityAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
opacityAnimation.Duration = TimeSpan.FromMilliseconds(100);
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
var scaleAnimation
= compositor.CreateVector3KeyFrameAnimation();
scaleAnimation.Target = "Scale";
scaleAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
scaleAnimation.Duration = TimeSpan.FromMilliseconds(100);
var animation = compositor.CreateImplicitAnimationCollection();
animation["RotationAngle"] = smoothRotationAnimation;
animation["Offset"] = offsetAnimation;
animation["Scale"] = scaleAnimation;
animation["Opacity"] = opacityAnimation;
composition.ImplicitAnimations = animation;
UpdateContent();
}; };
Content = _content;
} }
else else
{ {
@ -161,18 +232,23 @@ namespace Avalonia.Controls
{ {
case RefreshVisualizerState.Idle: case RefreshVisualizerState.Idle:
_played = false; _played = false;
if(_rotateAnimation != null)
{
_rotateAnimation.IterationBehavior = AnimationIterationBehavior.Count;
_rotateAnimation = null;
}
contentVisual.Opacity = MinimumIndicatorOpacity; contentVisual.Opacity = MinimumIndicatorOpacity;
contentVisual.RotationAngle = (float)(_startingRotationAngle * Math.PI / 180f); contentVisual.RotationAngle = _startingRotationAngle;
visualizerVisual.Offset = IsPullDirectionVertical ? visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, 0, 0) : new Vector3(visualizerVisual.Offset.X, 0, 0) :
new Vector3(0, visualizerVisual.Offset.Y, 0); new Vector3(0, visualizerVisual.Offset.Y, 0);
contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f);
visual.Offset = default; visual.Offset = default;
_content.InvalidateMeasure();
break; break;
case RefreshVisualizerState.Interacting: case RefreshVisualizerState.Interacting:
_played = false; _played = false;
contentVisual.Opacity = MinimumIndicatorOpacity; contentVisual.Opacity = MinimumIndicatorOpacity;
contentVisual.RotationAngle = (float)((_startingRotationAngle + (_interactionRatio * 360)) * Math.PI / 180f); contentVisual.RotationAngle = (float)(_startingRotationAngle + _interactionRatio * 2 * Math.PI);
Vector3 offset = default; Vector3 offset = default;
if (IsPullDirectionVertical) if (IsPullDirectionVertical)
{ {
@ -186,11 +262,10 @@ namespace Avalonia.Controls
visualizerVisual.Offset = IsPullDirectionVertical ? visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : new Vector3(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0); new Vector3(offset.X, visualizerVisual.Offset.Y, 0);
contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f);
break; break;
case RefreshVisualizerState.Pending: case RefreshVisualizerState.Pending:
contentVisual.Opacity = 1; contentVisual.Opacity = 1;
contentVisual.RotationAngle = (float)((_startingRotationAngle + 360) * Math.PI / 180f); contentVisual.RotationAngle = _startingRotationAngle + (float)(2 * Math.PI);
if (IsPullDirectionVertical) if (IsPullDirectionVertical)
{ {
offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
@ -217,17 +292,18 @@ namespace Avalonia.Controls
} }
break; break;
case RefreshVisualizerState.Refreshing: case RefreshVisualizerState.Refreshing:
var rotateAnimation = contentVisual.Compositor!.CreateScalarKeyFrameAnimation(); _rotateAnimation = contentVisual.Compositor!.CreateScalarKeyFrameAnimation();
rotateAnimation.Target = "RotationAngle"; _rotateAnimation.Target = "RotationAngle";
rotateAnimation.InsertKeyFrame(0, (float)(0)); _rotateAnimation.InsertKeyFrame(0, _startingRotationAngle, new LinearEasing());
rotateAnimation.InsertKeyFrame(0.5f, (float)(Math.PI)); _rotateAnimation.InsertKeyFrame(1, _startingRotationAngle + (float)(2 * Math.PI), new LinearEasing());
rotateAnimation.InsertKeyFrame(1, (float)(2 * Math.PI)); _rotateAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
rotateAnimation.Duration = TimeSpan.FromSeconds(1); _rotateAnimation.StopBehavior = AnimationStopBehavior.LeaveCurrentValue;
rotateAnimation.IterationCount = 1000; _rotateAnimation.Duration = TimeSpan.FromSeconds(0.5);
contentVisual.StartAnimation("RotationAngle", rotateAnimation); contentVisual.StartAnimation("RotationAngle", _rotateAnimation);
contentVisual.Opacity = 1; contentVisual.Opacity = 1;
contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f); float translationRatio = (float)(_refreshInfoProvider != null ? (1.0f - _refreshInfoProvider.ExecutionRatio) * ParallaxPositionRatio : 1.0f)
* (IsPullDirectionFar ? -1f : 1f);
if (IsPullDirectionVertical) if (IsPullDirectionVertical)
{ {
offset = new Vector3(0, (float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0); offset = new Vector3(0, (float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
@ -237,19 +313,24 @@ namespace Avalonia.Controls
offset = new Vector3((float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0); offset = new Vector3((float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
} }
visual.Offset = offset; visual.Offset = offset;
contentVisual.Offset += IsPullDirectionVertical ? new Vector3(0, (float)(translationRatio * root.Bounds.Height), 0) :
new Vector3((float)(translationRatio * root.Bounds.Width), 0, 0);
visualizerVisual.Offset = IsPullDirectionVertical ? visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) : new Vector3(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0); new Vector3(offset.X, visualizerVisual.Offset.Y, 0);
break; break;
case RefreshVisualizerState.Peeking: case RefreshVisualizerState.Peeking:
contentVisual.Opacity = 1; contentVisual.Opacity = 1;
contentVisual.RotationAngle = (float)(_startingRotationAngle * Math.PI / 180f); contentVisual.RotationAngle = _startingRotationAngle;
break; break;
} }
} }
} }
} }
/// <summary>
/// Initiates an update of the content.
/// </summary>
public void RequestRefresh() public void RequestRefresh()
{ {
RefreshVisualizerState = RefreshVisualizerState.Refreshing; RefreshVisualizerState = RefreshVisualizerState.Refreshing;
@ -286,63 +367,9 @@ namespace Avalonia.Controls
} }
else if (change.Property == ContentProperty) else if (change.Property == ContentProperty)
{ {
if (_root != null) if (_root != null && _content != null)
{ {
if (_content == null) _root.Children.Insert(0, _content);
{
_content = new PathIcon()
{
Data = PathGeometry.Parse(ArrowPathData),
Height = DefaultIndicatorSize,
Width = DefaultIndicatorSize
};
var transition = new Transitions
{
new DoubleTransition()
{
Property = OpacityProperty,
Duration = TimeSpan.FromSeconds(0.5)
},
};
_content.Transitions = transition;
_content.Loaded += (s, e) =>
{
var composition = ElementComposition.GetElementVisual(_content);
var compositor = composition!.Compositor;
composition.Opacity = 0;
var smoothRotationAnimation
= compositor.CreateScalarKeyFrameAnimation();
smoothRotationAnimation.Target = "RotationAngle";
smoothRotationAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
smoothRotationAnimation.Duration = TimeSpan.FromMilliseconds(100);
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
var scaleAnimation
= compositor.CreateVector3KeyFrameAnimation();
scaleAnimation.Target = "Scale";
scaleAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
scaleAnimation.Duration = TimeSpan.FromMilliseconds(100);
var animation = compositor.CreateImplicitAnimationCollection();
animation["RotationAngle"] = smoothRotationAnimation;
animation["Offset"] = offsetAnimation;
animation["Scale"] = scaleAnimation;
composition.ImplicitAnimations = animation;
UpdateContent();
};
}
_root.Children.Add(_content);
_content.VerticalAlignment = Layout.VerticalAlignment.Center; _content.VerticalAlignment = Layout.VerticalAlignment.Center;
_content.HorizontalAlignment = Layout.HorizontalAlignment.Center; _content.HorizontalAlignment = Layout.HorizontalAlignment.Center;
} }
@ -389,10 +416,10 @@ namespace Avalonia.Controls
_startingRotationAngle = 0.0f; _startingRotationAngle = 0.0f;
break; break;
case PullDirection.LeftToRight: case PullDirection.LeftToRight:
_startingRotationAngle = 270; _startingRotationAngle = (float)(-Math.PI / 2);
break; break;
case PullDirection.RightToLeft: case PullDirection.RightToLeft:
_startingRotationAngle = 90; _startingRotationAngle = (float)(Math.PI / 2);
break; break;
} }
break; break;
@ -400,10 +427,10 @@ namespace Avalonia.Controls
_startingRotationAngle = 0.0f; _startingRotationAngle = 0.0f;
break; break;
case RefreshVisualizerOrientation.Rotate90DegreesCounterclockwise: case RefreshVisualizerOrientation.Rotate90DegreesCounterclockwise:
_startingRotationAngle = 270; _startingRotationAngle = (float)(Math.PI / 2);
break; break;
case RefreshVisualizerOrientation.Rotate270DegreesCounterclockwise: case RefreshVisualizerOrientation.Rotate270DegreesCounterclockwise:
_startingRotationAngle = 90; _startingRotationAngle = (float)(-Math.PI / 2);
break; break;
} }
} }
@ -527,6 +554,9 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Defines constants that specify the state of a RefreshVisualizer
/// </summary>
public enum RefreshVisualizerState public enum RefreshVisualizerState
{ {
Idle, Idle,
@ -536,6 +566,9 @@ namespace Avalonia.Controls
Refreshing Refreshing
} }
/// <summary>
/// Defines constants that specify the orientation of a RefreshVisualizer.
/// </summary>
public enum RefreshVisualizerOrientation public enum RefreshVisualizerOrientation
{ {
Auto, Auto,
@ -544,11 +577,18 @@ namespace Avalonia.Controls
Rotate270DegreesCounterclockwise Rotate270DegreesCounterclockwise
} }
/// <summary>
/// Provides event data for RefreshRequested events.
/// </summary>
public class RefreshRequestedEventArgs : RoutedEventArgs public class RefreshRequestedEventArgs : RoutedEventArgs
{ {
private RefreshCompletionDeferral _refreshCompletionDeferral; private RefreshCompletionDeferral _refreshCompletionDeferral;
public RefreshCompletionDeferral GetRefreshCompletionDeferral() /// <summary>
/// Gets a deferral object for managing the work done in the RefreshRequested event handler.
/// </summary>
/// <returns>A <see cref="RefreshCompletionDeferral"/> object</returns>
public RefreshCompletionDeferral GetDeferral()
{ {
return _refreshCompletionDeferral.Get(); return _refreshCompletionDeferral.Get();
} }
@ -574,6 +614,9 @@ namespace Avalonia.Controls
} }
} }
/// <summary>
/// Deferral class for notify that a work done in RefreshRequested event is done.
/// </summary>
public class RefreshCompletionDeferral public class RefreshCompletionDeferral
{ {
private Action _deferredAction; private Action _deferredAction;

2
src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs

@ -6,7 +6,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.PullToRefresh namespace Avalonia.Controls.PullToRefresh
{ {
public class ScrollViewerIRefreshInfoProviderAdapter internal class ScrollViewerIRefreshInfoProviderAdapter
{ {
private const int MaxSearchDepth = 10; private const int MaxSearchDepth = 10;
private const int InitialOffsetThreshold = 1; private const int InitialOffsetThreshold = 1;

32
src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml

@ -12,26 +12,18 @@
Value="{DynamicResource RefreshVisualizerForeground}"/> Value="{DynamicResource RefreshVisualizerForeground}"/>
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
<Grid Name="PART_Root" MinHeight="80" Background="{TemplateBinding Background}"> <Grid Name="PART_Root"
<Grid.Styles> MinHeight="80"
<Style Selector="Grid > PathIcon.refreshing"> Background="{TemplateBinding Background}">
<Style.Animations> <Grid.Styles>
<Animation Duration="0:0:1" <Style Selector="PathIcon#PART_Icon">
IterationCount="Infinite"> <Setter Property="Data"
<KeyFrame Cue="0%"> Value="M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z">
<Setter Property="RotateTransform.Angle" </Setter>
Value="0"/> </Style>
</KeyFrame> </Grid.Styles>
<KeyFrame Cue="100%"> </Grid>
<Setter Property="RotateTransform.Angle" </ControlTemplate>
Value="360"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Grid.Styles>
</Grid>
</ControlTemplate>
</Setter> </Setter>
</ControlTheme> </ControlTheme>
</ResourceDictionary> </ResourceDictionary>

49
src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml

@ -18,51 +18,10 @@
MinHeight="80" MinHeight="80"
Background="{TemplateBinding Background}"> Background="{TemplateBinding Background}">
<Grid.Styles> <Grid.Styles>
<Style Selector="Grid.pending > Grid"> <Style Selector="PathIcon#PART_Icon">
<Style.Animations> <Setter Property="Data"
<Animation IterationCount="1" Value="M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z">
Duration="0:0:0.25"> </Setter>
<KeyFrame Cue="0%">
<Setter Property="ScaleTransform.ScaleX"
Value="1"/>
</KeyFrame>
<KeyFrame Cue="0%">
<Setter Property="ScaleTransform.ScaleY"
Value="1"/>
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="ScaleTransform.ScaleX"
Value="1.2"/>
</KeyFrame>
<KeyFrame Cue="50%">
<Setter Property="ScaleTransform.ScaleY"
Value="1.2"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="ScaleTransform.ScaleX"
Value="1"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="ScaleTransform.ScaleY"
Value="1"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Grid > Grid > PathIcon.refreshing">
<Style.Animations>
<Animation Duration="0:0:1"
IterationCount="Infinite">
<KeyFrame Cue="0%">
<Setter Property="RotateTransform.Angle"
Value="0"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="RotateTransform.Angle"
Value="360"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style> </Style>
</Grid.Styles> </Grid.Styles>
</Grid> </Grid>

Loading…
Cancel
Save