Browse Source

Merge pull request #1461 from jmacato/wip-animations

CSS-like Animations & Transitions
pull/1618/head
Steven Kirk 8 years ago
committed by GitHub
parent
commit
0c36c1baab
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      samples/ControlCatalog/MainView.xaml
  2. 4
      samples/ControlCatalog/Pages/CarouselPage.xaml
  3. 6
      samples/ControlCatalog/Pages/CarouselPage.xaml.cs
  4. 16
      samples/ControlCatalog/SideBar.xaml
  5. 13
      samples/RenderTest/App.xaml.cs
  6. 4
      samples/RenderTest/MainWindow.xaml
  7. 123
      samples/RenderTest/Pages/AnimationsPage.xaml
  8. 69
      samples/RenderTest/Pages/AnimationsPage.xaml.cs
  9. 45
      samples/RenderTest/Pages/ClippingPage.xaml
  10. 13
      samples/RenderTest/Pages/ClippingPage.xaml.cs
  11. 24
      samples/RenderTest/Program.cs
  12. 17
      samples/RenderTest/SideBar.xaml
  13. 40
      samples/RenderTest/ViewModels/AnimationsPageViewModel.cs
  14. 2
      samples/RenderTest/ViewModels/MainWindowViewModel.cs
  15. 92
      src/Avalonia.Animation/Animatable.cs
  16. 134
      src/Avalonia.Animation/Animate.cs
  17. 136
      src/Avalonia.Animation/Animation.cs
  18. 45
      src/Avalonia.Animation/AnimationExtensions.cs
  19. 17
      src/Avalonia.Animation/AnimationSetter.cs
  20. 56
      src/Avalonia.Animation/Animation`1.cs
  21. 18
      src/Avalonia.Animation/AnimatorAttribute.cs
  22. 23
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  23. 256
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  24. 192
      src/Avalonia.Animation/Animator`1.cs
  25. 88
      src/Avalonia.Animation/Cue.cs
  26. 41
      src/Avalonia.Animation/DoubleAnimator.cs
  27. 21
      src/Avalonia.Animation/DoubleSetter.cs
  28. 23
      src/Avalonia.Animation/DoubleTransition.cs
  29. 20
      src/Avalonia.Animation/Easing/BackEaseIn.cs
  30. 31
      src/Avalonia.Animation/Easing/BackEaseInOut.cs
  31. 21
      src/Avalonia.Animation/Easing/BackEaseOut.cs
  32. 21
      src/Avalonia.Animation/Easing/BounceEaseIn.cs
  33. 28
      src/Avalonia.Animation/Easing/BounceEaseInOut.cs
  34. 19
      src/Avalonia.Animation/Easing/BounceEaseOut.cs
  35. 21
      src/Avalonia.Animation/Easing/CircularEaseIn.cs
  36. 29
      src/Avalonia.Animation/Easing/CircularEaseInOut.cs
  37. 22
      src/Avalonia.Animation/Easing/CircularEaseOut.cs
  38. 18
      src/Avalonia.Animation/Easing/CubicEaseIn.cs
  39. 28
      src/Avalonia.Animation/Easing/CubicEaseInOut.cs
  40. 19
      src/Avalonia.Animation/Easing/CubicEaseOut.cs
  41. 56
      src/Avalonia.Animation/Easing/Easing.cs
  42. 23
      src/Avalonia.Animation/Easing/EasingTypeConverter.cs
  43. 22
      src/Avalonia.Animation/Easing/ElasticEaseIn.cs
  44. 31
      src/Avalonia.Animation/Easing/ElasticEaseInOut.cs
  45. 22
      src/Avalonia.Animation/Easing/ElasticEaseOut.cs
  46. 21
      src/Avalonia.Animation/Easing/ExponentialEaseIn.cs
  47. 29
      src/Avalonia.Animation/Easing/ExponentialEaseInOut.cs
  48. 21
      src/Avalonia.Animation/Easing/ExponentialEaseOut.cs
  49. 17
      src/Avalonia.Animation/Easing/LinearEasing.cs
  50. 18
      src/Avalonia.Animation/Easing/QuadraticEaseIn.cs
  51. 27
      src/Avalonia.Animation/Easing/QuadraticEaseInOut.cs
  52. 18
      src/Avalonia.Animation/Easing/QuadraticEaseOut.cs
  53. 19
      src/Avalonia.Animation/Easing/QuarticEaseIn.cs
  54. 30
      src/Avalonia.Animation/Easing/QuarticEaseInOut.cs
  55. 20
      src/Avalonia.Animation/Easing/QuarticEaseOut.cs
  56. 19
      src/Avalonia.Animation/Easing/QuinticEaseIn.cs
  57. 29
      src/Avalonia.Animation/Easing/QuinticEaseInOut.cs
  58. 20
      src/Avalonia.Animation/Easing/QuinticEaseOut.cs
  59. 21
      src/Avalonia.Animation/Easing/SineEaseIn.cs
  60. 20
      src/Avalonia.Animation/Easing/SineEaseInOut.cs
  61. 22
      src/Avalonia.Animation/Easing/SineEaseOut.cs
  62. 14
      src/Avalonia.Animation/FillMode.cs
  63. 23
      src/Avalonia.Animation/FloatTransition.cs
  64. 17
      src/Avalonia.Animation/IAnimation.cs
  65. 8
      src/Avalonia.Animation/IAnimationSetter.cs
  66. 22
      src/Avalonia.Animation/IAnimator.cs
  67. 13
      src/Avalonia.Animation/IEasing.cs
  68. 24
      src/Avalonia.Animation/IEasing`1.cs
  69. 26
      src/Avalonia.Animation/ITransition.cs
  70. 23
      src/Avalonia.Animation/IntegerTransition.cs
  71. 78
      src/Avalonia.Animation/KeyFrame.cs
  72. 41
      src/Avalonia.Animation/KeyFramePair`1.cs
  73. 41
      src/Avalonia.Animation/LinearDoubleEasing.cs
  74. 35
      src/Avalonia.Animation/LinearEasing.cs
  75. 27
      src/Avalonia.Animation/PlayState.cs
  76. 32
      src/Avalonia.Animation/PlaybackDirection.cs
  77. 8
      src/Avalonia.Animation/Properties/AssemblyInfo.cs
  78. 50
      src/Avalonia.Animation/PropertyTransition.cs
  79. 202
      src/Avalonia.Animation/RepeatCount.cs
  80. 23
      src/Avalonia.Animation/RepeatCountTypeConverter.cs
  81. 123
      src/Avalonia.Animation/Timing.cs
  82. 68
      src/Avalonia.Animation/Transition`1.cs
  83. 8
      src/Avalonia.Animation/Transitions.cs
  84. 38
      src/Avalonia.Animation/Utils/BounceEaseUtils.cs
  85. 18
      src/Avalonia.Animation/Utils/DoubleUtils.cs
  86. 18
      src/Avalonia.Animation/Utils/EasingUtils.cs
  87. 5
      src/Avalonia.Base/Logging/LogArea.cs
  88. 12
      src/Avalonia.Controls/Carousel.cs
  89. 19
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  90. 35
      src/Avalonia.Controls/ProgressBar.cs
  91. 12
      src/Avalonia.Controls/TabControl.cs
  92. 2
      src/Avalonia.Styling/Avalonia.Styling.csproj
  93. 25
      src/Avalonia.Styling/Styling/Style.cs
  94. 3
      src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs
  95. 2
      src/Avalonia.Themes.Default/Carousel.xaml
  96. 2
      src/Avalonia.Themes.Default/TabControl.xaml
  97. 19
      src/Avalonia.Visuals/Animation/CrossFade.cs
  98. 27
      src/Avalonia.Visuals/Animation/PageSlide.cs
  99. 31
      src/Avalonia.Visuals/Animation/PointTransition.cs
  100. 35
      src/Avalonia.Visuals/Animation/ThicknessTransition.cs

4
samples/ControlCatalog/MainView.xaml

@ -2,9 +2,9 @@
xmlns:pages="clr-namespace:ControlCatalog.Pages"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TabControl Classes="sidebar" Name="Sidebar">
<TabControl.Transition>
<TabControl.PageTransition>
<CrossFade Duration="0.25"/>
</TabControl.Transition>
</TabControl.PageTransition>
<TabItem Header="AutoCompleteBox"><pages:AutoCompleteBoxPage/></TabItem>
<TabItem Header="Border"><pages:BorderPage/></TabItem>
<TabItem Header="Button"><pages:ButtonPage/></TabItem>

4
samples/ControlCatalog/Pages/CarouselPage.xaml

@ -8,9 +8,9 @@
<Path Data="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" Fill="Black"/>
</Button>
<Carousel Name="carousel">
<Carousel.Transition>
<Carousel.PageTransition>
<PageSlide Duration="0.25" Orientation="Vertical" />
</Carousel.Transition>
</Carousel.PageTransition>
<Image Source="resm:ControlCatalog.Assets.delicate-arch-896885_640.jpg"/>
<Image Source="resm:ControlCatalog.Assets.hirsch-899118_640.jpg"/>
<Image Source="resm:ControlCatalog.Assets.maple-leaf-888807_640.jpg"/>

6
samples/ControlCatalog/Pages/CarouselPage.xaml.cs

@ -37,13 +37,13 @@ namespace ControlCatalog.Pages
switch (_transition.SelectedIndex)
{
case 0:
_carousel.Transition = null;
_carousel.PageTransition = null;
break;
case 1:
_carousel.Transition = new PageSlide(TimeSpan.FromSeconds(0.25), _orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical);
_carousel.PageTransition = new PageSlide(TimeSpan.FromSeconds(0.25), _orientation.SelectedIndex == 0 ? PageSlide.SlideAxis.Horizontal : PageSlide.SlideAxis.Vertical);
break;
case 2:
_carousel.Transition = new CrossFade(TimeSpan.FromSeconds(0.25));
_carousel.PageTransition = new CrossFade(TimeSpan.FromSeconds(0.25));
break;
}
}

16
samples/ControlCatalog/SideBar.xaml

@ -1,4 +1,5 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<Style Selector="TabControl.sidebar">
<Setter Property="Template">
<ControlTemplate>
@ -20,7 +21,7 @@
MemberSelector="{x:Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
Transition="{TemplateBinding Transition}"
PageTransition="{TemplateBinding PageTransition}"
Grid.Row="1"/>
</DockPanel>
</ControlTemplate>
@ -32,9 +33,20 @@
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Opacity" Value="0.5"/>
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.5"/>
</Transitions>
</Setter>
</Style>
<Style Selector="TabControl.sidebar TabStripItem:pointerover">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="TabControl.sidebar TabStripItem:selected">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/>
<Setter Property="Opacity" Value="1"/>
</Style>
</Styles>

13
samples/RenderTest/App.xaml.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia;
using Avalonia.Logging.Serilog;
using Avalonia.Markup.Xaml;
namespace RenderTest
@ -12,5 +13,17 @@ namespace RenderTest
{
AvaloniaXamlLoader.Load(this);
}
// TODO: Make this work with GTK/Skia/Cairo depending on command-line args
// again.
static void Main(string[] args) => BuildAvaloniaApp().Start<MainWindow>();
// App configuration, used by the entry point and previewer
static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.UseReactiveUI()
.LogToDebug();
}
}

4
samples/RenderTest/MainWindow.xaml

@ -24,9 +24,9 @@
</Menu>
<TabControl Classes="sidebar">
<TabControl.Transition>
<TabControl.PageTransition>
<CrossFade Duration="0.25"/>
</TabControl.Transition>
</TabControl.PageTransition>
<TabItem Header="Animations"><pages:AnimationsPage/></TabItem>
<TabItem Header="Clipping"><pages:ClippingPage/></TabItem>
<TabItem Header="Drawing"><pages:DrawingPage/></TabItem>

123
samples/RenderTest/Pages/AnimationsPage.xaml

@ -1,2 +1,123 @@
<UserControl xmlns="https://github.com/avaloniaui">
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Styles>
<Styles>
<Styles.Resources>
<Template x:Key="Acorn">
<Path Fill="White" Stretch="Uniform"
Data="F1 M 16.6309,18.6563C 17.1309,
8.15625 29.8809,14.1563 29.8809,
14.1563C 30.8809,11.1563 34.1308,
11.4063 34.1308,11.4063C 33.5,12
34.6309,13.1563 34.6309,13.1563C
32.1309,13.1562 31.1309,14.9062
31.1309,14.9062C 41.1309,23.9062
32.6309,27.9063 32.6309,27.9062C
24.6309,24.9063 21.1309,22.1562
16.6309,18.6563 Z M 16.6309,19.9063C
21.6309,24.1563 25.1309,26.1562
31.6309,28.6562C 31.6309,28.6562
26.3809,39.1562 18.3809,36.1563C
18.3809,36.1563 18,38 16.3809,36.9063C
15,36 16.3809,34.9063 16.3809,34.9063C
16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z"/>
</Template>
<Template x:Key="Heart">
<Path Fill="Red" Stretch="Uniform" Data="
M 272.70141,238.71731
C 206.46141,238.71731 152.70146,292.4773 152.70146,358.71731
C 152.70146,493.47282 288.63461,528.80461 381.26391,662.02535
C 468.83815,529.62199 609.82641,489.17075 609.82641,358.71731
C 609.82641,292.47731 556.06651,238.7173 489.82641,238.71731
C 441.77851,238.71731 400.42481,267.08774 381.26391,307.90481
C 362.10311,267.08773 320.74941,238.7173 272.70141,238.71731 z "/>
</Template>
</Styles.Resources>
<Style Selector="Border.Test">
<Setter Property="Margin" Value="15"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="100"/>
<Setter Property="Child" Value="{StaticResource Acorn}"/>
</Style>
<Style Selector="Border.Rect1:pointerover">
<Style.Animations>
<Animation Duration="0:0:2.5"
RepeatCount="4"
FillMode="None"
PlaybackDirection="AlternateReverse"
Easing="SineEaseInOut">
<KeyFrame Cue="20%">
<TransformSetter Property="RotateTransform.Angle" Value="45"/>
</KeyFrame>
<KeyFrame Cue="50%">
<TransformSetter Property="ScaleTransform.ScaleX" Value="1.5"/>
</KeyFrame>
<KeyFrame Cue="80%">
<TransformSetter Property="RotateTransform.Angle" Value="120"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Rect2:pointerover">
<Style.Animations>
<Animation Duration="0:0:0.5" Easing="SineEaseInOut">
<KeyFrame Cue="50%">
<TransformSetter Property="ScaleTransform.ScaleX" Value="0.8"/>
<TransformSetter Property="ScaleTransform.ScaleY" Value="0.8"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Rect3">
<Setter Property="Child" Value="{StaticResource Heart}"/>
<Style.Animations>
<Animation Duration="0:0:0.5"
Easing="QuadraticEaseInOut"
RepeatCount="Loop">
<KeyFrame Cue="50%">
<TransformSetter Property="ScaleTransform.ScaleX" Value="0.8"/>
<TransformSetter Property="ScaleTransform.ScaleY" Value="0.8"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Rect4:pointerover">
<Style.Animations>
<Animation Duration="0:0:3" Easing="BounceEaseInOut">
<KeyFrame Cue="48%">
<TransformSetter Property="TranslateTransform.Y" Value="-100"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Rect5:pointerover">
<Style.Animations>
<Animation Duration="0:0:3" Easing="CircularEaseInOut">
<KeyFrame Cue="25%">
<TransformSetter Property="SkewTransform.AngleX" Value="-20"/>
</KeyFrame>
<KeyFrame Cue="75%">
<TransformSetter Property="SkewTransform.AngleX" Value="20"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</UserControl.Styles>
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="False">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock>
<Button Content="{Binding PlayStateText}" Command="{Binding ToggleGlobalPlayState}"/>
</StackPanel>
<WrapPanel ClipToBounds="False">
<Border Classes="Test Rect1" Background="DarkRed"/>
<Border Classes="Test Rect2" Background="Magenta"/>
<Border Classes="Test Rect3"/>
<Border Classes="Test Rect4" Background="Navy"/>
<Border Classes="Test Rect5" Background="SeaGreen"/>
</WrapPanel>
</StackPanel>
</Grid>
</UserControl>

69
samples/RenderTest/Pages/AnimationsPage.xaml.cs

@ -7,6 +7,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using RenderTest.ViewModels;
namespace RenderTest.Pages
{
@ -14,77 +15,13 @@ namespace RenderTest.Pages
{
public AnimationsPage()
{
this.InitializeComponent();
this.CreateAnimations();
InitializeComponent();
this.DataContext = new AnimationsPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void CreateAnimations()
{
const int Count = 100;
var panel = new WrapPanel();
for (var i = 0; i < Count; ++i)
{
Ellipse ellipse;
var element = new Panel
{
Children =
{
(ellipse = new Ellipse
{
Name = $"ellipse{i}",
Width = 100,
Height = 100,
Fill = Brushes.Blue,
}),
new Path
{
Data = StreamGeometry.Parse(
"F1 M 16.6309,18.6563C 17.1309,8.15625 29.8809,14.1563 29.8809,14.1563C 30.8809,11.1563 34.1308,11.4063 34.1308,11.4063C 33.5,12 34.6309,13.1563 34.6309,13.1563C 32.1309,13.1562 31.1309,14.9062 31.1309,14.9062C 41.1309,23.9062 32.6309,27.9063 32.6309,27.9062C 24.6309,24.9063 21.1309,22.1562 16.6309,18.6563 Z M 16.6309,19.9063C 21.6309,24.1563 25.1309,26.1562 31.6309,28.6562C 31.6309,28.6562 26.3809,39.1562 18.3809,36.1563C 18.3809,36.1563 18,38 16.3809,36.9063C 15,36 16.3809,34.9063 16.3809,34.9063C 16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z"),
Fill = Brushes.Green,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
RenderTransform = new ScaleTransform(2, 2),
}
},
Margin = new Thickness(4),
RenderTransform = new ScaleTransform(),
};
var start = Animate.Stopwatch.Elapsed;
var index = i % (Count / 2);
var degrees = Animate.Timer
.Select(x => (x - start).TotalSeconds)
.Where(x => (x % Count) >= index && (x % Count) < index + 1)
.Select(x => (x % 1) / 1);
element.RenderTransform.Bind(
ScaleTransform.ScaleXProperty,
degrees,
BindingPriority.Animation);
ellipse.PointerEnter += Ellipse_PointerEnter;
ellipse.PointerLeave += Ellipse_PointerLeave;
panel.Children.Add(element);
}
Content = panel;
}
private void Ellipse_PointerEnter(object sender, PointerEventArgs e)
{
((Ellipse)sender).Fill = Brushes.Red;
}
private void Ellipse_PointerLeave(object sender, PointerEventArgs e)
{
((Ellipse)sender).Fill = Brushes.Blue;
}
}
}

45
samples/RenderTest/Pages/ClippingPage.xaml

@ -1,13 +1,52 @@
<UserControl xmlns="https://github.com/avaloniaui">
<UserControl
xmlns="https://github.com/avaloniaui">
<Grid ColumnDefinitions="Auto" RowDefinitions="Auto,Auto">
<Border Name="clipped"
<Grid.Styles>
<Styles>
<Style Selector="Border#clipped :pointerover">
<Setter Property="Border.Background" Value="Crimson"/>
</Style>
<Style Selector="Border#clipChild">
<Style.Animations>
<Animation Duration="0:0:2" RepeatCount="Loop">
<KeyFrame Cue="100%">
<TransformSetter Property="RotateTransform.Angle" Value="360"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</Grid.Styles>
<Border Name="clipped"
Background="Yellow"
Width="100"
Height="100"
Clip="M 58.625 0.07421875 C 50.305778 0.26687364 42.411858 7.0346526 41.806641 15.595703 C 42.446442 22.063923 39.707425 13.710754 36.982422 12.683594 C 29.348395 6.1821635 16.419398 8.4359222 11.480469 17.195312 C 6.0935256 25.476803 9.8118851 37.71125 18.8125 41.6875 C 9.1554771 40.62945 -0.070876925 49.146842 0.21679688 58.857422 C 0.21545578 60.872512 0.56758794 62.88911 1.2617188 64.78125 C 4.3821886 74.16708 16.298268 78.921772 25.03125 74.326172 C 28.266843 72.062552 26.298191 74.214838 25.414062 76.398438 C 21.407348 85.589198 27.295992 97.294293 37.097656 99.501953 C 46.864883 102.3541 57.82177 94.726518 58.539062 84.580078 C 58.142158 79.498998 59.307538 83.392694 61.207031 85.433594 C 67.532324 93.056874 80.440232 93.192029 86.882812 85.630859 C 93.836392 78.456939 92.396838 65.538666 84.115234 60.009766 C 79.783641 57.904836 83.569793 58.802369 86.375 58.193359 C 96.383335 56.457569 102.87506 44.824101 99.083984 35.394531 C 95.963498 26.008711 84.047451 21.254079 75.314453 25.849609 C 72.078834 28.113269 74.047517 25.960974 74.931641 23.777344 C 78.93827 14.586564 73.049722 2.8815081 63.248047 0.67382812 C 61.721916 0.22817968 60.165597 0.038541919 58.625 0.07421875 z ">
Clip="M 58.625 0.07421875
C 50.305778 0.26687364 42.411858 7.0346526 41.806641 15.595703
C 42.446442 22.063923 39.707425 13.710754 36.982422 12.683594
C 29.348395 6.1821635 16.419398 8.4359222 11.480469 17.195312
C 6.0935256 25.476803 9.8118851 37.71125 18.8125 41.6875
C 9.1554771 40.62945 -0.070876925 49.146842 0.21679688 58.857422
C 0.21545578 60.872512 0.56758794 62.88911 1.2617188 64.78125
C 4.3821886 74.16708 16.298268 78.921772 25.03125 74.326172
C 28.266843 72.062552 26.298191 74.214838 25.414062 76.398438
C 21.407348 85.589198 27.295992 97.294293 37.097656 99.501953
C 46.864883 102.3541 57.82177 94.726518 58.539062 84.580078
C 58.142158 79.498998 59.307538 83.392694 61.207031 85.433594
C 67.532324 93.056874 80.440232 93.192029 86.882812 85.630859
C 93.836392 78.456939 92.396838 65.538666 84.115234 60.009766
C 79.783641 57.904836 83.569793 58.802369 86.375 58.193359
C 96.383335 56.457569 102.87506 44.824101 99.083984 35.394531
C 95.963498 26.008711 84.047451 21.254079 75.314453 25.849609
C 72.078834 28.113269 74.047517 25.960974 74.931641 23.777344
C 78.93827 14.586564 73.049722 2.8815081 63.248047 0.67382812
C 61.721916 0.22817968 60.165597 0.038541919 58.625 0.07421875 z ">
<Border Name="clipChild" Background="{DynamicResource ThemeAccentBrush}" Margin="4">
<!-- Setting opacity puts the TextBox on a new layer -->
<TextBox Text="Avalonia" Opacity="0.9" VerticalAlignment="Center"/>
<Border.RenderTransform>
<RotateTransform/>
</Border.RenderTransform>
</Border>
</Border>
<CheckBox Name="useMask" IsChecked="True" Grid.Row="1">Apply Geometry Clip</CheckBox>

13
samples/RenderTest/Pages/ClippingPage.xaml.cs

@ -16,7 +16,6 @@ namespace RenderTest.Pages
public ClippingPage()
{
InitializeComponent();
CreateAnimations();
WireUpCheckbox();
}
@ -25,18 +24,6 @@ namespace RenderTest.Pages
AvaloniaXamlLoader.Load(this);
}
private void CreateAnimations()
{
var clipped = this.FindControl<Border>("clipChild");
var degrees = Animate.Timer.Select(x => x.TotalMilliseconds / 5);
clipped.RenderTransform = new RotateTransform();
clipped.RenderTransform.Bind(RotateTransform.AngleProperty, degrees, BindingPriority.Animation);
clipped.Bind(
Border.BackgroundProperty,
clipped.GetObservable(Control.IsPointerOverProperty)
.Select(x => x ? Brushes.Crimson : AvaloniaProperty.UnsetValue));
}
private void WireUpCheckbox()
{
var useMask = this.FindControl<CheckBox>("useMask");

24
samples/RenderTest/Program.cs

@ -1,24 +0,0 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Logging.Serilog;
using Avalonia.Platform;
using Serilog;
namespace RenderTest
{
internal class Program
{
static void Main(string[] args)
{
// TODO: Make this work with GTK/Skia/Cairo depending on command-line args
// again.
AppBuilder.Configure<App>()
.UsePlatformDetect()
.UseReactiveUI()
.LogToDebug()
.Start<MainWindow>();
}
}
}

17
samples/RenderTest/SideBar.xaml

@ -1,4 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:a="clr-namespace:Avalonia.Media.Animations;assembly=Avalonia.Media.Animations">
<Style Selector="TabControl.sidebar">
<Setter Property="Template">
<ControlTemplate>
@ -20,7 +22,7 @@
MemberSelector="{x:Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
Transition="{TemplateBinding Transition}"
PageTransition="{TemplateBinding PageTransition}"
Grid.Row="1"/>
</DockPanel>
</ControlTemplate>
@ -32,9 +34,20 @@
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Opacity" Value="0.5"/>
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.5"/>
</Transitions>
</Setter>
</Style>
<Style Selector="TabControl.sidebar TabStripItem:pointerover">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="TabControl.sidebar TabStripItem:selected">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/>
<Setter Property="Opacity" Value="1"/>
</Style>
</Styles>

40
samples/RenderTest/ViewModels/AnimationsPageViewModel.cs

@ -0,0 +1,40 @@
using System;
using ReactiveUI;
using Avalonia.Animation;
namespace RenderTest.ViewModels
{
public class AnimationsPageViewModel : ReactiveObject
{
private string _playStateText = "Pause all animations";
public AnimationsPageViewModel()
{
ToggleGlobalPlayState = ReactiveCommand.Create(()=>TogglePlayState());
}
void TogglePlayState()
{
switch (Timing.GetGlobalPlayState())
{
case PlayState.Run:
PlayStateText = "Resume all animations";
Timing.SetGlobalPlayState(PlayState.Pause);
break;
case PlayState.Pause:
PlayStateText = "Pause all animations";
Timing.SetGlobalPlayState(PlayState.Run);
break;
}
}
public string PlayStateText
{
get { return _playStateText; }
set { this.RaiseAndSetIfChanged(ref _playStateText, value); }
}
public ReactiveCommand ToggleGlobalPlayState { get; }
}
}

2
samples/RenderTest/ViewModels/MainWindowViewModel.cs

@ -5,7 +5,7 @@ namespace RenderTest.ViewModels
{
public class MainWindowViewModel : ReactiveObject
{
private bool drawDirtyRects = true;
private bool drawDirtyRects = false;
private bool drawFps = true;
public MainWindowViewModel()

92
src/Avalonia.Animation/Animatable.cs

@ -3,6 +3,13 @@
using System.Linq;
using Avalonia.Data;
using System;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Animation;
using System.Collections.Generic;
using System.Threading;
using System.Collections.Concurrent;
namespace Avalonia.Animation
{
@ -12,45 +19,90 @@ namespace Avalonia.Animation
public class Animatable : AvaloniaObject
{
/// <summary>
/// The property transitions for the control.
/// Initializes this <see cref="Animatable"/> object.
/// </summary>
private PropertyTransitions _propertyTransitions;
public Animatable()
{
Transitions = new Transitions();
AnimatableTimer = Timing.AnimationStateTimer
.Select(p =>
{
if (this._playState == PlayState.Pause)
{
return PlayState.Pause;
}
else return p;
})
.Publish()
.RefCount();
}
/// <summary>
/// Gets or sets the property transitions for the control.
/// The specific animations timer for this control.
/// </summary>
/// <value>
/// The property transitions for the control.
/// </value>
public PropertyTransitions PropertyTransitions
/// <returns></returns>
public IObservable<PlayState> AnimatableTimer;
/// <summary>
/// Defines the <see cref="PlayState"/> property.
/// </summary>
public static readonly DirectProperty<Animatable, PlayState> PlayStateProperty =
AvaloniaProperty.RegisterDirect<Animatable, PlayState>(
nameof(PlayState),
o => o.PlayState,
(o, v) => o.PlayState = v);
private PlayState _playState = PlayState.Run;
/// <summary>
/// Gets or sets the state of the animation for this
/// control.
/// </summary>
public PlayState PlayState
{
get
{
return _propertyTransitions ?? (_propertyTransitions = new PropertyTransitions());
}
get { return _playState; }
set { SetAndRaise(PlayStateProperty, ref _playState, value); }
set
{
_propertyTransitions = value;
}
}
/// <summary>
/// Reacts to a change in a <see cref="AvaloniaProperty"/> value in order to animate the
/// change if a <see cref="PropertyTransition"/> is set for the property..
/// Defines the <see cref="Transitions"/> property.
/// </summary>
public static readonly DirectProperty<Animatable, IEnumerable<ITransition>> TransitionsProperty =
AvaloniaProperty.RegisterDirect<Animatable, IEnumerable<ITransition>>(
nameof(Transitions),
o => o.Transitions,
(o, v) => o.Transitions = v);
private IEnumerable<ITransition> _transitions = new AvaloniaList<ITransition>();
/// <summary>
/// Gets or sets the property transitions for the control.
/// </summary>
public IEnumerable<ITransition> Transitions
{
get { return _transitions; }
set { SetAndRaise(TransitionsProperty, ref _transitions, value); }
}
/// <summary>
/// Reacts to a change in a <see cref="AvaloniaProperty"/> value in
/// order to animate the change if a <see cref="ITransition"/> is set for the property.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Priority != BindingPriority.Animation && _propertyTransitions != null)
if (e.Priority != BindingPriority.Animation && Transitions != null)
{
var match = _propertyTransitions.FirstOrDefault(x => x.Property == e.Property);
var match = Transitions.FirstOrDefault(x => x.Property == e.Property);
if (match != null)
{
Animate.Property(this, e.Property, e.OldValue, e.NewValue, match.Easing, match.Duration);
match.Apply(this, e.OldValue, e.NewValue);
}
}
}
}
}

134
src/Avalonia.Animation/Animate.cs

@ -1,134 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.Animation
{
/// <summary>
/// Utilities for creating animations.
/// </summary>
public static class Animate
{
/// <summary>
/// The number of frames per second.
/// </summary>
public const int FramesPerSecond = 60;
/// <summary>
/// The time span of each frame.
/// </summary>
private static readonly TimeSpan Tick = TimeSpan.FromSeconds(1.0 / FramesPerSecond);
/// <summary>
/// Initializes static members of the <see cref="Animate"/> class.
/// </summary>
static Animate()
{
Stopwatch = new Stopwatch();
Stopwatch.Start();
Timer = Observable.Interval(Tick, AvaloniaScheduler.Instance)
.Select(_ => Stopwatch.Elapsed)
.Publish()
.RefCount();
}
/// <summary>
/// The stopwatch used to track time.
/// </summary>
/// <value>
/// The stopwatch used to track time.
/// </value>
public static Stopwatch Stopwatch
{
get; }
/// <summary>
/// Gets the animation timer.
/// </summary>
/// <remarks>
/// The animation timer ticks <see cref="FramesPerSecond"/> times per second. The
/// parameter passed to a subsciber is the time span since the animation system was
/// initialized.
/// </remarks>
/// <value>
/// The animation timer.
/// </value>
public static IObservable<TimeSpan> Timer
{
get; }
/// <summary>
/// Gets a timer that fires every frame for the specified duration.
/// </summary>
/// <param name="duration">The duration of the animation.</param>
/// <returns>
/// An observable that notifies the subscriber of the progress along the animation.
/// </returns>
/// <remarks>
/// The parameter passed to the subscriber is the progress along the animation, with
/// 0 being the start and 1 being the end. The observable is guaranteed to fire 0
/// immediately on subscribe and 1 at the end of the duration.
/// </remarks>
public static IObservable<double> GetTimer(TimeSpan duration)
{
var startTime = Stopwatch.Elapsed.Ticks;
var endTime = startTime + duration.Ticks;
return Timer
.TakeWhile(x => x.Ticks < endTime)
.Select(x => (x.Ticks - startTime) / (double)duration.Ticks)
.StartWith(0.0)
.Concat(Observable.Return(1.0));
}
/// <summary>
/// Animates a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <param name="target">The target object.</param>
/// <param name="property">The target property.</param>
/// <param name="start">The value of the property at the start of the animation.</param>
/// <param name="finish">The value of the property at the end of the animation.</param>
/// <param name="easing">The easing function to use.</param>
/// <param name="duration">The duration of the animation.</param>
/// <returns>An <see cref="Animation"/> that can be used to track or stop the animation.</returns>
public static Animation Property(
IAvaloniaObject target,
AvaloniaProperty property,
object start,
object finish,
IEasing easing,
TimeSpan duration)
{
var o = GetTimer(duration).Select(progress => easing.Ease(progress, start, finish));
return new Animation(o, target.Bind(property, o, BindingPriority.Animation));
}
/// <summary>
/// Animates a <see cref="AvaloniaProperty"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="target">The target object.</param>
/// <param name="property">The target property.</param>
/// <param name="start">The value of the property at the start of the animation.</param>
/// <param name="finish">The value of the property at the end of the animation.</param>
/// <param name="easing">The easing function to use.</param>
/// <param name="duration">The duration of the animation.</param>
/// <returns>An <see cref="Animation"/> that can be used to track or stop the animation.</returns>
public static Animation<T> Property<T>(
IAvaloniaObject target,
AvaloniaProperty<T> property,
T start,
T finish,
IEasing<T> easing,
TimeSpan duration)
{
var o = GetTimer(duration).Select(progress => easing.Ease(progress, start, finish));
return new Animation<T>(o, target.Bind(property, o, BindingPriority.Animation));
}
}
}

136
src/Avalonia.Animation/Animation.cs

@ -1,34 +1,118 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Animation.Easings;
using Avalonia.Animation;
using Avalonia.Collections;
using Avalonia.Metadata;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reflection;
using System.Linq;
namespace Avalonia.Animation
{
/// <summary>
/// Tracks the progress of an animation.
/// </summary>
public class Animation : IObservable<object>, IDisposable
public class Animation : AvaloniaList<KeyFrame>, IDisposable, IAnimation
{
private bool _isChildrenChanged = false;
private List<IDisposable> _subscription = new List<IDisposable>();
public AvaloniaList<IAnimator> _animators { get; set; } = new AvaloniaList<IAnimator>();
/// <summary>
/// The animation being tracked.
/// Run time of this animation.
/// </summary>
private readonly IObservable<object> _inner;
public TimeSpan Duration { get; set; }
/// <summary>
/// The disposable used to cancel the animation.
/// Delay time for this animation.
/// </summary>
private readonly IDisposable _subscription;
public TimeSpan Delay { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Animation"/> class.
/// The repeat count for this animation.
/// </summary>
/// <param name="inner">The animation observable being tracked.</param>
/// <param name="subscription">A disposable used to cancel the animation.</param>
public Animation(IObservable<object> inner, IDisposable subscription)
public RepeatCount RepeatCount { get; set; }
/// <summary>
/// The playback direction for this animation.
/// </summary>
public PlaybackDirection PlaybackDirection { get; set; }
/// <summary>
/// The value fill mode for this animation.
/// </summary>
public FillMode FillMode { get; set; }
/// <summary>
/// Easing function to be used.
/// </summary>
public Easing Easing { get; set; } = new LinearEasing();
public Animation()
{
this.CollectionChanged += delegate { _isChildrenChanged = true; };
}
private void InterpretKeyframes()
{
_inner = inner;
_subscription = subscription;
var handlerList = new List<(Type, AvaloniaProperty)>();
var kfList = new List<AnimatorKeyFrame>();
foreach (var keyframe in this)
{
foreach (var setter in keyframe)
{
var custAttr = setter.GetType()
.GetCustomAttributes()
.Where(p => p.GetType() == typeof(AnimatorAttribute));
if (!custAttr.Any())
throw new InvalidProgramException($"Type {setter.GetType()} doesn't have Animator attribute.");
var handler = ((AnimatorAttribute)custAttr.First()).HandlerType;
if (!handlerList.Contains((handler, setter.Property)))
handlerList.Add((handler, setter.Property));
var newKF = new AnimatorKeyFrame()
{
Handler = handler,
Property = setter.Property,
Cue = keyframe.Cue,
KeyTime = keyframe.KeyTime,
timeSpanSet = keyframe.timeSpanSet,
cueSet = keyframe.cueSet,
Value = setter.Value
};
kfList.Add(newKF);
}
}
var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>();
foreach (var handler in handlerList)
{
var newInstance = (IAnimator)Activator.CreateInstance(handler.Item1);
newInstance.Property = handler.Item2;
newAnimatorInstances.Add((handler.Item1, handler.Item2, newInstance));
}
foreach (var kf in kfList)
{
var parent = newAnimatorInstances.Where(p => p.handler == kf.Handler &&
p.prop == kf.Property)
.First();
parent.inst.Add(kf);
}
foreach(var instance in newAnimatorInstances)
_animators.Add(instance.inst);
}
/// <summary>
@ -36,20 +120,26 @@ namespace Avalonia.Animation
/// </summary>
public void Dispose()
{
_subscription.Dispose();
foreach (var sub in _subscription)
{
sub.Dispose();
}
}
/// <summary>
/// Notifies the provider that an observer is to receive notifications.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>
/// A reference to an interface that allows observers to stop receiving notifications
/// before the provider has finished sending them.
/// </returns>
public IDisposable Subscribe(IObserver<object> observer)
/// <inheritdocs/>
public IDisposable Apply(Animatable control, IObservable<bool> matchObs)
{
return _inner.Subscribe(observer);
if (_isChildrenChanged)
{
InterpretKeyframes();
_isChildrenChanged = false;
}
foreach (IAnimator keyframes in _animators)
{
_subscription.Add(keyframes.Apply(this, control, matchObs));
}
return this;
}
}
}
}

45
src/Avalonia.Animation/AnimationExtensions.cs

@ -1,45 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation
{
/// <summary>
/// Defines animation extension methods.
/// </summary>
public static class AnimationExtensions
{
/// <summary>
/// Returns a new <see cref="PropertyTransition"/> for the specified
/// <see cref="AvaloniaProperty"/> using linear easing.
/// </summary>
/// <typeparam name="T">The type of the <see cref="AvaloniaProperty"/>.</typeparam>
/// <param name="property">The property to animate.</param>
/// <param name="milliseconds">The animation duration in milliseconds.</param>
/// <returns>
/// A <see cref="PropertyTransition"/> that can be added to the
/// <see cref="Animatable.PropertyTransitions"/> collection.
/// </returns>
public static PropertyTransition Transition<T>(this AvaloniaProperty<T> property, int milliseconds)
{
return Transition(property, TimeSpan.FromMilliseconds(milliseconds));
}
/// <summary>
/// Returns a new <see cref="PropertyTransition"/> for the specified
/// <see cref="AvaloniaProperty"/> using linear easing.
/// </summary>
/// <typeparam name="T">The type of the <see cref="AvaloniaProperty"/>.</typeparam>
/// <param name="property">The property to animate.</param>
/// <param name="duration">The animation duration.</param>
/// <returns>
/// A <see cref="PropertyTransition"/> that can be added to the
/// <see cref="Animatable.PropertyTransitions"/> collection.
/// </returns>
public static PropertyTransition Transition<T>(this AvaloniaProperty<T> property, TimeSpan duration)
{
return new PropertyTransition(property, duration, LinearEasing.For<T>());
}
}
}

17
src/Avalonia.Animation/AnimationSetter.cs

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reactive.Linq;
using System.Diagnostics;
using Avalonia.Animation.Utils;
using Avalonia.Data;
namespace Avalonia.Animation
{
public abstract class AnimationSetter : IAnimationSetter
{
public AvaloniaProperty Property { get; set; }
public object Value { get; set; }
}
}

56
src/Avalonia.Animation/Animation`1.cs

