Browse Source

Use Composition Api for animation

pull/9437/head
Emmanuel Hansen 4 years ago
parent
commit
68fd64f164
  1. 2
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  2. 2
      src/Avalonia.Base/Input/PullGestureRecognizer.cs
  3. 48
      src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs
  4. 13
      src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs
  5. 282
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
  6. 91
      src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs
  7. 32
      src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml

2
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

@ -2,9 +2,11 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:ControlCatalog.ViewModels"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="800" d:DesignWidth="800"
d:DesignHeight="450" d:DesignHeight="450"
x:DataType="viewModels:RefreshContainerViewModel"
x:Class="ControlCatalog.Pages.RefreshContainerPage"> x:Class="ControlCatalog.Pages.RefreshContainerPage">
<DockPanel HorizontalAlignment="Stretch" <DockPanel HorizontalAlignment="Stretch"
Height="600" Height="600"

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

@ -100,7 +100,7 @@ namespace Avalonia.Input
public void PointerPressed(PointerPressedEventArgs e) public void PointerPressed(PointerPressedEventArgs e)
{ {
if (_target != null && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen)) if (_target != null)// && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
{ {
var position = e.GetPosition(_target); var position = e.GetPosition(_target);

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

@ -12,27 +12,27 @@ namespace Avalonia.Controls
private readonly bool _hasDefaultRefreshInfoProviderAdapter; private readonly 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;
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);
internal static readonly DirectProperty<RefreshContainer, ScrollViewerIRefreshInfoProviderAdapter> RefreshInfoProviderAdapterProperty = internal static readonly DirectProperty<RefreshContainer, ScrollViewerIRefreshInfoProviderAdapter?> RefreshInfoProviderAdapterProperty =
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 = public static readonly DirectProperty<RefreshContainer, RefreshVisualizer?> RefreshVisualizerProperty =
AvaloniaProperty.RegisterDirect<RefreshContainer, RefreshVisualizer>(nameof(RefreshVisualizer), AvaloniaProperty.RegisterDirect<RefreshContainer, RefreshVisualizer?>(nameof(RefreshVisualizer),
s => s.RefreshVisualizer, (s, o) => s.RefreshVisualizer = o); s => s.RefreshVisualizer, (s, o) => s.RefreshVisualizer = o);
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 public ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter
{ {
get => _refreshInfoProviderAdapter; set get => _refreshInfoProviderAdapter; set
{ {
@ -42,7 +42,7 @@ namespace Avalonia.Controls
private bool _hasDefaultRefreshVisualizer; private bool _hasDefaultRefreshVisualizer;
public RefreshVisualizer RefreshVisualizer public RefreshVisualizer? RefreshVisualizer
{ {
get => _refreshVisualizer; set get => _refreshVisualizer; set
{ {
@ -88,7 +88,7 @@ namespace Avalonia.Controls
else else
{ {
_hasDefaultRefreshVisualizer = false; _hasDefaultRefreshVisualizer = false;
RaisePropertyChanged(RefreshVisualizerProperty, null, _refreshVisualizer); RaisePropertyChanged(RefreshVisualizerProperty, default, _refreshVisualizer);
} }
OnPullDirectionChanged(); OnPullDirectionChanged();
@ -115,19 +115,23 @@ namespace Avalonia.Controls
if (change.Property == RefreshInfoProviderAdapterProperty) if (change.Property == RefreshInfoProviderAdapterProperty)
{ {
if (_refreshInfoProvider != null) if (_refreshVisualizer != null)
{
_refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider;
}
else
{ {
if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null) if (_refreshInfoProvider != null)
{ {
_refreshInfoProvider = RefreshInfoProviderAdapter.AdaptFromTree(this, _refreshVisualizer.Bounds.Size); _refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider;
}
if (_refreshInfoProvider != null) else
{
if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null)
{ {
_refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider; _refreshInfoProvider = RefreshInfoProviderAdapter?.AdaptFromTree(this, _refreshVisualizer.Bounds.Size);
if (_refreshInfoProvider != null)
{
_refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider;
RefreshInfoProviderAdapter?.SetAnimations(_refreshVisualizer);
}
} }
} }
} }
@ -157,7 +161,7 @@ namespace Avalonia.Controls
private void OnPullDirectionChanged() private void OnPullDirectionChanged()
{ {
if (_visualizerPresenter != null) if (_visualizerPresenter != null && _refreshVisualizer != null)
{ {
switch (PullDirection) switch (PullDirection)
{ {

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

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Rendering.Composition;
namespace Avalonia.Controls.PullToRefresh namespace Avalonia.Controls.PullToRefresh
{ {
@ -11,7 +12,7 @@ namespace Avalonia.Controls.PullToRefresh
private readonly PullDirection _refreshPullDirection; private readonly PullDirection _refreshPullDirection;
private readonly Size _refreshVisualizerSize; private readonly Size _refreshVisualizerSize;
private readonly Visual _visual; private readonly CompositionVisual? _visual;
private bool _isInteractingForRefresh; private bool _isInteractingForRefresh;
private double _interactionRatio; private double _interactionRatio;
private bool _entered; private bool _entered;
@ -60,7 +61,7 @@ namespace Avalonia.Controls.PullToRefresh
} }
} }
internal Visual Visual => _visual; internal CompositionVisual? Visual => _visual;
public event EventHandler<RoutedEventArgs> RefreshStarted public event EventHandler<RoutedEventArgs> RefreshStarted
{ {
@ -74,7 +75,7 @@ namespace Avalonia.Controls.PullToRefresh
remove => RemoveHandler(RefreshCompletedEvent, value); remove => RemoveHandler(RefreshCompletedEvent, value);
} }
internal void InteractingStateEntered(object sender, PullGestureEventArgs e) internal void InteractingStateEntered(object? sender, PullGestureEventArgs e)
{ {
if (!_entered) if (!_entered)
{ {
@ -85,7 +86,7 @@ namespace Avalonia.Controls.PullToRefresh
ValuesChanged(e.Delta); ValuesChanged(e.Delta);
} }
internal void InteractingStateExited(object sender, PullGestureEndedEventArgs e) internal void InteractingStateExited(object? sender, PullGestureEndedEventArgs e)
{ {
IsInteractingForRefresh = false; IsInteractingForRefresh = false;
_entered = false; _entered = false;
@ -94,10 +95,10 @@ namespace Avalonia.Controls.PullToRefresh
} }
public RefreshInfoProvider(PullDirection refreshPullDirection, Size refreshVIsualizerSize, Visual visual) public RefreshInfoProvider(PullDirection refreshPullDirection, Size? refreshVIsualizerSize, CompositionVisual? visual)
{ {
_refreshPullDirection = refreshPullDirection; _refreshPullDirection = refreshPullDirection;
_refreshVisualizerSize = refreshVIsualizerSize; _refreshVisualizerSize = refreshVIsualizerSize ?? default;
_visual = visual; _visual = visual;
} }

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

@ -1,4 +1,5 @@
using System; using System;
using System.Numerics;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading; using System.Threading;
using Avalonia.Animation; using Avalonia.Animation;
@ -7,25 +8,24 @@ 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;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
public class RefreshVisualizer : ContentControl public class RefreshVisualizer : ContentControl
{ {
private const int DefaultIndicatorSize = 24; private const int DefaultIndicatorSize = 24;
private const double MinimumIndicatorOpacity = 0.4; 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 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 double _executingRatio = 0.8; private double _executingRatio = 0.8;
private RotateTransform _visualizerRotateTransform;
private TranslateTransform _contentTranslateTransform;
private RefreshVisualizerState _refreshVisualizerState; private RefreshVisualizerState _refreshVisualizerState;
private RefreshInfoProvider _refreshInfoProvider; private RefreshInfoProvider? _refreshInfoProvider;
private IDisposable _isInteractingSubscription; private IDisposable? _isInteractingSubscription;
private IDisposable _interactionRatioSubscription; private IDisposable? _interactionRatioSubscription;
private bool _isInteractingForRefresh; private bool _isInteractingForRefresh;
private Grid? _root; private Grid? _root;
private Control _content; private Control? _content;
private RefreshVisualizerOrientation _orientation; private RefreshVisualizerOrientation _orientation;
private float _startingRotationAngle; private float _startingRotationAngle;
private double _interactionRatio; private double _interactionRatio;
@ -46,9 +46,11 @@ namespace Avalonia.Controls
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 = public 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 public RefreshVisualizerState RefreshVisualizerState
{ {
@ -88,7 +90,7 @@ namespace Avalonia.Controls
} }
} }
public RefreshInfoProvider RefreshInfoProvider public RefreshInfoProvider? RefreshInfoProvider
{ {
get => _refreshInfoProvider; internal set get => _refreshInfoProvider; internal set
{ {
@ -106,20 +108,17 @@ namespace Avalonia.Controls
remove => RemoveHandler(RefreshRequestedEvent, value); remove => RemoveHandler(RefreshRequestedEvent, value);
} }
public RefreshVisualizer()
{
_visualizerRotateTransform = new RotateTransform();
_contentTranslateTransform = new TranslateTransform();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
this.ClipToBounds = false;
_root = e.NameScope.Find<Grid>("PART_Root"); _root = e.NameScope.Find<Grid>("PART_Root");
if (_root != null) if (_root != null)
{ {
_content = Content as Control;
if (_content == null) if (_content == null)
{ {
Content = new PathIcon() Content = new PathIcon()
@ -140,69 +139,113 @@ namespace Avalonia.Controls
UpdateContent(); UpdateContent();
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
UpdateContent();
}
private void UpdateContent() private void UpdateContent()
{ {
if (_content != null) if (_content != null && _root != null)
{ {
switch (RefreshVisualizerState) var root = _root;
var visual = _refreshInfoProvider?.Visual;
var contentVisual = ElementComposition.GetElementVisual(_content);
var visualizerVisual = ElementComposition.GetElementVisual(this);
if (visual != null && contentVisual != null && visualizerVisual != null)
{ {
case RefreshVisualizerState.Idle: contentVisual.CenterPoint = new Vector3((float)(_content.Bounds.Width / 2), (float)(_content.Bounds.Height / 2), 0);
_content.Classes.Remove("refreshing"); switch (RefreshVisualizerState)
_root.Classes.Remove("pending"); {
_content.RenderTransform = _visualizerRotateTransform; case RefreshVisualizerState.Idle:
_content.Opacity = MinimumIndicatorOpacity; _played = false;
_visualizerRotateTransform.Angle = _startingRotationAngle; contentVisual.Opacity = MinimumIndicatorOpacity;
_contentTranslateTransform.X = 0; contentVisual.RotationAngle = (float)(_startingRotationAngle * Math.PI / 180f);
_contentTranslateTransform.Y = 0; visualizerVisual.Offset = IsPullDirectionVertical ?
break; new Vector3(visualizerVisual.Offset.X, 0, 0) :
case RefreshVisualizerState.Interacting: new Vector3(0, visualizerVisual.Offset.Y, 0);
_content.Classes.Remove("refreshing"); contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f);
_root.Classes.Remove("pending"); visual.Offset = default;
_content.RenderTransform = _visualizerRotateTransform; break;
_content.Opacity = MinimumIndicatorOpacity; case RefreshVisualizerState.Interacting:
_visualizerRotateTransform.Angle = _startingRotationAngle + (_interactionRatio * 360); _played = false;
_content.Height = DefaultIndicatorSize; contentVisual.Opacity = MinimumIndicatorOpacity;
_content.Width = DefaultIndicatorSize; contentVisual.RotationAngle = (float)((_startingRotationAngle + (_interactionRatio * 360)) * Math.PI / 180f);
if (IsPullDirectionVertical) Vector3 offset = default;
{ if (IsPullDirectionVertical)
_contentTranslateTransform.X = 0; {
_contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
} }
else else
{ {
_contentTranslateTransform.Y = 0; offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
_contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; }
} visual.Offset = offset;
break; visualizerVisual.Offset = IsPullDirectionVertical ?
case RefreshVisualizerState.Pending: new Vector3(visualizerVisual.Offset.X, offset.Y, 0) :
_content.Classes.Remove("refreshing"); new Vector3(offset.X, visualizerVisual.Offset.Y, 0);
_content.Opacity = 1; contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f);
_content.RenderTransform = _visualizerRotateTransform; break;
if (IsPullDirectionVertical) case RefreshVisualizerState.Pending:
{ contentVisual.Opacity = 1;
_contentTranslateTransform.X = 0; contentVisual.RotationAngle = (float)((_startingRotationAngle + 360) * Math.PI / 180f);
_contentTranslateTransform.Y = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Height; if (IsPullDirectionVertical)
} {
else offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
{ }
_contentTranslateTransform.Y = 0; else
_contentTranslateTransform.X = _interactionRatio * (IsPullDirectionFar ? -1 : 1) * _root.Bounds.Width; {
} offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
}
_root.Classes.Add("pending"); visual.Offset = offset;
break; visualizerVisual.Offset = IsPullDirectionVertical ?
case RefreshVisualizerState.Refreshing: new Vector3(visualizerVisual.Offset.X, offset.Y, 0) :
_root.Classes.Remove("pending"); new Vector3(offset.X, visualizerVisual.Offset.Y, 0);
_content.Classes.Add("refreshing");
_content.Opacity = 1; if (!_played)
_content.Height = DefaultIndicatorSize; {
_content.Width = DefaultIndicatorSize; _played = true;
break; var scaleAnimation = contentVisual.Compositor!.CreateVector3KeyFrameAnimation();
case RefreshVisualizerState.Peeking: scaleAnimation.Target = "Scale";
_root.Classes.Remove("pending"); scaleAnimation.InsertKeyFrame(0.5f, new Vector3(1.5f, 1.5f, 1));
_content.Opacity = 1; scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1));
_visualizerRotateTransform.Angle += _startingRotationAngle; scaleAnimation.Duration = TimeSpan.FromSeconds(0.3);
break;
contentVisual.StartAnimation("Scale", scaleAnimation);
}
break;
case RefreshVisualizerState.Refreshing:
var rotateAnimation = contentVisual.Compositor!.CreateScalarKeyFrameAnimation();
rotateAnimation.Target = "RotationAngle";
rotateAnimation.InsertKeyFrame(0, (float)(0));
rotateAnimation.InsertKeyFrame(0.5f, (float)(Math.PI));
rotateAnimation.InsertKeyFrame(1, (float)(2 * Math.PI));
rotateAnimation.Duration = TimeSpan.FromSeconds(1);
rotateAnimation.IterationCount = 1000;
contentVisual.StartAnimation("RotationAngle", rotateAnimation);
contentVisual.Opacity = 1;
contentVisual.Scale = new Vector3(0.9f, 0.9f, 0.9f);
if (IsPullDirectionVertical)
{
offset = new Vector3(0, (float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
}
else
{
offset = new Vector3((float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
}
visual.Offset = offset;
visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0);
break;
case RefreshVisualizerState.Peeking:
contentVisual.Opacity = 1;
contentVisual.RotationAngle = (float)(_startingRotationAngle * Math.PI / 180f);
break;
}
} }
} }
} }
@ -254,12 +297,6 @@ namespace Avalonia.Controls
Width = DefaultIndicatorSize Width = DefaultIndicatorSize
}; };
var transformGroup = new TransformGroup();
transformGroup.Children.Add(_visualizerRotateTransform);
_content.RenderTransform = _visualizerRotateTransform;
_root.RenderTransform = _contentTranslateTransform;
var transition = new Transitions var transition = new Transitions
{ {
new DoubleTransition() new DoubleTransition()
@ -270,16 +307,42 @@ namespace Avalonia.Controls
}; };
_content.Transitions = transition; _content.Transitions = transition;
}
var scalingGrid = new Grid(); _content.Loaded += (s, e) =>
scalingGrid.VerticalAlignment = Layout.VerticalAlignment.Center; {
scalingGrid.HorizontalAlignment = Layout.HorizontalAlignment.Center; var composition = ElementComposition.GetElementVisual(_content);
scalingGrid.RenderTransform = new ScaleTransform(); var compositor = composition!.Compositor;
composition.Opacity = 0;
scalingGrid.Children.Add(_content);
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.Insert(0, scalingGrid); _root.Children.Add(_content);
_content.VerticalAlignment = Layout.VerticalAlignment.Center; _content.VerticalAlignment = Layout.VerticalAlignment.Center;
_content.HorizontalAlignment = Layout.HorizontalAlignment.Center; _content.HorizontalAlignment = Layout.HorizontalAlignment.Center;
} }
@ -294,25 +357,23 @@ namespace Avalonia.Controls
} }
else if (change.Property == BoundsProperty) else if (change.Property == BoundsProperty)
{ {
if (_content != null) switch (PullDirection)
{ {
var parent = _content.Parent as Control; case PullDirection.TopToBottom:
switch (PullDirection) RenderTransform = new TranslateTransform(0, -Bounds.Height);
{ break;
case PullDirection.TopToBottom: case PullDirection.BottomToTop:
parent.Margin = new Thickness(0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0); RenderTransform = new TranslateTransform(0, Bounds.Height);
break; break;
case PullDirection.BottomToTop: case PullDirection.LeftToRight:
parent.Margin = new Thickness(0, 0, 0, -Bounds.Height - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize)); RenderTransform = new TranslateTransform(-Bounds.Width, 0);
break; break;
case PullDirection.LeftToRight: case PullDirection.RightToLeft:
parent.Margin = new Thickness(-Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0, 0, 0); RenderTransform = new TranslateTransform(Bounds.Width, 0);
break; break;
case PullDirection.RightToLeft:
parent.Margin = new Thickness(0, 0, -Bounds.Width - DefaultIndicatorSize - (0.5 * DefaultIndicatorSize), 0);
break;
}
} }
UpdateContent();
} }
} }
@ -354,16 +415,15 @@ namespace Avalonia.Controls
_interactionRatioSubscription?.Dispose(); _interactionRatioSubscription?.Dispose();
_interactionRatioSubscription = null; _interactionRatioSubscription = null;
if (_refreshInfoProvider != null) if (RefreshInfoProvider != null)
{ {
_isInteractingSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty) _isInteractingSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty)
.Subscribe(InteractingForRefreshObserver); .Subscribe(InteractingForRefreshObserver);
_interactionRatioSubscription = _refreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty) _interactionRatioSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty)
.Subscribe(InteractionRatioObserver); .Subscribe(InteractionRatioObserver);
var visual = _refreshInfoProvider.Visual; var visual = RefreshInfoProvider.Visual;
visual.RenderTransform = _contentTranslateTransform;
_executingRatio = RefreshInfoProvider.ExecutionRatio; _executingRatio = RefreshInfoProvider.ExecutionRatio;
} }

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

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Rendering.Composition;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Controls.PullToRefresh namespace Avalonia.Controls.PullToRefresh
@ -11,9 +12,9 @@ namespace Avalonia.Controls.PullToRefresh
private const int InitialOffsetThreshold = 1; private const int InitialOffsetThreshold = 1;
private PullDirection _refreshPullDirection; private PullDirection _refreshPullDirection;
private ScrollViewer _scrollViewer; private ScrollViewer? _scrollViewer;
private RefreshInfoProvider _refreshInfoProvider; private RefreshInfoProvider? _refreshInfoProvider;
private PullGestureRecognizer _pullGestureRecognizer; private PullGestureRecognizer? _pullGestureRecognizer;
private InputElement? _interactionSource; private InputElement? _interactionSource;
private bool _isVisualizerInteractionSourceAttached; private bool _isVisualizerInteractionSourceAttached;
@ -22,7 +23,7 @@ namespace Avalonia.Controls.PullToRefresh
_refreshPullDirection = pullDirection; _refreshPullDirection = pullDirection;
} }
public RefreshInfoProvider AdaptFromTree(IVisual root, Size refreshVIsualizerSize) public RefreshInfoProvider? AdaptFromTree(IVisual root, Size? refreshVIsualizerSize)
{ {
if (root is ScrollViewer scrollViewer) if (root is ScrollViewer scrollViewer)
{ {
@ -44,7 +45,7 @@ namespace Avalonia.Controls.PullToRefresh
} }
} }
ScrollViewer AdaptFromTreeRecursiveHelper(IVisual root, int depth) ScrollViewer? AdaptFromTreeRecursiveHelper(IVisual root, int depth)
{ {
if (depth == 0) if (depth == 0)
{ {
@ -74,7 +75,7 @@ namespace Avalonia.Controls.PullToRefresh
return null; return null;
} }
public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size refreshVIsualizerSize) public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size? refreshVIsualizerSize)
{ {
if (adaptee == null) if (adaptee == null)
{ {
@ -121,7 +122,7 @@ namespace Avalonia.Controls.PullToRefresh
} }
} }
_refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, content); _refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, ElementComposition.GetElementVisual(content));
_pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection); _pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection);
@ -140,7 +141,7 @@ namespace Avalonia.Controls.PullToRefresh
return _refreshInfoProvider; return _refreshInfoProvider;
} }
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) private void ScrollViewer_ScrollChanged(object? sender, ScrollChangedEventArgs e)
{ {
if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh) if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh)
{ {
@ -151,9 +152,46 @@ namespace Avalonia.Controls.PullToRefresh
} }
} }
private void ScrollViewer_Loaded(object sender, RoutedEventArgs e) public void SetAnimations(RefreshVisualizer refreshVisualizer)
{ {
var content = _scrollViewer.Content as Visual; var visualizerComposition = ElementComposition.GetElementVisual(refreshVisualizer);
if (visualizerComposition != null)
{
var compositor = visualizerComposition.Compositor;
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
var animation = compositor.CreateImplicitAnimationCollection();
animation["Offset"] = offsetAnimation;
visualizerComposition.ImplicitAnimations = animation;
}
if(_scrollViewer != null && _scrollViewer.Content is Visual visual)
{
var scollContentComposition = ElementComposition.GetElementVisual(visual);
if(scollContentComposition != null)
{
var compositor = scollContentComposition.Compositor;
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
var animation = compositor.CreateImplicitAnimationCollection();
animation["Offset"] = offsetAnimation;
scollContentComposition.ImplicitAnimations = animation;
}
}
}
private void ScrollViewer_Loaded(object? sender, RoutedEventArgs? e)
{
var content = _scrollViewer?.Content as Visual;
if (content == null) if (content == null)
{ {
throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual"); throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual");
@ -166,23 +204,26 @@ namespace Avalonia.Controls.PullToRefresh
MakeInteractionSource(content.Parent as InputElement); MakeInteractionSource(content.Parent as InputElement);
_scrollViewer.Loaded -= ScrollViewer_Loaded; if (_scrollViewer != null)
{
_scrollViewer.Loaded -= ScrollViewer_Loaded;
}
} }
private void MakeInteractionSource(InputElement element) private void MakeInteractionSource(InputElement? element)
{ {
_interactionSource = element; _interactionSource = element;
if (_pullGestureRecognizer != null) if (_pullGestureRecognizer != null && _refreshInfoProvider != null)
{ {
element.GestureRecognizers.Add(_pullGestureRecognizer); element?.GestureRecognizers.Add(_pullGestureRecognizer);
_interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered); _interactionSource?.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered);
_interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited); _interactionSource?.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited);
_isVisualizerInteractionSourceAttached = true; _isVisualizerInteractionSourceAttached = true;
} }
} }
private void ScrollViewer_PointerReleased(object sender, PointerReleasedEventArgs e) private void ScrollViewer_PointerReleased(object? sender, PointerReleasedEventArgs e)
{ {
if (_refreshInfoProvider != null) if (_refreshInfoProvider != null)
{ {
@ -190,9 +231,12 @@ namespace Avalonia.Controls.PullToRefresh
} }
} }
private void ScrollViewer_PointerPressed(object sender, PointerPressedEventArgs e) private void ScrollViewer_PointerPressed(object? sender, PointerPressedEventArgs e)
{ {
_refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold(); if (_refreshInfoProvider != null)
{
_refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold();
}
} }
private bool IsWithinOffsetThreashold() private bool IsWithinOffsetThreashold()
@ -219,9 +263,12 @@ namespace Avalonia.Controls.PullToRefresh
private void CleanUpScrollViewer() private void CleanUpScrollViewer()
{ {
_scrollViewer.PointerPressed -= ScrollViewer_PointerPressed; if (_scrollViewer != null)
_scrollViewer.PointerReleased -= ScrollViewer_PointerReleased; {
_scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged; _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed;
_scrollViewer.PointerReleased -= ScrollViewer_PointerReleased;
_scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
}
} }
} }
} }

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

@ -14,37 +14,7 @@
<ControlTemplate> <ControlTemplate>
<Grid Name="PART_Root" MinHeight="80" Background="{TemplateBinding Background}"> <Grid Name="PART_Root" MinHeight="80" Background="{TemplateBinding Background}">
<Grid.Styles> <Grid.Styles>
<Style Selector="Grid.pending > Grid"> <Style Selector="Grid > PathIcon.refreshing">
<Style.Animations>
<Animation IterationCount="1" Duration="0:0:0.25">
<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> <Style.Animations>
<Animation Duration="0:0:1" <Animation Duration="0:0:1"
IterationCount="Infinite"> IterationCount="Infinite">

Loading…
Cancel
Save