diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Behaviors/AnimationBehaviorPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Behaviors/AnimationBehaviorPage.xaml
index db7ee9f13b..cf59c1db3f 100644
--- a/samples/CommunityToolkit.Maui.Sample/Pages/Behaviors/AnimationBehaviorPage.xaml
+++ b/samples/CommunityToolkit.Maui.Sample/Pages/Behaviors/AnimationBehaviorPage.xaml
@@ -86,7 +86,62 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Maui.UnitTests/Animations/BaseAnimationTests.cs b/src/CommunityToolkit.Maui.UnitTests/Animations/BaseAnimationTests.cs
new file mode 100644
index 0000000000..e4b0a64b2c
--- /dev/null
+++ b/src/CommunityToolkit.Maui.UnitTests/Animations/BaseAnimationTests.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics;
+using CommunityToolkit.Maui.Animations;
+using CommunityToolkit.Maui.UnitTests.Mocks;
+using FluentAssertions;
+using Xunit;
+
+namespace CommunityToolkit.Maui.UnitTests.Animations;
+
+public abstract class BaseAnimationTests : BaseTest where TAnimation : BaseAnimation, new()
+{
+ protected virtual TAnimation CreateAnimation() => new();
+
+ [Fact]
+ public async Task LengthShouldDictateFullAnimationLength()
+ {
+ var animation = CreateAnimation();
+
+ Label label = new();
+
+ label.EnableAnimations();
+
+ var stopwatch = new Stopwatch();
+ stopwatch.Start();
+ await animation.Animate(label);
+ stopwatch.Stop();
+
+ double allowance = (animation.Length * 0.1) + 50;
+
+ stopwatch.ElapsedMilliseconds.Should().BeCloseTo(animation.Length, (uint)allowance);
+
+ stopwatch.Reset();
+ }
+}
diff --git a/src/CommunityToolkit.Maui.UnitTests/Animations/FadeAnimationTests.cs b/src/CommunityToolkit.Maui.UnitTests/Animations/FadeAnimationTests.cs
index 92091ad858..93dcf4deed 100644
--- a/src/CommunityToolkit.Maui.UnitTests/Animations/FadeAnimationTests.cs
+++ b/src/CommunityToolkit.Maui.UnitTests/Animations/FadeAnimationTests.cs
@@ -5,12 +5,12 @@
namespace CommunityToolkit.Maui.UnitTests.Animations;
-public class FadeAnimationTests : BaseTest
+public class FadeAnimationTests : BaseAnimationTests
{
[Fact(Timeout = (int)TestDuration.Short)]
public async Task AnimateShouldThrowWithNullView()
{
- FadeAnimation animation = new();
+ FadeAnimation animation = CreateAnimation();
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
await Assert.ThrowsAsync(() => animation.Animate(null, CancellationToken.None));
@@ -54,7 +54,7 @@ public async Task CancellationTokenExpired()
[Fact(Timeout = (int)TestDuration.Medium)]
public async Task AnimateShouldReturnToOriginalOpacity()
{
- FadeAnimation animation = new();
+ FadeAnimation animation = CreateAnimation();
var label = new Label
{
diff --git a/src/CommunityToolkit.Maui.UnitTests/Animations/FlipHorizontalAnimationTests.cs b/src/CommunityToolkit.Maui.UnitTests/Animations/FlipHorizontalAnimationTests.cs
new file mode 100644
index 0000000000..2653a3aa7c
--- /dev/null
+++ b/src/CommunityToolkit.Maui.UnitTests/Animations/FlipHorizontalAnimationTests.cs
@@ -0,0 +1,7 @@
+using CommunityToolkit.Maui.Animations;
+
+namespace CommunityToolkit.Maui.UnitTests.Animations;
+
+public class FlipHorizontalAnimationTests : BaseAnimationTests
+{
+}
diff --git a/src/CommunityToolkit.Maui.UnitTests/Animations/FlipVerticalAnimationTests.cs b/src/CommunityToolkit.Maui.UnitTests/Animations/FlipVerticalAnimationTests.cs
new file mode 100644
index 0000000000..b6941f4fcc
--- /dev/null
+++ b/src/CommunityToolkit.Maui.UnitTests/Animations/FlipVerticalAnimationTests.cs
@@ -0,0 +1,7 @@
+using CommunityToolkit.Maui.Animations;
+
+namespace CommunityToolkit.Maui.UnitTests.Animations;
+
+public class FlipVerticalAnimationTests : BaseAnimationTests
+{
+}
diff --git a/src/CommunityToolkit.Maui.UnitTests/Animations/RotateAnimationTests.cs b/src/CommunityToolkit.Maui.UnitTests/Animations/RotateAnimationTests.cs
new file mode 100644
index 0000000000..42b3179cf0
--- /dev/null
+++ b/src/CommunityToolkit.Maui.UnitTests/Animations/RotateAnimationTests.cs
@@ -0,0 +1,7 @@
+using CommunityToolkit.Maui.Animations;
+
+namespace CommunityToolkit.Maui.UnitTests.Animations;
+
+public class RotateAnimationTests : BaseAnimationTests
+{
+}
diff --git a/src/CommunityToolkit.Maui.UnitTests/Animations/ScaleAnimationTests.cs b/src/CommunityToolkit.Maui.UnitTests/Animations/ScaleAnimationTests.cs
new file mode 100644
index 0000000000..180c4272c1
--- /dev/null
+++ b/src/CommunityToolkit.Maui.UnitTests/Animations/ScaleAnimationTests.cs
@@ -0,0 +1,7 @@
+using CommunityToolkit.Maui.Animations;
+
+namespace CommunityToolkit.Maui.UnitTests.Animations;
+
+public class ScaleAnimationTests : BaseAnimationTests
+{
+}
diff --git a/src/CommunityToolkit.Maui.UnitTests/Animations/ShakeAnimationTests.cs b/src/CommunityToolkit.Maui.UnitTests/Animations/ShakeAnimationTests.cs
new file mode 100644
index 0000000000..5ab7100e36
--- /dev/null
+++ b/src/CommunityToolkit.Maui.UnitTests/Animations/ShakeAnimationTests.cs
@@ -0,0 +1,8 @@
+using CommunityToolkit.Maui.Animations;
+
+namespace CommunityToolkit.Maui.UnitTests.Animations;
+
+public class ShakeAnimationTests : BaseAnimationTests
+{
+
+}
diff --git a/src/CommunityToolkit.Maui/Animations/BaseAnimation.shared.cs b/src/CommunityToolkit.Maui/Animations/BaseAnimation.shared.cs
index 1c4aaa39d4..cb0924155f 100644
--- a/src/CommunityToolkit.Maui/Animations/BaseAnimation.shared.cs
+++ b/src/CommunityToolkit.Maui/Animations/BaseAnimation.shared.cs
@@ -1,4 +1,5 @@
namespace CommunityToolkit.Maui.Animations;
+
///
/// Abstract class for animation types to inherit.
///
diff --git a/src/CommunityToolkit.Maui/Animations/FadeAnimation.shared.cs b/src/CommunityToolkit.Maui/Animations/FadeAnimation.shared.cs
index f600e3a71d..62d4856587 100644
--- a/src/CommunityToolkit.Maui/Animations/FadeAnimation.shared.cs
+++ b/src/CommunityToolkit.Maui/Animations/FadeAnimation.shared.cs
@@ -20,7 +20,7 @@ public class FadeAnimation : BaseAnimation
///
/// Initializes a new instance of .
///
- public FadeAnimation() : base(300)
+ public FadeAnimation() : base(600)
{
}
@@ -41,7 +41,9 @@ public override async Task Animate(VisualElement view, CancellationToken token =
var originalOpacity = view.Opacity;
- await view.FadeTo(Opacity, Length, Easing).WaitAsync(token);
- await view.FadeTo(originalOpacity, Length, Easing).WaitAsync(token);
+ var duration = Length / 2;
+
+ await view.FadeTo(Opacity, duration, Easing).WaitAsync(token);
+ await view.FadeTo(originalOpacity, duration, Easing).WaitAsync(token);
}
}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui/Animations/FlipHorizontalAnimation.shared.cs b/src/CommunityToolkit.Maui/Animations/FlipHorizontalAnimation.shared.cs
new file mode 100644
index 0000000000..7494426dc0
--- /dev/null
+++ b/src/CommunityToolkit.Maui/Animations/FlipHorizontalAnimation.shared.cs
@@ -0,0 +1,31 @@
+using Microsoft.Maui.Controls;
+
+namespace CommunityToolkit.Maui.Animations;
+
+///
+/// Animation that will flip the supplied view horizontally.
+///
+public class FlipHorizontalAnimation : RotateAnimation
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ public FlipHorizontalAnimation() : base(600)
+ {
+
+ }
+
+ ///
+ protected override double DefaultRotation { get; set; } = 90;
+
+ ///
+ public override async Task Animate(VisualElement view)
+ {
+ ArgumentNullException.ThrowIfNull(view);
+
+ var duration = Length / 2;
+
+ await view.RotateYTo(Rotation, duration, Easing);
+ await view.RotateYTo(0, duration, Easing);
+ }
+}
diff --git a/src/CommunityToolkit.Maui/Animations/FlipVerticalAnimation.shared.cs b/src/CommunityToolkit.Maui/Animations/FlipVerticalAnimation.shared.cs
new file mode 100644
index 0000000000..f83c432aa4
--- /dev/null
+++ b/src/CommunityToolkit.Maui/Animations/FlipVerticalAnimation.shared.cs
@@ -0,0 +1,29 @@
+namespace CommunityToolkit.Maui.Animations;
+
+///
+/// Animation that will flip the supplied view vertically.
+///
+public class FlipVerticalAnimation : RotateAnimation
+{
+ ///
+ /// Initializes a new instance of .
+ ///
+ public FlipVerticalAnimation() : base(600)
+ {
+
+ }
+
+ ///
+ protected override double DefaultRotation { get; set; } = 90;
+
+ ///
+ public override async Task Animate(VisualElement view)
+ {
+ ArgumentNullException.ThrowIfNull(view);
+
+ var duration = Length / 2;
+
+ await view.RotateXTo(Rotation, duration, Easing);
+ await view.RotateXTo(0, duration, Easing);
+ }
+}
diff --git a/src/CommunityToolkit.Maui/Animations/RotateAnimation.shared.cs b/src/CommunityToolkit.Maui/Animations/RotateAnimation.shared.cs
new file mode 100644
index 0000000000..600819e003
--- /dev/null
+++ b/src/CommunityToolkit.Maui/Animations/RotateAnimation.shared.cs
@@ -0,0 +1,61 @@
+namespace CommunityToolkit.Maui.Animations;
+
+///
+/// Animation that will rotate the supplied view by the specified .
+///
+public class RotateAnimation : BaseAnimation
+{
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly BindableProperty RotationProperty =
+ BindableProperty.Create(
+ nameof(Rotation),
+ typeof(double),
+ typeof(RotateAnimation),
+ 180.0,
+ BindingMode.TwoWay,
+ defaultValueCreator: GetDefaultRotationProperty);
+
+ ///
+ /// Gets or sets the rotation used by the animation.
+ ///
+ public double Rotation
+ {
+ get => (double)GetValue(RotationProperty);
+ set => SetValue(RotationProperty, value);
+ }
+
+ static object GetDefaultRotationProperty(BindableObject bindable)
+ => ((RotateAnimation)bindable).DefaultRotation;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public RotateAnimation() : base(200)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The default length of the animation.
+ protected RotateAnimation(uint defaultLength) : base(defaultLength)
+ {
+ }
+
+ ///
+ /// Gets or sets the default rotation used by the animation.
+ ///
+ protected virtual double DefaultRotation { get; set; } = 180.0;
+
+ ///
+ public override async Task Animate(VisualElement view)
+ {
+ ArgumentNullException.ThrowIfNull(view);
+
+ await view.RotateTo(Rotation, Length, Easing);
+ view.Rotation = 0;
+ }
+}
+
diff --git a/src/CommunityToolkit.Maui/Animations/ScaleAnimation.shared.cs b/src/CommunityToolkit.Maui/Animations/ScaleAnimation.shared.cs
new file mode 100644
index 0000000000..af334d97f5
--- /dev/null
+++ b/src/CommunityToolkit.Maui/Animations/ScaleAnimation.shared.cs
@@ -0,0 +1,48 @@
+namespace CommunityToolkit.Maui.Animations;
+
+///
+/// Animation that will scale the supplied view to the specified and then back down to its original scale.
+///
+public class ScaleAnimation : BaseAnimation
+{
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly BindableProperty ScaleProperty =
+ BindableProperty.Create(
+ nameof(Scale),
+ typeof(double),
+ typeof(ScaleAnimation),
+ 1.2,
+ BindingMode.TwoWay);
+
+ ///
+ /// Gets or sets the scale to animate to before returning to the element's current scale.
+ ///
+ public double Scale
+ {
+ get => (double)GetValue(ScaleProperty);
+ set => SetValue(ScaleProperty, value);
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public ScaleAnimation() : base(340)
+ {
+
+ }
+
+ ///
+ public override async Task Animate(VisualElement view)
+ {
+ ArgumentNullException.ThrowIfNull(view);
+
+ var originalScale = view.Scale;
+
+ var duration = Length / 2;
+
+ await view.ScaleTo(Scale, duration, Easing);
+ await view.ScaleTo(originalScale, duration, Easing);
+ }
+}
diff --git a/src/CommunityToolkit.Maui/Animations/ShakeAnimation.shared.cs b/src/CommunityToolkit.Maui/Animations/ShakeAnimation.shared.cs
new file mode 100644
index 0000000000..13d4c4032f
--- /dev/null
+++ b/src/CommunityToolkit.Maui/Animations/ShakeAnimation.shared.cs
@@ -0,0 +1,73 @@
+namespace CommunityToolkit.Maui.Animations;
+
+///
+/// Animation that will shake the supplied view on the x-axis, starting with the
+/// and then reducing each time by the .
+///
+public class ShakeAnimation : BaseAnimation
+{
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly BindableProperty StartFactorProperty =
+ BindableProperty.Create(
+ nameof(StartFactor),
+ typeof(double),
+ typeof(ShakeAnimation),
+ 15.0,
+ BindingMode.TwoWay);
+
+ ///
+ /// Gets or sets the start factor, this is the biggest movement during the shake.
+ ///
+ public double StartFactor
+ {
+ get => (double)GetValue(StartFactorProperty);
+ set => SetValue(StartFactorProperty, value);
+ }
+
+ ///
+ /// Backing BindableProperty for the property.
+ ///
+ public static readonly BindableProperty ReducingAmountProperty =
+ BindableProperty.Create(
+ nameof(ReducingAmount),
+ typeof(double),
+ typeof(ShakeAnimation),
+ 5.0,
+ BindingMode.TwoWay);
+
+ ///
+ /// Gets or sets the amount to reduce the by on each return to 0 on the x-axis.
+ ///
+ public double ReducingAmount
+ {
+ get => (double)GetValue(ReducingAmountProperty);
+ set => SetValue(ReducingAmountProperty, value);
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public ShakeAnimation() : base(300)
+ {
+
+ }
+
+ ///
+ public override async Task Animate(VisualElement view)
+ {
+ ArgumentNullException.ThrowIfNull(view);
+
+ var duration = (uint)(Length / Math.Ceiling(StartFactor / ReducingAmount)) / 2;
+
+ for (var i = StartFactor; i > 0; i -= ReducingAmount)
+ {
+ await view.TranslateTo(-i, 0, duration, Easing);
+ await view.TranslateTo(i, 0, duration, Easing);
+ }
+
+ view.TranslationX = 0;
+ }
+}
+
diff --git a/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj b/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj
index 858cb1cc9e..1c6db7c900 100644
--- a/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj
+++ b/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj
@@ -47,6 +47,9 @@
Debug;Release
+
+ false
+