@ -1,56 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation
{
/// <summary>
/// Tracks the progress of an animation.
/// </summary>
/// <typeparam name="T">The type of the value being animated./</typeparam>
public class Animation<T> : IObservable<T>, IDisposable
{
/// <summary>
/// The animation being tracked.
/// </summary>
private readonly IObservable<T> _inner;
/// <summary>
/// The disposable used to cancel the animation.
/// </summary>
private readonly IDisposable _subscription;
/// <summary>
/// Initializes a new instance of the <see cref="Animation{T}"/> class.
/// </summary>
/// <param name="inner">The animation observable being tracked.</param>
/// <param name="subscription">A disposable used to cancel the animation.</param>
public Animation(IObservable<T> inner, IDisposable subscription)
{
_inner = inner;
_subscription = subscription;
}
/// <summary>
/// Cancels the animation.
/// </summary>
public void Dispose()
{
_subscription.Dispose();
}
/// <summary>
/// Notifies the provider that an observer is to receive notifications.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>
/// A reference to an interface that allows observers to stop receiving notifications
/// before the provider has finished sending them.
/// </returns>
public IDisposable Subscribe(IObserver<T> observer)
{
return _inner.Subscribe(observer);
}
}
}

18
src/Avalonia.Animation/AnimatorAttribute.cs

@ -0,0 +1,18 @@
using System;
namespace Avalonia.Animation
{
/// <summary>
/// Attribute for <see cref="IAnimationSetter"/> objects
/// that maps the setter to it's <see cref="Animator{T}"/>.
/// </summary>
public class AnimatorAttribute : Attribute
{
public Type HandlerType;
public AnimatorAttribute(Type handler)
{
this.HandlerType = handler;
}
}
}

23
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using Avalonia.Metadata;
using Avalonia.Collections;
namespace Avalonia.Animation
{
/// <summary>
/// Defines a KeyFrame that is used for
/// <see cref="Animators"/> objects.
/// </summary>
public class AnimatorKeyFrame
{
public Type Handler;
public Cue Cue;
public TimeSpan KeyTime;
internal bool timeSpanSet, cueSet;
public AvaloniaProperty Property;
public object Value;
}
}

256
src/Avalonia.Animation/AnimatorStateMachine`1.cs

@ -0,0 +1,256 @@
using System;
using System.Linq;
using Avalonia.Data;
namespace Avalonia.Animation
{
/// <summary>
/// Provides statefulness for an iteration of a keyframe animation.
/// </summary>
internal class AnimatorStateMachine<T> : IObservable<object>, IDisposable
{
object _lastInterpValue;
object _firstKFValue;
private ulong _delayTotalFrameCount;
private ulong _durationTotalFrameCount;
private ulong _delayFrameCount;
private ulong _durationFrameCount;
private ulong _repeatCount;
private ulong _currentIteration;
private bool _isLooping;
private bool _isRepeating;
private bool _isReversed;
private bool _checkLoopAndRepeat;
private bool _gotFirstKFValue;
private FillMode _fillMode;
private PlaybackDirection _animationDirection;
private KeyFramesStates _currentState;
private KeyFramesStates _savedState;
private Animator<T> _parent;
private Animation _targetAnimation;
private Animatable _targetControl;
private T _neutralValue;
internal bool _unsubscribe = false;
private IObserver<object> _targetObserver;
[Flags]
private enum KeyFramesStates
{
Initialize,
DoDelay,
DoRun,
RunForwards,
RunBackwards,
RunApplyValue,
RunComplete,
Pause,
Stop,
Disposed
}
public void Initialize(Animation animation, Animatable control, Animator<T> keyframes)
{
_parent = keyframes;
_targetAnimation = animation;
_targetControl = control;
_neutralValue = (T)_targetControl.GetValue(_parent.Property);
_delayTotalFrameCount = (ulong)(animation.Delay.Ticks / Timing.FrameTick.Ticks);
_durationTotalFrameCount = (ulong)(animation.Duration.Ticks / Timing.FrameTick.Ticks);
switch (animation.RepeatCount.RepeatType)
{
case RepeatType.Loop:
_isLooping = true;
_checkLoopAndRepeat = true;
break;
case RepeatType.Repeat:
_isRepeating = true;
_checkLoopAndRepeat = true;
_repeatCount = animation.RepeatCount.Value;
break;
}
_isReversed = (animation.PlaybackDirection & PlaybackDirection.Reverse) != 0;
_animationDirection = _targetAnimation.PlaybackDirection;
_fillMode = _targetAnimation.FillMode;
if (_durationTotalFrameCount > 0)
_currentState = KeyFramesStates.DoDelay;
else
_currentState = KeyFramesStates.DoRun;
}
public void Step(PlayState _playState, Func<double, T, T> Interpolator)
{
try
{
InternalStep(_playState, Interpolator);
}
catch (Exception e)
{
_targetObserver?.OnError(e);
}
}
private void InternalStep(PlayState _playState, Func<double, T, T> Interpolator)
{
if (!_gotFirstKFValue)
{
_firstKFValue = _parent.First().Value;
_gotFirstKFValue = true;
}
if (_currentState == KeyFramesStates.Disposed)
throw new InvalidProgramException("This KeyFrames Animation is already disposed.");
if (_playState == PlayState.Stop)
_currentState = KeyFramesStates.Stop;
// Save state and pause the machine
if (_playState == PlayState.Pause && _currentState != KeyFramesStates.Pause)
{
_savedState = _currentState;
_currentState = KeyFramesStates.Pause;
}
// Resume the previous state
if (_playState != PlayState.Pause && _currentState == KeyFramesStates.Pause)
_currentState = _savedState;
double _tempDuration = 0d, _easedTime;
checkstate:
switch (_currentState)
{
case KeyFramesStates.DoDelay:
if (_fillMode == FillMode.Backward
|| _fillMode == FillMode.Both)
{
if (_currentIteration == 0)
{
_targetObserver.OnNext(_firstKFValue);
}
else
{
_targetObserver.OnNext(_lastInterpValue);
}
}
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
goto checkstate;
}
_delayFrameCount++;
break;
case KeyFramesStates.DoRun:
if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
goto checkstate;
case KeyFramesStates.RunForwards:
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
goto checkstate;
case KeyFramesStates.RunBackwards:
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
goto checkstate;
case KeyFramesStates.RunApplyValue:
_easedTime = _targetAnimation.Easing.Ease(_tempDuration);
_durationFrameCount++;
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
break;
case KeyFramesStates.RunComplete:
if (_checkLoopAndRepeat)
{
_delayFrameCount = 0;
_durationFrameCount = 0;
if (_isLooping)
{
_currentState = KeyFramesStates.DoRun;
}
else if (_isRepeating)
{
if (_currentIteration >= _repeatCount)
{
_currentState = KeyFramesStates.Stop;
}
else
{
_currentState = KeyFramesStates.DoRun;
}
_currentIteration++;
}
if (_animationDirection == PlaybackDirection.Alternate
|| _animationDirection == PlaybackDirection.AlternateReverse)
_isReversed = !_isReversed;
break;
}
_currentState = KeyFramesStates.Stop;
goto checkstate;
case KeyFramesStates.Stop:
if (_fillMode == FillMode.Forward
|| _fillMode == FillMode.Both)
{
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
}
_targetObserver.OnCompleted();
break;
}
}
public IDisposable Subscribe(IObserver<object> observer)
{
_targetObserver = observer;
return this;
}
public void Dispose()
{
_unsubscribe = true;
_currentState = KeyFramesStates.Disposed;
}
}
}

192
src/Avalonia.Animation/Animator`1.cs

@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Collections;
using System.ComponentModel;
using Avalonia.Animation.Utils;
using System.Reactive.Linq;
using System.Linq;
using Avalonia.Data;
using System.Reactive.Disposables;
namespace Avalonia.Animation
{
/// <summary>
/// Base class for KeyFrames objects
/// </summary>
public abstract class Animator<T> : AvaloniaList<AnimatorKeyFrame>, IAnimator
{
/// <summary>
/// List of type-converted keyframes.
/// </summary>
private Dictionary<double, (T, bool isNeutral)> _convertedKeyframes = new Dictionary<double, (T, bool)>();
private bool _isVerfifiedAndConverted;
/// <summary>
/// Gets or sets the target property for the keyframe.
/// </summary>
public AvaloniaProperty Property { get; set; }
public Animator()
{
// Invalidate keyframes when changed.
this.CollectionChanged += delegate { _isVerfifiedAndConverted = false; };
}
/// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
{
if (!_isVerfifiedAndConverted)
VerifyConvertKeyFrames(animation, typeof(T));
return obsMatch
.Where(p => p == true)
// Ignore triggers when global timers are paused.
.Where(p => Timing.GetGlobalPlayState() != PlayState.Pause)
.Subscribe(_ =>
{
var timerObs = RunKeyFrames(animation, control);
});
}
/// <summary>
/// Get the nearest pair of cue-time ordered keyframes
/// according to the given time parameter that is relative to the
/// total animation time and the normalized intra-keyframe pair time
/// (i.e., the normalized time between the selected keyframes, relative to the
/// time parameter).
/// </summary>
/// <param name="t">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
{
KeyValuePair<double, (T, bool)> firstCue, lastCue;
int kvCount = _convertedKeyframes.Count();
if (kvCount > 2)
{
if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
{
firstCue = _convertedKeyframes.First();
lastCue = _convertedKeyframes.Skip(1).First();
}
else if (DoubleUtils.AboutEqual(t, 1.0) || t > 1.0)
{
firstCue = _convertedKeyframes.Skip(kvCount - 2).First();
lastCue = _convertedKeyframes.Last();
}
else
{
firstCue = _convertedKeyframes.Where(j => j.Key <= t).Last();
lastCue = _convertedKeyframes.Where(j => j.Key >= t).First();
}
}
else
{
firstCue = _convertedKeyframes.First();
lastCue = _convertedKeyframes.Last();
}
double t0 = firstCue.Key;
double t1 = lastCue.Key;
var intraframeTime = (t - t0) / (t1 - t0);
return (intraframeTime, new KeyFramePair<T>(firstCue, lastCue));
}
/// <summary>
/// Runs the KeyFrames Animation.
/// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control)
{
var _kfStateMach = new AnimatorStateMachine<T>();
_kfStateMach.Initialize(animation, control, this);
Timing.AnimationStateTimer
.TakeWhile(_ => !_kfStateMach._unsubscribe)
.Subscribe(p =>
{
_kfStateMach.Step(p, DoInterpolation);
});
return control.Bind(Property, _kfStateMach, BindingPriority.Animation);
}
/// <summary>
/// Interpolates a value given the desired time.
/// </summary>
protected abstract T DoInterpolation(double time, T neutralValue);
/// <summary>
/// Verifies and converts keyframe values according to this class's target type.
/// </summary>
private void VerifyConvertKeyFrames(Animation animation, Type type)
{
var typeConv = TypeDescriptor.GetConverter(type);
foreach (AnimatorKeyFrame k in this)
{
if (k.Value == null)
{
throw new ArgumentNullException($"KeyFrame value can't be null.");
}
if (!typeConv.CanConvertTo(k.Value.GetType()))
{
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
T convertedValue = (T)typeConv.ConvertTo(k.Value, type);
Cue _normalizedCue = k.Cue;
if (k.timeSpanSet)
{
_normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks);
}
_convertedKeyframes.Add(_normalizedCue.CueValue, (convertedValue, false));
}
SortKeyFrameCues(_convertedKeyframes);
_isVerfifiedAndConverted = true;
}
private void SortKeyFrameCues(Dictionary<double, (T, bool)> convertedValues)
{
bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false;
// Make start and end keyframe mandatory.
foreach (var converted in _convertedKeyframes.Keys)
{
if (DoubleUtils.AboutEqual(converted, 0.0))
{
hasStartKey = true;
}
else if (DoubleUtils.AboutEqual(converted, 1.0))
{
hasEndKey = true;
}
}
if (!hasStartKey || !hasEndKey)
AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes);
_convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key)
.ToDictionary((k) => k.Key, (v) => v.Value);
}
private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary<double, (T, bool)> convertedKeyframes)
{
if (!hasStartKey)
{
convertedKeyframes.Add(0.0d, (default(T), true));
}
if (!hasEndKey)
{
convertedKeyframes.Add(1.0d, (default(T), true));
}
}
}
}

88
src/Avalonia.Animation/Cue.cs

@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Text;
namespace Avalonia.Animation
{
/// <summary>
/// A Cue object for <see cref="KeyFrame"/>.
/// </summary>
[TypeConverter(typeof(CueTypeConverter))]
public struct Cue : IEquatable<Cue>, IEquatable<double>
{
/// <summary>
/// The normalized percent value, ranging from 0.0 to 1.0
/// </summary>
public double CueValue { get; }
/// <summary>
/// Sets a new <see cref="Cue"/> object.
/// </summary>
/// <param name="value"></param>
public Cue(double value)
{
if (value <= 1 && value >= 0)
CueValue = value;
else
throw new ArgumentException($"This cue object's value should be within or equal to 0.0 and 1.0");
}
/// <summary>
/// Parses a string to a <see cref="Cue"/> object.
/// </summary>
public static object Parse(string value, CultureInfo culture)
{
string v = value;
if (value.EndsWith("%"))
{
v = v.TrimEnd('%');
}
if (double.TryParse(v, NumberStyles.Float, culture, out double res))
{
return new Cue(res / 100d);
}
else
{
throw new FormatException($"Invalid Cue string \"{value}\"");
}
}
/// <summary>
/// Checks for equality between two <see cref="Cue"/>s.
/// </summary>
/// <param name="other">The second cue.</param>
public bool Equals(Cue other)
{
return CueValue == other.CueValue;
}
/// <summary>
/// Checks for equality between a <see cref="Cue"/>
/// and a <see cref="double"/> value.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool Equals(double other)
{
return CueValue == other;
}
}
public class CueTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return Cue.Parse((string)value, culture);
}
}
}

41
src/Avalonia.Animation/DoubleAnimator.cs

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reactive.Linq;
using System.Diagnostics;
using Avalonia.Animation.Utils;
using Avalonia.Data;
namespace Avalonia.Animation
{
/// <summary>
/// Animator that handles <see cref="double"/> properties.
/// </summary>
public class DoubleAnimator : Animator<double>
{
/// <inheritdocs/>
protected override double DoInterpolation(double t, double neutralValue)
{
var pair = GetKFPairAndIntraKFTime(t);
double y0, y1;
var firstKF = pair.KFPair.FirstKeyFrame;
var secondKF = pair.KFPair.SecondKeyFrame;
if (firstKF.Value.isNeutral)
y0 = neutralValue;
else
y0 = firstKF.Value.TargetValue;
if (secondKF.Value.isNeutral)
y1 = neutralValue;
else
y1 = secondKF.Value.TargetValue;
// Do linear parametric interpolation
return y0 + (pair.IntraKFTime) * (y1 - y0);
}
}
}

21
src/Avalonia.Animation/DoubleSetter.cs

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Reactive.Linq;
using System.Diagnostics;
using Avalonia.Animation.Utils;
using Avalonia.Data;
namespace Avalonia.Animation
{
/// <summary>
/// Setter that handles <see cref="double"/> properties
/// in the target.
/// </summary>
[Animator(typeof(DoubleAnimator))]
public class DoubleSetter : AnimationSetter
{
}
}

23
src/Avalonia.Animation/DoubleTransition.cs

@ -0,0 +1,23 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Metadata;
using System;
using System.Reactive.Linq;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="double"/> types.
/// </summary>
public class DoubleTransition : Transition<double>
{
/// <inheritdocs/>
public override IObservable<double> DoTransition(IObservable<double> progress, double oldValue, double newValue)
{
var delta = newValue - oldValue;
return progress
.Select(p => Easing.Ease(p) * delta + oldValue);
}
}
}

20
src/Avalonia.Animation/Easing/BackEaseIn.cs

@ -0,0 +1,20 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases in a <see cref="double"/> value
/// using a overshooting cubic function.
/// </summary>
public class BackEaseIn : Easing
{
/// <inheritdoc/>
public override double Ease(double p)
{
return p * (p * p - Math.Sin(p * Math.PI));
}
}
}

31
src/Avalonia.Animation/Easing/BackEaseInOut.cs

@ -0,0 +1,31 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a piecewise overshooting cubic function.
/// </summary>
public class BackEaseInOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
if (p < 0.5d)
{
double f = 2d * p;
return 0.5d * f * (f * f - Math.Sin(f * Math.PI));
}
else
{
double f = (1d - (2d * p - 1d));
return 0.5d * (1d - f * (f * f - Math.Sin(f * Math.PI))) + 0.5d;
}
}
}
}

21
src/Avalonia.Animation/Easing/BackEaseOut.cs

@ -0,0 +1,21 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases out a <see cref="double"/> value
/// using a overshooting cubic function.
/// </summary>
public class BackEaseOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = 1d - progress;
return 1 - p * (p * p - Math.Sin(p * Math.PI));
}
}
}

21
src/Avalonia.Animation/Easing/BounceEaseIn.cs

@ -0,0 +1,21 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Animation.Utils;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases in a <see cref="double"/> value
/// using a simulated bounce function.
/// </summary>
public class BounceEaseIn : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
return 1 - BounceEaseUtils.Bounce(1 - progress);
}
}
}

28
src/Avalonia.Animation/Easing/BounceEaseInOut.cs

@ -0,0 +1,28 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Animation.Utils;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a piecewise simulated bounce function.
/// </summary>
public class BounceEaseInOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
if (p < 0.5d)
{
return 0.5f * (1 - BounceEaseUtils.Bounce(1 - (p * 2)));
}
else
{
return 0.5f * BounceEaseUtils.Bounce(p * 2 - 1) + 0.5f;
}
}
}
}

19
src/Avalonia.Animation/Easing/BounceEaseOut.cs

@ -0,0 +1,19 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Animation.Utils;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases out a <see cref="double"/> value
/// using a simulated bounce function.
/// </summary>
public class BounceEaseOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
return BounceEaseUtils.Bounce(progress);
}
}
}

21
src/Avalonia.Animation/Easing/CircularEaseIn.cs

@ -0,0 +1,21 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases in a <see cref="double"/> value
/// using the shifted fourth quadrant of
/// the unit circle.
/// </summary>
public class CircularEaseIn : Easing
{
/// <inheritdoc/>
public override double Ease(double p)
{
return 1d - Math.Sqrt(1d - p * p);
}
}
}

29
src/Avalonia.Animation/Easing/CircularEaseInOut.cs

@ -0,0 +1,29 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a piecewise unit circle function.
/// </summary>
public class CircularEaseInOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
if (p < 0.5d)
{
return 0.5d * (1d - Math.Sqrt(1d - 4d * p * p));
}
else
{
double t = 2d * p;
return 0.5d * (Math.Sqrt((3d - t) * (t - 1d)) + 1d);
}
}
}
}

22
src/Avalonia.Animation/Easing/CircularEaseOut.cs

@ -0,0 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases out a <see cref="double"/> value
/// using the shifted second quadrant of
/// the unit circle.
/// </summary>
public class CircularEaseOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
return Math.Sqrt((2d - p) * p);
}
}
}

18
src/Avalonia.Animation/Easing/CubicEaseIn.cs

@ -0,0 +1,18 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases in a <see cref="double"/> value
/// using a cubic equation.
/// </summary>
public class CubicEaseIn : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
return progress * progress * progress;
}
}
}

28
src/Avalonia.Animation/Easing/CubicEaseInOut.cs

@ -0,0 +1,28 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a piece-wise cubic equation.
/// </summary>
public class CubicEaseInOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
if (progress < 0.5d)
{
return 4d * p * p * p;
}
else
{
double f = 2d * (p - 1);
return 0.5d * f * f * f + 1d;
}
}
}
}

19
src/Avalonia.Animation/Easing/CubicEaseOut.cs

@ -0,0 +1,19 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases out a <see cref="double"/> value
/// using a cubic equation.
/// </summary>
public class CubicEaseOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double f = (progress - 1d);
return f * f * f + 1d;
}
}
}

56
src/Avalonia.Animation/Easing/Easing.cs

@ -0,0 +1,56 @@
using Avalonia.Collections;
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Linq;
using System.ComponentModel;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Base class for all Easing classes.
/// </summary>
[TypeConverter(typeof(EasingTypeConverter))]
public abstract class Easing : IEasing
{
/// <inheritdoc/>
public abstract double Ease(double progress);
static Dictionary<string, Type> _easingTypes;
static readonly Type s_thisType = typeof(Easing);
/// <summary>
/// Parses a Easing type string.
/// </summary>
/// <param name="e">The Easing type string.</param>
/// <returns>Returns the instance of the parsed type.</returns>
public static Easing Parse(string e)
{
if (_easingTypes == null)
{
_easingTypes = new Dictionary<string, Type>();
// Fetch the built-in easings.
var derivedTypes = typeof(Easing).Assembly.GetTypes()
.Where(p => p.Namespace == s_thisType.Namespace)
.Where(p => p.IsSubclassOf(s_thisType))
.Select(p => p).ToList();
foreach (var easingType in derivedTypes)
_easingTypes.Add(easingType.Name, easingType);
}
if (_easingTypes.ContainsKey(e))
{
var type = _easingTypes[e];
return (Easing)Activator.CreateInstance(type);
}
else
{
throw new FormatException($"Easing \"{e}\" was not found in {s_thisType.Namespace} namespace.");
}
}
}
}

23
src/Avalonia.Animation/Easing/EasingTypeConverter.cs

@ -0,0 +1,23 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Animation.Easings;
using System;
using System.ComponentModel;
using System.Globalization;
namespace Avalonia.Animation.Easings
{
public class EasingTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return Easing.Parse((string)value);
}
}
}

22
src/Avalonia.Animation/Easing/ElasticEaseIn.cs

@ -0,0 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Animation.Utils;
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases in a <see cref="double"/> value
/// using a damped sine function.
/// </summary>
public class ElasticEaseIn : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
return Math.Sin(13d * EasingUtils.HALFPI * p) * Math.Pow(2d, 10d * (p - 1));
}
}
}

31
src/Avalonia.Animation/Easing/ElasticEaseInOut.cs

@ -0,0 +1,31 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Animation.Utils;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a piecewise damped sine function.
/// </summary>
public class ElasticEaseInOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
if (p < 0.5d)
{
double t = 2d * p;
return 0.5d * Math.Sin(13d * EasingUtils.HALFPI * t) * Math.Pow(2d, 10d * (t - 1d));
}
else
{
return 0.5d * (Math.Sin(-13d * EasingUtils.HALFPI * ((2d * p - 1d) + 1d)) * Math.Pow(2d, -10d * (2d * p - 1d)) + 2d);
}
}
}
}

22
src/Avalonia.Animation/Easing/ElasticEaseOut.cs

@ -0,0 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Animation.Utils;
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases out a <see cref="double"/> value
/// using a damped sine function.
/// </summary>
public class ElasticEaseOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
return Math.Sin(-13d * EasingUtils.HALFPI * (p + 1)) * Math.Pow(2d, -10d * p) + 1d;
}
}
}

21
src/Avalonia.Animation/Easing/ExponentialEaseIn.cs

@ -0,0 +1,21 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases in a <see cref="double"/> value
/// using a exponential function.
/// </summary>
public class ExponentialEaseIn : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
return (p == 0.0d) ? p : Math.Pow(2d, 10d * (p - 1d));
}
}
}

29
src/Avalonia.Animation/Easing/ExponentialEaseInOut.cs

@ -0,0 +1,29 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a piecewise exponential function.
/// </summary>
public class ExponentialEaseInOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
if (p < 0.5d)
{
return 0.5d * Math.Pow(2d, 20d * p - 10d);
}
else
{
return -0.5d * Math.Pow(2d, -20d * p + 10d) + 1d;
}
}
}
}

21
src/Avalonia.Animation/Easing/ExponentialEaseOut.cs

@ -0,0 +1,21 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases out a <see cref="double"/> value
/// using a exponential function.
/// </summary>
public class ExponentialEaseOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
return (p == 1.0d) ? p : 1d - Math.Pow(2d, -10d * p);
}
}
}

17
src/Avalonia.Animation/Easing/LinearEasing.cs

@ -0,0 +1,17 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Linearly eases a <see cref="double"/> value.
/// </summary>
public class LinearEasing : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
return progress;
}
}
}

18
src/Avalonia.Animation/Easing/QuadraticEaseIn.cs

@ -0,0 +1,18 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases in a <see cref="double"/> value
/// using a quadratic function.
/// </summary>
public class QuadraticEaseIn : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
return progress * progress;
}
}
}

27
src/Avalonia.Animation/Easing/QuadraticEaseInOut.cs

@ -0,0 +1,27 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a piece-wise quadratic function.
/// </summary>
public class QuadraticEaseInOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
if (progress < 0.5d)
{
return 2d * p * p;
}
else
{
return p * (-2d * p + 4d) - 1d;
}
}
}
}

18
src/Avalonia.Animation/Easing/QuadraticEaseOut.cs

@ -0,0 +1,18 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases out a <see cref="double"/> value
/// using a quadratic function.
/// </summary>
public class QuadraticEaseOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
return -(progress * (progress - 2d));
}
}
}

19
src/Avalonia.Animation/Easing/QuarticEaseIn.cs

@ -0,0 +1,19 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases in a <see cref="double"/> value
/// using a quartic equation.
/// </summary>
public class QuarticEaseIn : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p2 = progress * progress;
return p2 * p2;
}
}
}

30
src/Avalonia.Animation/Easing/QuarticEaseInOut.cs

@ -0,0 +1,30 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a piece-wise quartic equation.
/// </summary>
public class QuarticEaseInOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
if (p < 0.5d)
{
double p2 = p * p;
return 8d * p2 * p2;
}
else
{
double f = p - 1d;
double f2 = f * f;
return -8d * f2 * f2 + 1d;
}
}
}
}

20
src/Avalonia.Animation/Easing/QuarticEaseOut.cs

@ -0,0 +1,20 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases out a <see cref="double"/> value
/// using a quartic equation.
/// </summary>
public class QuarticEaseOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double f = progress - 1d;
double f2 = f * f;
return -f2 * f2 + 1d;
}
}
}

19
src/Avalonia.Animation/Easing/QuinticEaseIn.cs

@ -0,0 +1,19 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases in a <see cref="double"/> value
/// using a quartic equation.
/// </summary>
public class QuinticEaseIn : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p2 = progress * progress;
return p2 * p2 * progress;
}
}
}

29
src/Avalonia.Animation/Easing/QuinticEaseInOut.cs

@ -0,0 +1,29 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a piece-wise quartic equation.
/// </summary>
public class QuinticEaseInOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double p = progress;
if (p < 0.5d)
{
double p2 = p * p;
return 16d * p2 * p2 * p;
}
else
{
double f = 2d * p - 2d;
double f2 = f * f;
return 0.5d * f2 * f2 * f + 1d;
}
}
}
}

20
src/Avalonia.Animation/Easing/QuinticEaseOut.cs

@ -0,0 +1,20 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases out a <see cref="double"/> value
/// using a quartic equation.
/// </summary>
public class QuinticEaseOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
double f = (progress - 1d);
double f2 = f * f;
return f2 * f2 * f + 1d;
}
}
}

21
src/Avalonia.Animation/Easing/SineEaseIn.cs

@ -0,0 +1,21 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Animation.Utils;
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases in a <see cref="double"/> value
/// using the quarter-wave of sine function.
/// </summary>
public class SineEaseIn : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
return Math.Sin((progress - 1) * EasingUtils.HALFPI) + 1;
}
}
}

20
src/Avalonia.Animation/Easing/SineEaseInOut.cs

@ -0,0 +1,20 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases a <see cref="double"/> value
/// using a half sine wave function.
/// </summary>
public class SineEaseInOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
return 0.5d * (1d - Math.Cos(progress * Math.PI));
}
}
}

22
src/Avalonia.Animation/Easing/SineEaseOut.cs

@ -0,0 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Animation.Utils;
using System;
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Eases out a <see cref="double"/> value
/// using the quarter-wave of sine function
/// with shifted phase.
/// </summary>
public class SineEaseOut : Easing
{
/// <inheritdoc/>
public override double Ease(double progress)
{
return Math.Sin(progress * EasingUtils.HALFPI);
}
}
}

14
src/Avalonia.Animation/FillMode.cs

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Animation
{
public enum FillMode
{
None,
Forward,
Backward,
Both
}
}

23
src/Avalonia.Animation/FloatTransition.cs

@ -0,0 +1,23 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Metadata;
using System;
using System.Reactive.Linq;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="float"/> types.
/// </summary>
public class FloatTransition : Transition<float>
{
/// <inheritdocs/>
public override IObservable<float> DoTransition(IObservable<double> progress, float oldValue, float newValue)
{
var delta = newValue - oldValue;
return progress
.Select(p => (float)Easing.Ease(p) * delta + oldValue);
}
}
}

17
src/Avalonia.Animation/IAnimation.cs

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Animation
{
/// <summary>
/// Interface for Animation objects
/// </summary>
public interface IAnimation
{
/// <summary>
/// Apply the animation to the specified control
/// </summary>
IDisposable Apply(Animatable control, IObservable<bool> match);
}
}

8
src/Avalonia.Animation/IAnimationSetter.cs

@ -0,0 +1,8 @@
namespace Avalonia.Animation
{
public interface IAnimationSetter
{
AvaloniaProperty Property { get; set; }
object Value { get; set; }
}
}

22
src/Avalonia.Animation/IAnimator.cs

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Animation
{
/// <summary>
/// Interface for Animator objects
/// </summary>
public interface IAnimator : IList<AnimatorKeyFrame>
{
/// <summary>
/// The target property.
/// </summary>
AvaloniaProperty Property {get; set;}
/// <summary>
/// Applies the current KeyFrame group to the specified control.
/// </summary>
IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch);
}
}

13
src/Avalonia.Animation/IEasing.cs

@ -1,23 +1,16 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation
namespace Avalonia.Animation.Easings
{
/// <summary>
/// Defines the interface for easing functions.
/// Defines the interface for easing classes.
/// </summary>
public interface IEasing
{
/// <summary>
/// Returns the value of the transition for the specified progress.
/// </summary>
/// <param name="progress">The progress of the transition, from 0 to 1.</param>
/// <param name="start">The start value of the transition.</param>
/// <param name="finish">The end value of the transition.</param>
/// <returns>
/// A value between <paramref name="start"/> and <paramref name="finish"/> as determined
/// by <paramref name="progress"/>.
/// </returns>
object Ease(double progress, object start, object finish);
double Ease(double progress);
}
}

24
src/Avalonia.Animation/IEasing`1.cs

@ -1,24 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation
{
/// <summary>
/// Defines the interface for easing functions.
/// </summary>
/// <typeparam name="T">The type of the property being transitioned.</typeparam>
public interface IEasing<T> : IEasing
{
/// <summary>
/// Returns the value of the transition for the specified progress.
/// </summary>
/// <param name="progress">The progress of the transition, from 0 to 1.</param>
/// <param name="start">The start value of the transition.</param>
/// <param name="finish">The end value of the transition.</param>
/// <returns>
/// A value between <paramref name="start"/> and <paramref name="finish"/> as determined
/// by <paramref name="progress"/>.
/// </returns>
T Ease(double progress, T start, T finish);
}
}

26
src/Avalonia.Animation/ITransition.cs

@ -0,0 +1,26 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Metadata;
using System;
using System.Reactive.Linq;
namespace Avalonia.Animation
{
/// <summary>
/// Interface for Transition objects.
/// </summary>
public interface ITransition
{
/// <summary>
/// Applies the transition to the specified <see cref="Animatable"/>.
/// </summary>
IDisposable Apply(Animatable control, object oldValue, object newValue);
/// <summary>
/// Gets the property to be animated.
/// </summary>
AvaloniaProperty Property { get; set; }
}
}

23
src/Avalonia.Animation/IntegerTransition.cs

@ -0,0 +1,23 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Metadata;
using System;
using System.Reactive.Linq;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="int"/> types.
/// </summary>
public class IntegerTransition : Transition<int>
{
/// <inheritdocs/>
public override IObservable<int> DoTransition(IObservable<double> progress, int oldValue, int newValue)
{
var delta = newValue - oldValue;
return progress
.Select(p => (int)(Easing.Ease(p) * delta + oldValue));
}
}
}

78
src/Avalonia.Animation/KeyFrame.cs

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using Avalonia.Metadata;
using Avalonia.Collections;
namespace Avalonia.Animation
{
/// <summary>
/// Stores data regarding a specific key
/// point and value in an animation.
/// </summary>
public class KeyFrame : AvaloniaList<IAnimationSetter>
{
internal bool timeSpanSet, cueSet;
private TimeSpan _ktimeSpan;
private Cue _kCue;
public KeyFrame()
{
}
public KeyFrame(IEnumerable<IAnimationSetter> items) : base(items)
{
}
public KeyFrame(params IAnimationSetter[] items) : base(items)
{
}
/// <summary>
/// Gets or sets the key time of this <see cref="KeyFrame"/>.
/// </summary>
/// <value>The key time.</value>
public TimeSpan KeyTime
{
get
{
return _ktimeSpan;
}
set
{
if (cueSet)
{
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
}
timeSpanSet = true;
_ktimeSpan = value;
}
}
/// <summary>
/// Gets or sets the cue of this <see cref="KeyFrame"/>.
/// </summary>
/// <value>The cue.</value>
public Cue Cue
{
get
{
return _kCue;
}
set
{
if (timeSpanSet)
{
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
}
cueSet = true;
_kCue = value;
}
}
}
}

41
src/Avalonia.Animation/KeyFramePair`1.cs

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Collections;
using System.ComponentModel;
using Avalonia.Animation.Utils;
using System.Reactive.Linq;
using System.Linq;
using Avalonia.Data;
using System.Reactive.Disposables;
namespace Avalonia.Animation
{
/// <summary>
/// Represents a pair of keyframe, usually the
/// Start and End keyframes of a <see cref="Animator{T}"/> object.
/// </summary>
public struct KeyFramePair<T>
{
/// <summary>
/// Initializes this <see cref="KeyFramePair{T}"/>
/// </summary>
/// <param name="FirstKeyFrame"></param>
/// <param name="LastKeyFrame"></param>
public KeyFramePair(KeyValuePair<double, (T, bool)> FirstKeyFrame, KeyValuePair<double, (T, bool)> LastKeyFrame) : this()
{
this.FirstKeyFrame = FirstKeyFrame;
this.SecondKeyFrame = LastKeyFrame;
}
/// <summary>
/// First <see cref="KeyFrame"/> object.
/// </summary>
public KeyValuePair<double, (T TargetValue, bool isNeutral)> FirstKeyFrame { get; private set; }
/// <summary>
/// Second <see cref="KeyFrame"/> object.
/// </summary>
public KeyValuePair<double, (T TargetValue, bool isNeutral)> SecondKeyFrame { get; private set; }
}
}

41
src/Avalonia.Animation/LinearDoubleEasing.cs

@ -1,41 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation
{
/// <summary>
/// Linearly eases a double value.
/// </summary>
public class LinearDoubleEasing : IEasing<double>
{
/// <summary>
/// Returns the value of the transition for the specified progress.
/// </summary>
/// <param name="progress">The progress of the transition, from 0 to 1.</param>
/// <param name="start">The start value of the transition.</param>
/// <param name="finish">The end value of the transition.</param>
/// <returns>
/// A value between <paramref name="start"/> and <paramref name="finish"/> as determined
/// by <paramref name="progress"/>.
/// </returns>
public double Ease(double progress, double start, double finish)
{
return ((finish - start) * progress) + start;
}
/// <summary>
/// Returns the value of the transition for the specified progress.
/// </summary>
/// <param name="progress">The progress of the transition, from 0 to 1.</param>
/// <param name="start">The start value of the transition.</param>
/// <param name="finish">The end value of the transition.</param>
/// <returns>
/// A value between <paramref name="start"/> and <paramref name="finish"/> as determined
/// by <paramref name="progress"/>.
/// </returns>
object IEasing.Ease(double progress, object start, object finish)
{
return Ease(progress, (double)start, (double)finish);
}
}
}

35
src/Avalonia.Animation/LinearEasing.cs

@ -1,35 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation
{
/// <summary>
/// Returns a linear <see cref="IEasing"/> for the specified type.
/// </summary>
/// <remarks>
/// Unfortunately this class is needed as there's no way to create a true generic easing
/// function at compile time, as mathematical operators don't have an interface.
/// </remarks>
public static class LinearEasing
{
/// <summary>
/// A linear easing function for the specified type.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>An easing function.</returns>
public static IEasing<T> For<T>()
{
if (typeof(T) == typeof(double))
{
return (IEasing<T>)new LinearDoubleEasing();
}
else
{
throw new NotSupportedException(
$"Don't know how to create a LinearEasing for type '{typeof(T).FullName}'.");
}
}
}
}

27
src/Avalonia.Animation/PlayState.cs

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Animation
{
/// <summary>
/// Determines the playback state of an animation.
/// </summary>
public enum PlayState
{
/// <summary>
/// The animation is running.
/// </summary>
Run,
/// <summary>
/// The animation is paused.
/// </summary>
Pause,
/// <summary>
/// The animation is stopped.
/// </summary>
Stop
}
}

32
src/Avalonia.Animation/PlaybackDirection.cs

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Animation
{
/// <summary>
/// Determines the playback direction of an animation.
/// </summary>
public enum PlaybackDirection
{
/// <summary>
/// The animation is played normally.
/// </summary>
Normal,
/// <summary>
/// The animation is played in reverse direction.
/// </summary>
Reverse,
/// <summary>
/// The animation is played forwards first, then backwards.
/// </summary>
Alternate,
/// <summary>
/// The animation is played backwards first, then forwards.
/// </summary>
AlternateReverse
}
}

8
src/Avalonia.Animation/Properties/AssemblyInfo.cs

@ -0,0 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Metadata;
using System.Reflection;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")]

50
src/Avalonia.Animation/PropertyTransition.cs

@ -1,50 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation
{
/// <summary>
/// Defines how a property should be animated using a transition.
/// </summary>
public class PropertyTransition
{
/// <summary>
/// Initializes a new instance of the <see cref="PropertyTransition"/> class.
/// </summary>
/// <param name="property">The property to be animated/</param>
/// <param name="duration">The duration of the animation.</param>
/// <param name="easing">The easing function to use.</param>
public PropertyTransition(AvaloniaProperty property, TimeSpan duration, IEasing easing)
{
Property = property;
Duration = duration;
Easing = easing;
}
/// <summary>
/// Gets the property to be animated.
/// </summary>
/// <value>
/// The property to be animated.
/// </value>
public AvaloniaProperty Property { get; }
/// <summary>
/// Gets the duration of the animation.
/// </summary>
/// <value>
/// The duration of the animation.
/// </value>
public TimeSpan Duration { get; }
/// <summary>
/// Gets the easing function used.
/// </summary>
/// <value>
/// The easing function.
/// </value>
public IEasing Easing { get; }
}
}

202
src/Avalonia.Animation/RepeatCount.cs

@ -0,0 +1,202 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Utilities;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
namespace Avalonia.Animation
{
/// <summary>
/// Defines the valid modes for a <see cref="RepeatCount"/>.
/// </summary>
public enum RepeatType
{
None,
Repeat,
Loop
}
/// <summary>
/// Determines the number of iterations of an animation.
/// Also defines its repeat behavior.
/// </summary>
[TypeConverter(typeof(RepeatCountTypeConverter))]
public struct RepeatCount : IEquatable<RepeatCount>
{
private readonly RepeatType _type;
private readonly ulong _value;
/// <summary>
/// Initializes a new instance of the <see cref="RepeatCount"/> struct.
/// </summary>
/// <param name="value">The number of iterations of an animation.</param>
public RepeatCount(ulong value)
: this(value, RepeatType.Repeat)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RepeatCount"/> struct.
/// </summary>
/// <param name="value">The size of the RepeatCount.</param>
/// <param name="type">The unit of the RepeatCount.</param>
public RepeatCount(ulong value, RepeatType type)
{
if (type < RepeatType.None || type > RepeatType.Loop)
{
throw new ArgumentException("Invalid value", "type");
}
_type = type;
_value = value;
}
/// <summary>
/// Gets an instance of <see cref="RepeatCount"/> that indicates that an animation
/// should repeat forever.
/// </summary>
public static RepeatCount Loop => new RepeatCount(0, RepeatType.Loop);
/// <summary>
/// Gets an instance of <see cref="RepeatCount"/> that indicates that an animation
/// should not repeat.
/// </summary>
public static RepeatCount None => new RepeatCount(0, RepeatType.None);
/// <summary>
/// Gets the unit of the <see cref="RepeatCount"/>.
/// </summary>
public RepeatType RepeatType => _type;
/// <summary>
/// Gets a value that indicates whether the <see cref="RepeatCount"/> is set to loop.
/// </summary>
public bool IsLoop => _type == RepeatType.Loop;
/// <summary>
/// Gets a value that indicates whether the <see cref="RepeatCount"/> is set to not repeat.
/// </summary>
public bool IsNone => _type == RepeatType.None;
/// <summary>
/// Gets the number of repeat iterations.
/// </summary>
public ulong Value => _value;
/// <summary>
/// Compares two RepeatCount structures for equality.
/// </summary>
/// <param name="a">The first RepeatCount.</param>
/// <param name="b">The second RepeatCount.</param>
/// <returns>True if the structures are equal, otherwise false.</returns>
public static bool operator ==(RepeatCount a, RepeatCount b)
{
return (a.IsNone && b.IsNone) && (a.IsLoop && b.IsLoop)
|| (a._value == b._value && a._type == b._type);
}
/// <summary>
/// Compares two RepeatCount structures for inequality.
/// </summary>
/// <param name="rc1">The first RepeatCount.</param>
/// <param name="rc2">The first RepeatCount.</param>
/// <returns>True if the structures are unequal, otherwise false.</returns>
public static bool operator !=(RepeatCount rc1, RepeatCount rc2)
{
return !(rc1 == rc2);
}
/// <summary>
/// Determines whether the <see cref="RepeatCount"/> is equal to the specified object.
/// </summary>
/// <param name="o">The object with which to test equality.</param>
/// <returns>True if the objects are equal, otherwise false.</returns>
public override bool Equals(object o)
{
if (o == null)
{
return false;
}
if (!(o is RepeatCount))
{
return false;
}
return this == (RepeatCount)o;
}
/// <summary>
/// Compares two RepeatCount structures for equality.
/// </summary>
/// <param name="RepeatCount">The structure with which to test equality.</param>
/// <returns>True if the structures are equal, otherwise false.</returns>
public bool Equals(RepeatCount RepeatCount)
{
return this == RepeatCount;
}
/// <summary>
/// Gets a hash code for the RepeatCount.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
return _value.GetHashCode() ^ _type.GetHashCode();
}
/// <summary>
/// Gets a string representation of the <see cref="RepeatCount"/>.
/// </summary>
/// <returns>The string representation.</returns>
public override string ToString()
{
if (IsLoop)
{
return "Auto";
}
else if (IsNone)
{
return "None";
}
string s = _value.ToString();
return s;
}
/// <summary>
/// Parses a string to return a <see cref="RepeatCount"/>.
/// </summary>
/// <param name="s">The string.</param>
/// <returns>The <see cref="RepeatCount"/>.</returns>
public static RepeatCount Parse(string s)
{
s = s.ToUpperInvariant().Trim();
if (s == "NONE")
{
return None;
}
else if (s.EndsWith("LOOP"))
{
return Loop;
}
else
{
if(s.StartsWith("-"))
throw new InvalidCastException("RepeatCount can't be a negative number.");
var value = ulong.Parse(s, CultureInfo.InvariantCulture);
if (value == 1)
return None;
return new RepeatCount(value);
}
}
}
}

23
src/Avalonia.Animation/RepeatCountTypeConverter.cs

@ -0,0 +1,23 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Animation.Easings;
using System;
using System.ComponentModel;
using System.Globalization;
namespace Avalonia.Animation
{
public class RepeatCountTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return RepeatCount.Parse((string)value);
}
}
}

123
src/Avalonia.Animation/Timing.cs

@ -0,0 +1,123 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Threading;
namespace Avalonia.Animation
{
/// <summary>
/// Provides global timing functions for animations.
/// </summary>
public static class Timing
{
static ulong _transitionsFrameCount;
static PlayState _globalState = PlayState.Run;
/// <summary>
/// The number of frames per second.
/// </summary>
public const int FramesPerSecond = 60;
/// <summary>
/// The time span of each frame.
/// </summary>
internal static readonly TimeSpan FrameTick = TimeSpan.FromSeconds(1.0 / FramesPerSecond);
/// <summary>
/// Initializes static members of the <see cref="Timing"/> class.
/// </summary>
static Timing()
{
var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance);
AnimationStateTimer = globalTimer
.Select(_ =>
{
return _globalState;
})
.Publish()
.RefCount();
TransitionsTimer = globalTimer
.Select(p => _transitionsFrameCount++)
.Publish()
.RefCount();
}
/// <summary>
/// Sets the animation play state for all animations
/// </summary>
public static void SetGlobalPlayState(PlayState playState)
{
Dispatcher.UIThread.VerifyAccess();
_globalState = playState;
}
/// <summary>
/// Gets the animation play state for all animations
/// </summary>
public static PlayState GetGlobalPlayState()
{
Dispatcher.UIThread.VerifyAccess();
return _globalState;
}
/// <summary>
/// Gets the animation timer.
/// </summary>
/// <remarks>
/// The animation timer triggers usually at 60 times per second or as
/// defined in <see cref="FramesPerSecond"/>.
/// The parameter passed to a subsciber is the current playstate of the animation.
/// </remarks>
internal static IObservable<PlayState> AnimationStateTimer
{
get;
}
/// <summary>
/// Gets the transitions timer.
/// </summary>
/// <remarks>
/// The transitions timer increments usually 60 times per second as
/// defined in <see cref="FramesPerSecond"/>.
/// The parameter passed to a subsciber is the number of frames since the animation system was
/// initialized.
/// </remarks>
public static IObservable<ulong> TransitionsTimer
{
get;
}
/// <summary>
/// Gets a timer that fires every frame for the specified duration with delay.
/// </summary>
/// <returns>
/// An observable that notifies the subscriber of the progress along the transition.
/// </returns>
/// <remarks>
/// The parameter passed to the subscriber is the progress along the transition, with
/// 0 being the start and 1 being the end. The observable is guaranteed to fire 0
/// immediately on subscribe and 1 at the end of the duration.
/// </remarks>
public static IObservable<double> GetTransitionsTimer(Animatable control, TimeSpan duration, TimeSpan delay = default(TimeSpan))
{
var startTime = _transitionsFrameCount;
var _duration = (ulong)(duration.Ticks / FrameTick.Ticks);
var endTime = startTime + _duration;
return TransitionsTimer
.TakeWhile(x => x < endTime)
.Select(x => (double)(x - startTime) / _duration)
.StartWith(0.0)
.Concat(Observable.Return(1.0));
}
}
}

68
src/Avalonia.Animation/Transition`1.cs

@ -0,0 +1,68 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Metadata;
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Easings;
namespace Avalonia.Animation
{
/// <summary>
/// Defines how a property should be animated using a transition.
/// </summary>
public abstract class Transition<T> : AvaloniaObject, ITransition
{
private AvaloniaProperty _prop;
private Easing _easing;
/// <summary>
/// Gets the duration of the animation.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Gets the easing class to be used.
/// </summary>
public Easing Easing
{
get
{
return _easing ?? (_easing = new LinearEasing());
}
set
{
_easing = value;
}
}
/// <inheritdocs/>
public AvaloniaProperty Property
{
get
{
return _prop;
}
set
{
if (!(value.PropertyType.IsAssignableFrom(typeof(T))))
throw new InvalidCastException
($"Invalid property type \"{typeof(T).Name}\" for this transition: {GetType().Name}.");
_prop = value;
}
}
/// <summary>
/// Apply interpolation to the property.
/// </summary>
public abstract IObservable<T> DoTransition(IObservable<double> progress, T oldValue, T newValue);
/// <inheritdocs/>
public virtual IDisposable Apply(Animatable control, object oldValue, object newValue)
{
var transition = DoTransition(Timing.GetTransitionsTimer(control, Duration, TimeSpan.Zero), (T)oldValue, (T)newValue).Select(p => (object)p);
return control.Bind(Property, transition, Data.BindingPriority.Animation);
}
}
}

8
src/Avalonia.Animation/PropertyTransitions.cs → src/Avalonia.Animation/Transitions.cs

@ -6,14 +6,14 @@ using Avalonia.Collections;
namespace Avalonia.Animation
{
/// <summary>
/// A collection of <see cref="PropertyTransition"/> definitions.
/// A collection of <see cref="ITransition"/> definitions.
/// </summary>
public class PropertyTransitions : AvaloniaList<PropertyTransition>
public class Transitions : AvaloniaList<ITransition>
{
/// <summary>
/// Initializes a new instance of the <see cref="PropertyTransitions"/> class.
/// Initializes a new instance of the <see cref="Transitions"/> class.
/// </summary>
public PropertyTransitions()
public Transitions()
{
ResetBehavior = ResetBehavior.Remove;
}

38
src/Avalonia.Animation/Utils/BounceEaseUtils.cs

@ -0,0 +1,38 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Avalonia.Animation.Utils
{
/// <summary>
/// Helper static class for BounceEase classes.
/// </summary>
internal static class BounceEaseUtils
{
/// <summary>
/// Returns the consequent <see cref="double"/> value of
/// a simulated bounce function.
/// </summary>
/// <param name="progress">The amount of progress from 0 to 1.</param>
/// <returns>The result of the easing function</returns>
internal static double Bounce(double progress)
{
double p = progress;
if (p < 4d / 11.0d)
{
return (121d * p * p) / 16.0d;
}
else if (p < 8d / 11.0d)
{
return (363d / 40.0d * p * p) - (99d / 10.0d * p) + 17d / 5.0d;
}
else if (p < 9d / 10.0d)
{
return (4356d / 361.0d * p * p) - (35442d / 1805.0d * p) + 16061d / 1805.0d;
}
else
{
return (54d / 5.0d * p * p) - (513d / 25.0d * p) + 268d / 25.0d;
}
}
}
}

18
src/Avalonia.Animation/Utils/DoubleUtils.cs

@ -0,0 +1,18 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Animation.Utils
{
internal static class DoubleUtils
{
internal static bool AboutEqual(double x, double y)
{
double epsilon = Math.Max(Math.Abs(x), Math.Abs(y)) * 1E-15;
return Math.Abs(x - y) <= epsilon;
}
}
}

18
src/Avalonia.Animation/Utils/EasingUtils.cs

@ -0,0 +1,18 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Animation.Utils
{
/// <summary>
/// Helper static class for easing mathematical constants.
/// </summary>
internal static class EasingUtils
{
/// <summary>
/// Half of <see cref="Math.PI"/>
/// </summary>
internal static double HALFPI = Math.PI / 2d;
}
}

5
src/Avalonia.Base/Logging/LogArea.cs

@ -18,6 +18,11 @@ namespace Avalonia.Logging
/// </summary>
public const string Binding = "Binding";
/// <summary>
/// The log event comes from the animations system.
/// </summary>
public const string Animations = "Animations";
/// <summary>
/// The log event comes from the visual system.
/// </summary>

12
src/Avalonia.Controls/Carousel.cs

@ -21,10 +21,10 @@ namespace Avalonia.Controls
AvaloniaProperty.Register<Carousel, bool>(nameof(IsVirtualized), true);
/// <summary>
/// Defines the <see cref="Transition"/> property.
/// Defines the <see cref="PageTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition> TransitionProperty =
AvaloniaProperty.Register<Carousel, IPageTransition>(nameof(Transition));
public static readonly StyledProperty<IPageTransition> PageTransitionProperty =
AvaloniaProperty.Register<Carousel, IPageTransition>(nameof(PageTransition));
/// <summary>
/// The default value of <see cref="ItemsControl.ItemsPanelProperty"/> for
@ -57,10 +57,10 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the transition to use when moving between pages.
/// </summary>
public IPageTransition Transition
public IPageTransition PageTransition
{
get { return GetValue(TransitionProperty); }
set { SetValue(TransitionProperty, value); }
get { return GetValue(PageTransitionProperty); }
set { SetValue(PageTransitionProperty, value); }
}
/// <summary>

19
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -33,12 +33,13 @@ namespace Avalonia.Controls.Presenters
(o, v) => o.SelectedIndex = v);
/// <summary>
/// Defines the <see cref="Transition"/> property.
/// Defines the <see cref="PageTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition> TransitionProperty =
Carousel.TransitionProperty.AddOwner<CarouselPresenter>();
public static readonly StyledProperty<IPageTransition> PageTransitionProperty =
Carousel.PageTransitionProperty.AddOwner<CarouselPresenter>();
private int _selectedIndex = -1;
// private Task _current;
private Task _currentTransition;
private int _queuedTransitionIndex = -1;
@ -88,10 +89,10 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Gets or sets a transition to use when switching pages.
/// </summary>
public IPageTransition Transition
public IPageTransition PageTransition
{
get { return GetValue(TransitionProperty); }
set { SetValue(TransitionProperty, value); }
get { return GetValue(PageTransitionProperty); }
set { SetValue(PageTransitionProperty, value); }
}
/// <inheritdoc/>
@ -121,7 +122,7 @@ namespace Avalonia.Controls.Presenters
}
/// <summary>
/// Moves to the selected page, animating if a <see cref="Transition"/> is set.
/// Moves to the selected page, animating if a <see cref="PageTransition"/> is set.
/// </summary>
/// <param name="fromIndex">The index of the old page.</param>
/// <param name="toIndex">The index of the new page.</param>
@ -144,9 +145,9 @@ namespace Avalonia.Controls.Presenters
to = GetOrCreateContainer(toIndex);
}
if (Transition != null && (from != null || to != null))
if (PageTransition != null && (from != null || to != null))
{
await Transition.Start((Visual)from, (Visual)to, fromIndex < toIndex);
await PageTransition.Start((Visual)from, (Visual)to, fromIndex < toIndex);
}
else if (to != null)
{

35
src/Avalonia.Controls/ProgressBar.cs

@ -102,11 +102,13 @@ namespace Avalonia.Controls
UpdateIndicator(Bounds.Size);
}
// TODO: Implement Indeterminate Progress animation
// in xaml (most ideal) or if it's not possible
// then on this class.
private class IndeterminateAnimation : IDisposable
{
private WeakReference<ProgressBar> _progressBar;
private IDisposable _indeterminateBindSubscription;
private TimeSpan _startTime;
private bool _disposed;
public bool Disposed => _disposed;
@ -114,12 +116,7 @@ namespace Avalonia.Controls
private IndeterminateAnimation(ProgressBar progressBar)
{
_progressBar = new WeakReference<ProgressBar>(progressBar);
_startTime = Animate.Stopwatch.Elapsed;
_indeterminateBindSubscription = Animate.Timer.TakeWhile(x => (x - _startTime).TotalSeconds <= 4.0)
.Select(GetAnimationRect)
.Finally(() => _startTime = Animate.Stopwatch.Elapsed)
.Repeat()
.Subscribe(AnimationTick);
}
public static IndeterminateAnimation StartAnimation(ProgressBar progressBar)
@ -129,31 +126,11 @@ namespace Avalonia.Controls
private Rect GetAnimationRect(TimeSpan time)
{
if (_progressBar.TryGetTarget(out var progressBar))
{
if (progressBar.Orientation == Orientation.Horizontal)
return new Rect(-progressBar._indicator.Width - 5 + (time - _startTime).TotalSeconds / 4.0 * (progressBar.Bounds.Width + progressBar._indicator.Width + 10), 0, progressBar._indicator.Bounds.Width, progressBar._indicator.Bounds.Height);
else
return new Rect(0, progressBar.Bounds.Height + 5 - (time - _startTime).TotalSeconds / 4.0 * (progressBar.Bounds.Height + progressBar._indicator.Height + 10), progressBar._indicator.Bounds.Width, progressBar._indicator.Bounds.Height);
}
else
{
_indeterminateBindSubscription.Dispose();
return Rect.Empty;
}
}
private void AnimationTick(Rect rect)
{
if (_progressBar.TryGetTarget(out var progressBar))
progressBar._indicator.Arrange(rect);
else
_indeterminateBindSubscription.Dispose();
return Rect.Empty;
}
public void Dispose()
{
_indeterminateBindSubscription?.Dispose();
_disposed = true;
}
}

12
src/Avalonia.Controls/TabControl.cs

@ -14,10 +14,10 @@ namespace Avalonia.Controls
public class TabControl : SelectingItemsControl
{
/// <summary>
/// Defines the <see cref="Transition"/> property.
/// Defines the <see cref="PageTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition> TransitionProperty =
Avalonia.Controls.Carousel.TransitionProperty.AddOwner<TabControl>();
public static readonly StyledProperty<IPageTransition> PageTransitionProperty =
Avalonia.Controls.Carousel.PageTransitionProperty.AddOwner<TabControl>();
/// <summary>
/// Defines an <see cref="IMemberSelector"/> that selects the content of a <see cref="TabItem"/>.
@ -68,10 +68,10 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the transition to use when switching tabs.
/// </summary>
public IPageTransition Transition
public IPageTransition PageTransition
{
get { return GetValue(TransitionProperty); }
set { SetValue(TransitionProperty, value); }
get { return GetValue(PageTransitionProperty); }
set { SetValue(PageTransitionProperty, value); }
}
/// <summary>

2
src/Avalonia.Styling/Avalonia.Styling.csproj

@ -5,9 +5,9 @@
<RootNamespace>Avalonia</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
</Project>

25
src/Avalonia.Styling/Styling/Style.cs

@ -7,6 +7,8 @@ using System.Collections.Specialized;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Metadata;
using Avalonia.Animation;
using System.Diagnostics;
namespace Avalonia.Styling
{
@ -20,6 +22,8 @@ namespace Avalonia.Styling
private IResourceNode _parent;
private IResourceDictionary _resources;
private IList<IAnimation> _animations;
/// <summary>
/// Initializes a new instance of the <see cref="Style"/> class.
/// </summary>
@ -78,6 +82,14 @@ namespace Avalonia.Styling
[Content]
public IList<ISetter> Setters { get; set; } = new List<ISetter>();
public IList<IAnimation> Animations
{
get
{
return _animations ?? (_animations = new List<IAnimation>());
}
}
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => _parent;
@ -101,6 +113,19 @@ namespace Avalonia.Styling
{
var subs = GetSubscriptions(control);
foreach (var animation in Animations)
{
IObservable<bool> obsMatch = match.ObservableResult;
if (match.ImmediateResult == true)
{
obsMatch = Observable.Return(true);
}
var sub = animation.Apply((Animatable)control, obsMatch);
subs.Add(sub);
}
foreach (var setter in Setters)
{
var sub = setter.Apply(this, control, match.ObservableResult);

3
src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs

@ -128,7 +128,8 @@ namespace Avalonia.Styling
x => control.Classes.CollectionChanged += x,
x => control.Classes.CollectionChanged -= x)
.StartWith((EventPattern<NotifyCollectionChangedEventArgs>)null)
.Select(_ => Matches(control.Classes));
.Select(_ => Matches(control.Classes))
.DistinctUntilChanged();
return new SelectorMatch(observable);
}
else

2
src/Avalonia.Themes.Default/Carousel.xaml

@ -10,7 +10,7 @@
Margin="{TemplateBinding Padding}"
MemberSelector="{TemplateBinding MemberSelector}"
SelectedIndex="{TemplateBinding SelectedIndex}"
Transition="{TemplateBinding Transition}"/>
PageTransition="{TemplateBinding PageTransition}"/>
</Border>
</ControlTemplate>
</Setter>

2
src/Avalonia.Themes.Default/TabControl.xaml

@ -14,7 +14,7 @@
MemberSelector="{x:Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
Transition="{TemplateBinding Transition}"
PageTransition="{TemplateBinding PageTransition}"
Grid.Row="1"/>
</DockPanel>
</Border>

19
src/Avalonia.Visuals/Animation/CrossFade.cs

@ -51,6 +51,8 @@ namespace Avalonia.Animation
{
var tasks = new List<Task>();
// TODO: Implement relevant transition logic here (or discard this class)
// in favor of XAML based transition for pages
if (to != null)
{
to.Opacity = 0;
@ -58,13 +60,6 @@ namespace Avalonia.Animation
if (from != null)
{
tasks.Add(Animate.Property(
(IAvaloniaObject)from,
Visual.OpacityProperty,
from.Opacity,
0,
LinearEasing.For<double>(),
Duration).ToTask());
}
if (to != null)
@ -72,16 +67,10 @@ namespace Avalonia.Animation
to.Opacity = 0;
to.IsVisible = true;
tasks.Add(Animate.Property(
(IAvaloniaObject)to,
Visual.OpacityProperty,
0,
1,
LinearEasing.For<double>(),
Duration).ToTask());
}
await Task.WhenAll(tasks.ToArray());
// FIXME: This is temporary until animations are fixed.
await Task.Delay(1);
if (from != null)
{

27
src/Avalonia.Visuals/Animation/PageSlide.cs

@ -74,34 +74,21 @@ namespace Avalonia.Animation
var distance = Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
var translateProperty = Orientation == SlideAxis.Horizontal ? TranslateTransform.XProperty : TranslateTransform.YProperty;
// TODO: Implement relevant transition logic here (or discard this class)
// in favor of XAML based transition for pages
if (from != null)
{
var transform = new TranslateTransform();
from.RenderTransform = transform;
tasks.Add(Animate.Property(
transform,
translateProperty,
0.0,
forward ? -distance : distance,
LinearEasing.For<double>(),
Duration).ToTask());
}
if (to != null)
{
var transform = new TranslateTransform();
to.RenderTransform = transform;
to.IsVisible = true;
tasks.Add(Animate.Property(
transform,
translateProperty,
forward ? distance : -distance,
0.0,
LinearEasing.For<double>(),
Duration).ToTask());
}
await Task.WhenAll(tasks.ToArray());
// FIXME: This is temporary until animations are fixed.
await Task.Delay(1);
if (from != null)
{

31
src/Avalonia.Visuals/Animation/PointTransition.cs

@ -0,0 +1,31 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Metadata;
using System;
using System.Reactive.Linq;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Point"/> type.
/// </summary>
public class PointTransition : Transition<Point>
{
/// <inheritdocs/>
public override IObservable<Point> DoTransition(IObservable<double> progress, Point oldValue, Point newValue)
{
var deltaX = newValue.X - oldValue.Y;
var deltaY = newValue.X - oldValue.Y;
return progress
.Select(p =>
{
var f = Easing.Ease(p);
var nX = f * deltaX + oldValue.X;
var nY = f * deltaY + oldValue.Y;
return new Point(nX, nY);
});
}
}
}

35
src/Avalonia.Visuals/Animation/ThicknessTransition.cs

@ -0,0 +1,35 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Metadata;
using System;
using System.Reactive.Linq;
namespace Avalonia.Animation
{
/// <summary>
/// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Thickness"/> type.
/// </summary>
public class ThicknessTransition : Transition<Thickness>
{
/// <inheritdocs/>
public override IObservable<Thickness> DoTransition(IObservable<double> progress, Thickness oldValue, Thickness newValue)
{
var deltaL = newValue.Left - oldValue.Left;
var deltaT = newValue.Top - oldValue.Top;
var deltaR = newValue.Right - oldValue.Right;
var deltaB = newValue.Bottom - oldValue.Bottom;
return progress
.Select(p =>
{
var f = Easing.Ease(p);
var nL = f * deltaL + oldValue.Left;
var nT = f * deltaT + oldValue.Right;
var nR = f * deltaR + oldValue.Top;
var nB = f * deltaB + oldValue.Bottom;
return new Thickness(nL, nT, nR, nB);
});
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save