diff --git a/build/ios-uitest-run.sh b/build/ios-uitest-run.sh
index 6bb53b2c4a41..8b5549551e95 100755
--- a/build/ios-uitest-run.sh
+++ b/build/ios-uitest-run.sh
@@ -45,7 +45,7 @@ else
namespace = 'SamplesApp.UITests.Windows_UI_Xaml_Controls.PivotTests' or \
namespace = 'SamplesApp.UITests.Windows_UI_Xaml_Controls.CommandBarTests' or \
namespace = 'SamplesApp.UITests.Windows_UI_Xaml_Controls.ComboBoxTests' or \
- namespace = 'SamplesApp.UITests.Windows_UI_Xaml_Media_Animation.DoubleAnimation_Tests' or \
+ namespace = 'SamplesApp.UITests.Windows_UI_Xaml_Media_Animation' or \
namespace = 'SamplesApp.UITests.Windows_UI_Xaml_Controls.BorderTests'
"
fi
diff --git a/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Media_Animation/ColorAnimation_Tests.cs b/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Media_Animation/ColorAnimation_Tests.cs
new file mode 100644
index 000000000000..8ca605430875
--- /dev/null
+++ b/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Media_Animation/ColorAnimation_Tests.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using SamplesApp.UITests.TestFramework;
+using Uno.UITest.Helpers.Queries;
+
+namespace SamplesApp.UITests.Windows_UI_Xaml_Media_Animation
+{
+ [TestFixture]
+ public class ColorAnimation_Tests : SampleControlUITestBase
+ {
+ [Test]
+ [AutoRetry]
+ [ActivePlatforms(Platform.Android, Platform.Browser)] // Disabled for iOS because HasColor() behaves strangely: https://github.com/unoplatform/uno/issues/1955
+ public void When_Border_Background_Animated()
+ {
+ Run("UITests.Windows_UI_Xaml_Media_Animation.ColorAnimation_Background");
+
+ _app.WaitForElement("PlayColorAnimation");
+
+ _app.FastTap("BrushEqualityButton");
+
+ _app.WaitForText("BrushEqualityText", "true");
+
+ _app.FastTap("PlayColorAnimation");
+
+ _app.WaitForText("StatusText", "Completed");
+
+ _app.FastTap("BrushEqualityButton");
+
+ _app.WaitForText("BrushEqualityText", "false");
+
+ var targetRect = _app.GetRect("TargetBorder");
+
+ var indepRect = _app.GetRect("IndependentBorder");
+
+ var bmp = _app.Screenshot("Completed");
+
+ ImageAssert.HasColorAt(bmp, targetRect.CenterX, targetRect.CenterY, Color.Red);
+
+ ImageAssert.HasColorAt(bmp, indepRect.CenterX, indepRect.CenterY, Color.Blue); //Shared resource shouldn't be modified
+ }
+
+ [Test]
+ [AutoRetry]
+ [ActivePlatforms(Platform.Android, Platform.Browser)] // Disabled for iOS because HasColor() behaves strangely: https://github.com/unoplatform/uno/issues/1955
+ public void When_Rectangle_Fill_Animated()
+ {
+ Run("UITests.Windows_UI_Xaml_Media_Animation.ColorAnimation_Fill");
+
+ _app.WaitForElement("PlayColorAnimation");
+
+ _app.FastTap("PlayColorAnimation");
+
+ _app.WaitForText("StatusText", "Completed");
+
+ var targetRect = _app.GetRect("TargetRectangle");
+
+ var bmp = _app.Screenshot("Completed");
+
+ ImageAssert.HasColorAt(bmp, targetRect.CenterX, targetRect.CenterY, Color.Brown);
+ }
+ }
+}
diff --git a/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Media_Animation/DoubleAnimation_Tests.FinalState_Opacity.cs b/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Media_Animation/DoubleAnimation_Tests.FinalState_Opacity.cs
index 621b41dfa40e..d0fc58c4d192 100644
--- a/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Media_Animation/DoubleAnimation_Tests.FinalState_Opacity.cs
+++ b/src/SamplesApp/SamplesApp.UITests/Windows_UI_Xaml_Media_Animation/DoubleAnimation_Tests.FinalState_Opacity.cs
@@ -14,6 +14,7 @@
namespace SamplesApp.UITests.Windows_UI_Xaml_Media_Animation
{
[TestFixture]
+ [ActivePlatforms(Platform.Android, Platform.Browser)] // Disabled for iOS: https://github.com/unoplatform/uno/issues/1955
public partial class DoubleAnimation_Tests : SampleControlUITestBase
{
private const string _finalStateOpacityTestControl = "UITests.Windows_UI_Xaml_Media_Animation.DoubleAnimation_FinalState_Opacity";
diff --git a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems
index 37407f43ff8e..12ea99ffd0a2 100644
--- a/src/SamplesApp/UITests.Shared/UITests.Shared.projitems
+++ b/src/SamplesApp/UITests.Shared/UITests.Shared.projitems
@@ -2573,6 +2573,14 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
@@ -4436,6 +4444,12 @@
RoutedEvent_TappedControl.xaml
+
+ ColorAnimation_Background.xaml
+
+
+ ColorAnimation_Fill.xaml
+
DoubleAnimation_FinalState_Opacity.xaml
diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Background.xaml b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Background.xaml
new file mode 100644
index 000000000000..b37721dd2851
--- /dev/null
+++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Background.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Background.xaml.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Background.xaml.cs
new file mode 100644
index 000000000000..fb1cfb57e02d
--- /dev/null
+++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Background.xaml.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices.WindowsRuntime;
+using Uno.UI.Samples.Controls;
+using Windows.Foundation;
+using Windows.Foundation.Collections;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Controls.Primitives;
+using Windows.UI.Xaml.Data;
+using Windows.UI.Xaml.Input;
+using Windows.UI.Xaml.Media;
+using Windows.UI.Xaml.Navigation;
+
+// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236
+
+namespace UITests.Windows_UI_Xaml_Media_Animation
+{
+ [Sample("Animations")]
+ public sealed partial class ColorAnimation_Background : UserControl
+ {
+ public ColorAnimation_Background()
+ {
+ this.InitializeComponent();
+ }
+
+ private void PlayColorAnimation_Click(object sender, RoutedEventArgs args)
+ {
+ colorStoryboard.Completed += (o, e) =>
+ {
+ StatusText.Text = "Completed";
+ };
+ colorStoryboard.Begin();
+ }
+
+ private void CheckBrushEquality_Click(object sender, RoutedEventArgs args)
+ {
+ // Assert non-null
+ var areEqual = ReferenceEquals(TargetBorder.Background, IndependentBorder.Background);
+ BrushEqualityText.Text = areEqual.ToString().ToLowerInvariant();
+ }
+ }
+}
diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Fill.xaml b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Fill.xaml
new file mode 100644
index 000000000000..63c422186bec
--- /dev/null
+++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Fill.xaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Fill.xaml.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Fill.xaml.cs
new file mode 100644
index 000000000000..30077ed301db
--- /dev/null
+++ b/src/SamplesApp/UITests.Shared/Windows_UI_Xaml_Media_Animation/ColorAnimation_Fill.xaml.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices.WindowsRuntime;
+using Uno.UI.Samples.Controls;
+using Windows.Foundation;
+using Windows.Foundation.Collections;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Controls.Primitives;
+using Windows.UI.Xaml.Data;
+using Windows.UI.Xaml.Input;
+using Windows.UI.Xaml.Media;
+using Windows.UI.Xaml.Navigation;
+
+// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236
+
+namespace UITests.Windows_UI_Xaml_Media_Animation
+{
+ [Sample("Animations")]
+ public sealed partial class ColorAnimation_Fill : UserControl
+ {
+ public ColorAnimation_Fill()
+ {
+ this.InitializeComponent();
+ }
+
+ private void PlayColorAnimation_Click(object sender, RoutedEventArgs args)
+ {
+ colorStoryboard.Completed += (o, e) =>
+ {
+ StatusText.Text = "Completed";
+ };
+ colorStoryboard.Begin();
+ }
+ }
+}
diff --git a/src/Uno.UI/DataBinding/BindingPath.cs b/src/Uno.UI/DataBinding/BindingPath.cs
index 28de8e831263..c8e4f6805812 100644
--- a/src/Uno.UI/DataBinding/BindingPath.cs
+++ b/src/Uno.UI/DataBinding/BindingPath.cs
@@ -12,6 +12,7 @@
using Uno.Logging;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Data;
+using Windows.UI.Xaml.Media;
namespace Uno.UI.DataBinding
{
@@ -100,6 +101,26 @@ public IEnumerable GetPathItems()
return _chain.Flatten(i => i.Next);
}
+ ///
+ /// Checks the property path for members which may be shared resources (es and s) and creates a
+ /// copy of them if need be (ie if not already copied). Intended to be used prior to animating the targeted property.
+ ///
+ internal void CloneShareableObjectsInPath()
+ {
+ foreach (BindingItem item in GetPathItems())
+ {
+ if (item.PropertyType == typeof(Brush) || item.PropertyType == typeof(GeneralTransform))
+ {
+ if (item.Value is IShareableDependencyObject shareable && !shareable.IsClone && item.DataContext is DependencyObject owner)
+ {
+ var clone = shareable.Clone();
+
+ item.Value = clone;
+ break;
+ }
+ }
+ }
+ }
///
/// Registers a property changed registration handler.
diff --git a/src/Uno.UI/DataBinding/BindingPropertyHelper.FastConvert.cs b/src/Uno.UI/DataBinding/BindingPropertyHelper.FastConvert.cs
index 04158b1f6a58..66a7d0b845a6 100644
--- a/src/Uno.UI/DataBinding/BindingPropertyHelper.FastConvert.cs
+++ b/src/Uno.UI/DataBinding/BindingPropertyHelper.FastConvert.cs
@@ -10,6 +10,7 @@
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI.Xaml.Media.Animation;
+using Windows.UI;
#if XAMARIN_ANDROID
using View = Android.Views.View;
@@ -66,6 +67,13 @@ private static bool FastConvert(Type outputType, object input, ref object output
return true;
}
break;
+
+ case ColorOffset colorOffsetInput:
+ if (FastColorOffsetConvert(outputType, colorOffsetInput, ref output))
+ {
+ return true;
+ }
+ break;
}
return false;
@@ -93,6 +101,17 @@ private static bool FastEnumConvert(Type outputType, object input, ref object ou
return false;
}
+ private static bool FastColorOffsetConvert(Type outputType, ColorOffset input, ref object output)
+ {
+ if (outputType == typeof(Windows.UI.Color))
+ {
+ output = (Windows.UI.Color)input;
+ return true;
+ }
+
+ return false;
+ }
+
private static bool FastNumberConvert(Type outputType, object input, ref object output)
{
if (input is double doubleInput)
diff --git a/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Media.Animation/ColorAnimation.cs b/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Media.Animation/ColorAnimation.cs
index 406abf953f76..e94933a55adb 100644
--- a/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Media.Animation/ColorAnimation.cs
+++ b/src/Uno.UI/Generated/3.0.0.0/Windows.UI.Xaml.Media.Animation/ColorAnimation.cs
@@ -2,53 +2,11 @@
#pragma warning disable 114 // new keyword hiding
namespace Windows.UI.Xaml.Media.Animation
{
- #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
+#if false || false || false || false || __MACOS__
[global::Uno.NotImplemented]
- #endif
- public partial class ColorAnimation : global::Windows.UI.Xaml.Media.Animation.Timeline
+#endif
+ public partial class ColorAnimation : global::Windows.UI.Xaml.Media.Animation.Timeline
{
- #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
- [global::Uno.NotImplemented]
- public global::Windows.UI.Color? To
- {
- get
- {
- return (global::Windows.UI.Color?)this.GetValue(ToProperty);
- }
- set
- {
- this.SetValue(ToProperty, value);
- }
- }
- #endif
- #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
- [global::Uno.NotImplemented]
- public global::Windows.UI.Color? From
- {
- get
- {
- return (global::Windows.UI.Color?)this.GetValue(FromProperty);
- }
- set
- {
- this.SetValue(FromProperty, value);
- }
- }
- #endif
- #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
- [global::Uno.NotImplemented]
- public bool EnableDependentAnimation
- {
- get
- {
- return (bool)this.GetValue(EnableDependentAnimationProperty);
- }
- set
- {
- this.SetValue(EnableDependentAnimationProperty, value);
- }
- }
- #endif
#if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
public global::Windows.UI.Xaml.Media.Animation.EasingFunctionBase EasingFunction
@@ -65,65 +23,12 @@ public bool EnableDependentAnimation
#endif
#if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
[global::Uno.NotImplemented]
- public global::Windows.UI.Color? By
- {
- get
- {
- return (global::Windows.UI.Color?)this.GetValue(ByProperty);
- }
- set
- {
- this.SetValue(ByProperty, value);
- }
- }
- #endif
- #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
- [global::Uno.NotImplemented]
- public static global::Windows.UI.Xaml.DependencyProperty ByProperty { get; } =
- Windows.UI.Xaml.DependencyProperty.Register(
- "By", typeof(global::Windows.UI.Color?),
- typeof(global::Windows.UI.Xaml.Media.Animation.ColorAnimation),
- new FrameworkPropertyMetadata(default(global::Windows.UI.Color?)));
- #endif
- #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
- [global::Uno.NotImplemented]
public static global::Windows.UI.Xaml.DependencyProperty EasingFunctionProperty { get; } =
Windows.UI.Xaml.DependencyProperty.Register(
"EasingFunction", typeof(global::Windows.UI.Xaml.Media.Animation.EasingFunctionBase),
typeof(global::Windows.UI.Xaml.Media.Animation.ColorAnimation),
new FrameworkPropertyMetadata(default(global::Windows.UI.Xaml.Media.Animation.EasingFunctionBase)));
#endif
- #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
- [global::Uno.NotImplemented]
- public static global::Windows.UI.Xaml.DependencyProperty EnableDependentAnimationProperty { get; } =
- Windows.UI.Xaml.DependencyProperty.Register(
- "EnableDependentAnimation", typeof(bool),
- typeof(global::Windows.UI.Xaml.Media.Animation.ColorAnimation),
- new FrameworkPropertyMetadata(default(bool)));
- #endif
- #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
- [global::Uno.NotImplemented]
- public static global::Windows.UI.Xaml.DependencyProperty FromProperty { get; } =
- Windows.UI.Xaml.DependencyProperty.Register(
- "From", typeof(global::Windows.UI.Color?),
- typeof(global::Windows.UI.Xaml.Media.Animation.ColorAnimation),
- new FrameworkPropertyMetadata(default(global::Windows.UI.Color?)));
- #endif
- #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
- [global::Uno.NotImplemented]
- public static global::Windows.UI.Xaml.DependencyProperty ToProperty { get; } =
- Windows.UI.Xaml.DependencyProperty.Register(
- "To", typeof(global::Windows.UI.Color?),
- typeof(global::Windows.UI.Xaml.Media.Animation.ColorAnimation),
- new FrameworkPropertyMetadata(default(global::Windows.UI.Color?)));
- #endif
- #if __ANDROID__ || __IOS__ || NET461 || __WASM__ || __MACOS__
- [global::Uno.NotImplemented]
- public ColorAnimation() : base()
- {
- global::Windows.Foundation.Metadata.ApiInformation.TryRaiseNotImplemented("Windows.UI.Xaml.Media.Animation.ColorAnimation", "ColorAnimation.ColorAnimation()");
- }
- #endif
// Forced skipping of method Windows.UI.Xaml.Media.Animation.ColorAnimation.ColorAnimation()
// Forced skipping of method Windows.UI.Xaml.Media.Animation.ColorAnimation.From.get
// Forced skipping of method Windows.UI.Xaml.Media.Animation.ColorAnimation.From.set
diff --git a/src/Uno.UI/UI/Xaml/FrameworkElement.Interface.wasm.cs b/src/Uno.UI/UI/Xaml/FrameworkElement.Interface.wasm.cs
index 62360d3caf06..d1a1665840b6 100644
--- a/src/Uno.UI/UI/Xaml/FrameworkElement.Interface.wasm.cs
+++ b/src/Uno.UI/UI/Xaml/FrameworkElement.Interface.wasm.cs
@@ -11,11 +11,13 @@
using Microsoft.Extensions.Logging;
using Uno.Extensions;
using Uno.UI;
+using Uno.Disposables;
namespace Windows.UI.Xaml
{
public partial class FrameworkElement : UIElement, IFrameworkElement
{
+ private readonly SerialDisposable _backgroundSubscription = new SerialDisposable();
public T FindFirstParent() where T : class
{
var view = this.Parent;
@@ -114,8 +116,11 @@ public Brush Background
protected virtual void OnBackgroundChanged(DependencyPropertyChangedEventArgs e)
{
+ _backgroundSubscription.Disposable = null;
var brush = e.NewValue as Brush;
SetBackgroundBrush(brush);
+
+ _backgroundSubscription.Disposable = Brush.AssignAndObserveBrush(brush, _ => SetBackgroundBrush(brush));
}
private protected void SetBackgroundBrush(Brush brush)
diff --git a/src/Uno.UI/UI/Xaml/IShareableDependencyObject.cs b/src/Uno.UI/UI/Xaml/IShareableDependencyObject.cs
new file mode 100644
index 000000000000..ce52026c5fb0
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/IShareableDependencyObject.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Windows.UI.Xaml
+{
+ ///
+ /// A that supports association with multiple 'owners'.
+ ///
+ internal interface IShareableDependencyObject
+ {
+ bool IsClone { get; }
+ DependencyObject Clone();
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.Android.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.Android.cs
index 955e7a777a8f..51bff27a9540 100644
--- a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.Android.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.Android.cs
@@ -10,12 +10,12 @@
namespace Windows.UI.Xaml.Media.Animation
{
- internal static partial class AnimatorFactory
- {
+ internal static partial class AnimatorFactory
+ {
///
/// Creates the actual animator instance
///
- internal static IValueAnimator Create(Timeline timeline, double startingValue, double targetValue)
+ private static IValueAnimator CreateDouble(Timeline timeline, double startingValue, double targetValue)
{
if (timeline.GetIsDependantAnimation() || timeline.GetIsDurationZero())
{
@@ -27,6 +27,16 @@ internal static IValueAnimator Create(Timeline timeline, double startingValue, d
}
}
+ ///
+ /// Creates the actual animator instance
+ ///
+ private static IValueAnimator CreateColor(Timeline timeline, ColorOffset startingValue, ColorOffset targetValue)
+ {
+ // TODO: GPU-bound color animations - https://github.com/unoplatform/uno/issues/2947
+
+ return new NativeValueAnimatorAdapter(ValueAnimator.OfArgb((Android.Graphics.Color)(Color)startingValue, (Android.Graphics.Color)(Color)targetValue));
+ }
+
private static IValueAnimator GetGPUAnimator(this Timeline timeline, double startingValue, double targetValue)
{
// Overview : http://developer.android.com/guide/topics/graphics/prop-animation.html#property-vs-view
@@ -110,11 +120,11 @@ private static IValueAnimator GetGPUAnimator(this Timeline timeline, double star
case nameof(CompositeTransform.ScaleY):
return new NativeValueAnimatorAdapter(GetRelativeAnimator(composite.View, "scaleY", startingValue, targetValue), PrepareScaleY(composite, startingValue), Complete(composite));
- //case nameof(CompositeTransform.SkewX):
- // return ObjectAnimator.OfFloat(composite.View, "scaleX", ViewHelper.LogicalToPhysicalPixels(targetValue), startingValue);
+ //case nameof(CompositeTransform.SkewX):
+ // return ObjectAnimator.OfFloat(composite.View, "scaleX", ViewHelper.LogicalToPhysicalPixels(targetValue), startingValue);
- //case nameof(CompositeTransform.SkewY):
- // return ObjectAnimator.OfFloat(composite.View, "scaleY", ViewHelper.LogicalToPhysicalPixels(targetValue), startingValue);
+ //case nameof(CompositeTransform.SkewY):
+ // return ObjectAnimator.OfFloat(composite.View, "scaleY", ViewHelper.LogicalToPhysicalPixels(targetValue), startingValue);
}
}
@@ -203,7 +213,7 @@ private static void ResetPivot(View view)
// Apply transform using native values
OverridePivot(scale.View, scale.CenterX, scale.CenterY);
scale.View.ScaleX = (float)from;
- scale.View.ScaleY = (float) scale.ScaleY;
+ scale.View.ScaleY = (float)scale.ScaleY;
};
private static Action PrepareScaleY(ScaleTransform scale, double from) => () =>
@@ -252,8 +262,8 @@ private static void ResetPivot(View view)
OverridePivot(composite.View, composite.CenterX, composite.CenterY);
composite.View.TranslationX = ViewHelper.LogicalToPhysicalPixels(composite.TranslateX);
composite.View.TranslationY = ViewHelper.LogicalToPhysicalPixels(composite.TranslateY);
- composite.View.ScaleX = (float) composite.ScaleX;
- composite.View.ScaleY = (float) composite.ScaleY;
+ composite.View.ScaleX = (float)composite.ScaleX;
+ composite.View.ScaleY = (float)composite.ScaleY;
composite.View.Rotation = (float)from;
};
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.cs
new file mode 100644
index 000000000000..9217f3cbf2d6
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ partial class AnimatorFactory
+ {
+ ///
+ /// Creates the actual animator instance
+ ///
+ internal static IValueAnimator Create(Timeline timeline, T startingValue, T targetValue) where T : struct
+ {
+ if (startingValue is float startingFloat && targetValue is float targetFloat)
+ {
+ return CreateDouble(timeline, startingFloat, targetFloat);
+ }
+
+ if (startingValue is double startingDouble && targetValue is double targetDouble)
+ {
+ return CreateDouble(timeline, startingDouble, targetDouble);
+ }
+
+ if (startingValue is ColorOffset startingColor && targetValue is ColorOffset targetColor)
+ {
+ return CreateColor(timeline, startingColor, targetColor);
+ }
+
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.iOSmacOS.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.iOSmacOS.cs
index 952697c83871..b3eb75742ecf 100644
--- a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.iOSmacOS.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.iOSmacOS.cs
@@ -19,7 +19,7 @@ internal static partial class AnimatorFactory
///
/// Creates the actual animator instance
///
- internal static IValueAnimator Create(Timeline timeline, double startingValue, double targetValue)
+ private static IValueAnimator CreateDouble(Timeline timeline, double startingValue, double targetValue)
{
if (!timeline.GetIsHardwareAnimated())
{
@@ -31,5 +31,12 @@ internal static IValueAnimator Create(Timeline timeline, double startingValue, d
return new GPUFloatValueAnimator((float)startingValue, (float)targetValue, timeline.PropertyInfo.GetPathItems());
}
}
+
+ private static IValueAnimator CreateColor(Timeline timeline, ColorOffset startingValue, ColorOffset targetValue)
+ {
+ // TODO: GPU-bound color animations - https://github.com/unoplatform/uno/issues/2947
+
+ return new ColorValueAnimator(startingValue, targetValue);
+ }
}
}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.net.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.net.cs
index ab0e602a3dca..8d3b28214e9d 100644
--- a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.net.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.net.cs
@@ -12,7 +12,12 @@ internal static partial class AnimatorFactory
///
/// Creates the actual animator instance
///
- internal static IValueAnimator Create(Timeline timeline, double startingValue, double targetValue)
+ private static IValueAnimator CreateDouble(Timeline timeline, double startingValue, double targetValue)
+ {
+ return new NotSupportedAnimator();
+ }
+
+ private static IValueAnimator CreateColor(Timeline timeline, ColorOffset startingValue, ColorOffset targetValue)
{
return new NotSupportedAnimator();
}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.wasm.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.wasm.cs
index f4850e152714..12e78c42b5ed 100644
--- a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.wasm.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/AnimatorFactory.wasm.cs
@@ -12,9 +12,28 @@ internal static partial class AnimatorFactory
///
/// Creates the actual animator instance
///
- internal static IValueAnimator Create(Timeline timeline, double startingValue, double targetValue)
+ private static IValueAnimator CreateDouble(Timeline timeline, double startingValue, double targetValue)
{
+ if (timeline.GetIsDurationZero())
+ {
+ // Avoid unnecessary JS interop in the case of a zero-duration animation
+ return new ImmediateAnimator(startingValue, targetValue);
+ }
+
return new RenderingLoopFloatAnimator((float)startingValue, (float)targetValue);
}
+
+ private static IValueAnimator CreateColor(Timeline timeline, ColorOffset startingValue, ColorOffset targetValue)
+ {
+ if (timeline.GetIsDurationZero())
+ {
+ // Avoid unnecessary JS interop in the case of a zero-duration animation
+ return new ImmediateAnimator(startingValue, targetValue);
+ }
+
+ // TODO: GPU-bound color animations - https://github.com/unoplatform/uno/issues/2947
+
+ return new RenderingLoopColorAnimator(startingValue, targetValue);
+ }
}
}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/CPUBoundFloatAnimator.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/CPUBoundAnimator.cs
similarity index 90%
rename from src/Uno.UI/UI/Xaml/Media/Animation/Animators/CPUBoundFloatAnimator.cs
rename to src/Uno.UI/UI/Xaml/Media/Animation/Animators/CPUBoundAnimator.cs
index 0a493bff172c..6f7b0650f39b 100644
--- a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/CPUBoundFloatAnimator.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/CPUBoundAnimator.cs
@@ -5,18 +5,18 @@
namespace Windows.UI.Xaml.Media.Animation
{
- internal abstract class CPUBoundFloatAnimator : IValueAnimator
- {
- private readonly float _from;
- private readonly float _to;
+ internal abstract class CPUBoundAnimator : IValueAnimator where T : struct
+ {
+ private readonly T _from;
+ private readonly T _to;
private readonly Stopwatch _elapsed;
- private float _currentValue;
- private IEasingFunction _easing = LinearEase.Instance;
+ private T _currentValue;
+ protected IEasingFunction _easing = LinearEase.Instance;
private bool _isDisposed;
private bool _isDelaying;
- protected CPUBoundFloatAnimator(float from, float to)
+ protected CPUBoundAnimator(T from, T to)
{
_from = from;
_to = to;
@@ -165,7 +165,7 @@ protected void OnFrame(object sender, object e)
}
var frame = elapsed - StartDelay;
- var value = (float)_easing.Ease(frame, _from, _to, Duration);
+ var value = GetUpdatedValue(frame, _from, _to);
CurrentPlayTime = elapsed;
_currentValue = value;
@@ -174,6 +174,8 @@ protected void OnFrame(object sender, object e)
}
}
+ protected abstract T GetUpdatedValue(long frame, T from, T to);
+
private void ConfigureStartInterval(long elapsed)
{
if (StartDelay > 0)
@@ -197,7 +199,7 @@ private void CheckDisposed()
{
if (_isDisposed)
{
- throw new ObjectDisposedException(nameof(CPUBoundFloatAnimator));
+ throw new ObjectDisposedException(nameof(CPUBoundAnimator));
}
}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/ColorValueAnimator.iOSmacOS.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/ColorValueAnimator.iOSmacOS.cs
new file mode 100644
index 000000000000..711930f8e50d
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/ColorValueAnimator.iOSmacOS.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ internal class ColorValueAnimator : DisplayLinkValueAnimator
+ {
+ private readonly ColorOffset _from;
+ private readonly ColorOffset _to;
+
+ private ColorOffset[] _animatedValues;
+
+ public ColorValueAnimator(ColorOffset from, ColorOffset to)
+ {
+ _to = to;
+ _from = from;
+ }
+
+ protected override void PrebuildFrames(long numberOfFrames)
+ {
+ if (_animatedValues != null)
+ {
+ return;
+ }
+
+ _animatedValues = new ColorOffset[numberOfFrames];
+
+ var by = _to - _from;
+ var interpolation = (1f / numberOfFrames) * by;
+
+ for (int f = 0; f < numberOfFrames; f++)
+ {
+ // TODO: apply easing, if any - https://github.com/unoplatform/uno/issues/2948
+
+ _animatedValues[f] = _from + (f * interpolation);//frame value
+ }
+ }
+
+ protected override object GetFinalUpdate() => _to;
+
+ protected override object GetUpdate(int frame) => _animatedValues[frame];
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/DispatcherFloatAnimator.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/DispatcherFloatAnimator.cs
index 442ba382a9cf..0bc0e144669a 100644
--- a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/DispatcherFloatAnimator.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/DispatcherFloatAnimator.cs
@@ -3,7 +3,7 @@
namespace Windows.UI.Xaml.Media.Animation
{
- internal sealed class DispatcherFloatAnimator : CPUBoundFloatAnimator
+ internal sealed class DispatcherFloatAnimator : CPUBoundAnimator
{
public const int DefaultFrameRate = 30;
@@ -23,5 +23,7 @@ public DispatcherFloatAnimator(float from, float to, int frameRate = DefaultFram
protected override void SetStartFrameDelay(long delayMs) => _timer.Interval = TimeSpan.FromMilliseconds(delayMs);
protected override void SetAnimationFramesInterval() =>_timer.Interval = TimeSpan.FromSeconds(1d / _frameRate);
+
+ protected override float GetUpdatedValue(long frame, float from, float to) => (float)_easing.Ease(frame, from, to, Duration);
}
-}
\ No newline at end of file
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/DisplayLinkValueAnimator.iOSmacOS.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/DisplayLinkValueAnimator.iOSmacOS.cs
new file mode 100644
index 000000000000..6343c1e673d5
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/DisplayLinkValueAnimator.iOSmacOS.cs
@@ -0,0 +1,281 @@
+using CoreAnimation;
+using Foundation;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ ///
+ /// Animates a property using Xaml property setters.
+ ///
+ internal abstract class DisplayLinkValueAnimator : IValueAnimator
+ {
+ private const double MillisecondsPerSecond = 1000d;
+ private const long MaxNumberOfFrames = 10000;// 10,000 frames is enough 40kb of floats
+ private const long MinNumberOfFrames = 1;// At least 1 frame please
+
+ private long _duration;
+
+ private const int FrameRate = 50;
+
+ private long _numberOfFrames;
+#if __IOS__
+ private CADisplayLink _displayLink;
+#else
+ private NSTimer _timer;
+#endif
+
+ private double _startTime = 0;
+
+ private bool _isDisposed;
+ private bool _isAttachedToLooper;
+
+ protected IEasingFunction _easingFunction = null;
+
+ public void Start()
+ {
+ if (_isDisposed)
+ {
+ throw new ObjectDisposedException(GetType().Name);
+ }
+
+ PrebuildFrames();//Preload as much of the animation as possible
+
+ IsRunning = true;
+
+ _startTime = 0;// Start time will be set if it is equal to zero
+
+ if (!_isAttachedToLooper)
+ {
+#if __IOS__
+ //http://www.bigspaceship.com/ios-animation-intervals/
+ _displayLink = CADisplayLink.Create(OnFrame);//OnFrame is called when an animation frame is ready
+
+ //Need to attach the _displayLink to the MainLoop (uiThread) so that the call back will be called on the UI thread
+ //Default == normal UI updates
+ _displayLink.AddToRunLoop(NSRunLoop.Main, NSRunLoopMode.Default);
+ //UITracking == updates during scrolling
+ _displayLink.AddToRunLoop(NSRunLoop.Main, NSRunLoopMode.UITracking);
+#else
+ ScheduleTimer();
+#endif
+ _isAttachedToLooper = true;
+ }
+ }
+
+#if __MACOS__
+ private void ScheduleTimer()
+ {
+ if (_timer?.IsValid == true) return;
+ UnscheduleTimer();
+ _timer = NSTimer.CreateRepeatingScheduledTimer(0.001, OnTimerTick);
+ }
+
+ private void UnscheduleTimer()
+ {
+ _timer?.Invalidate();
+ _timer?.Dispose();
+ _timer = null;
+ }
+
+ private void OnTimerTick(NSTimer obj)
+ {
+ OnFrame();
+ }
+#endif
+
+ ///
+ /// Precalculates the frame values.
+ ///
+ private void PrebuildFrames()
+ {
+ var frames = (FrameRate * _duration) / MillisecondsPerSecond;
+
+ _numberOfFrames = (int)Math.Max(
+ Math.Min(frames, MaxNumberOfFrames),
+ MinNumberOfFrames);
+
+ PrebuildFrames(_numberOfFrames);
+ }
+
+ protected abstract void PrebuildFrames(long numberOfFrames);
+
+ ///
+ /// When a frame is free update the value
+ ///
+ private void OnFrame()
+ {
+ if (_isDisposed || !IsRunning)
+ {
+ return;
+ }
+#if __IOS__
+ var currentTime = _displayLink.Timestamp; // current time in seconds
+#else
+ var currentTime = NSProcessInfo.ProcessInfo.SystemUptime;
+#endif
+
+ if (_startTime == 0)
+ { // if start time is not set, set it
+ _startTime = currentTime - (CurrentPlayTime / MillisecondsPerSecond); // Remove current play time as an offset (lie: tell the animation it started x seconds ago)
+ }
+
+ var delta = ((currentTime - _startTime) * MillisecondsPerSecond) - StartDelay; // Remove the start Delay (the delta may be negative)
+
+ if (delta >= _duration)
+ {//animation is done
+
+ SendUpdate(GetFinalUpdate());//Set the final value
+
+ if (AnimationEnd != null)
+ {
+ AnimationEnd(this, EventArgs.Empty); //Send an animation ended
+ }
+
+ Stop();
+ return;
+ }
+
+ if (delta < 0)
+ { // Start Delay can cause this - wait till the start delay is spent before continuing
+ return;
+ }
+
+ CurrentPlayTime = (long)delta; //Update play time
+
+ var percent = delta / _duration;
+ var frame = (int)(percent * _numberOfFrames); // get the appropriate frame
+
+ SendUpdate(GetUpdate(frame)); // send an update
+ }
+
+ private void SendUpdate(object value)
+ {
+ AnimatedValue = value;
+ Update?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected abstract object GetUpdate(int frame);
+
+ protected abstract object GetFinalUpdate();
+
+ public void Resume()
+ {
+ Start();
+ }
+
+ public void Pause()
+ {
+ IsRunning = false;
+
+ AnimationPause?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void Cancel()
+ {
+ ReleaseDisplayLink();
+
+ IsRunning = false;
+ AnimationCancel?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ /// Stop this instance.
+ ///
+ private void Stop()
+ {
+ ReleaseDisplayLink();
+
+ IsRunning = false;
+ }
+
+ private void ReleaseDisplayLink()
+ {
+ if (_isAttachedToLooper)
+ {
+#if __IOS__
+ //Detach the _displayLink to the MainLoop (uiThread).
+ _displayLink?.RemoveFromRunLoop(NSRunLoop.Main, NSRunLoop.NSDefaultRunLoopMode);//detaches from the UI thread
+ _displayLink?.RemoveFromRunLoop(NSRunLoop.Main, NSRunLoop.UITrackingRunLoopMode);
+ _displayLink = null;
+#endif
+ _isAttachedToLooper = false;
+ }
+ }
+
+ public void SetDuration(long duration)
+ {
+ _duration = duration;
+ }
+
+ public void SetEasingFunction(IEasingFunction easingFunction)
+ {
+ _easingFunction = easingFunction;
+ }
+
+ public event EventHandler AnimationEnd;
+
+ public event EventHandler AnimationPause;
+
+ public event EventHandler AnimationCancel;
+
+ public event EventHandler Update;
+
+ public long StartDelay { get; set; }
+
+#if __IOS__
+ public bool IsRunning
+ {
+ get { return !(_displayLink?.Paused ?? false); }
+ private set
+ {
+ if (_displayLink != null)
+ {
+ _displayLink.Paused = !value;
+ }
+ }
+ }
+#else
+ public bool IsRunning
+ {
+ get => _timer?.IsValid ?? false;
+ private set
+ {
+ if (_timer != null)
+ {
+ if (value && !_timer.IsValid)
+ {
+ ScheduleTimer();
+ }
+ else
+ {
+ UnscheduleTimer();
+ }
+ }
+ }
+ }
+#endif
+
+ public long CurrentPlayTime { get; set; }
+
+ public object AnimatedValue { get; private set; }
+
+ public long Duration => _duration;
+
+ public void Dispose()
+ {
+ _isDisposed = true;
+ Update = null;
+ AnimationEnd = null;
+ AnimationCancel = null;
+
+ Stop();
+#if __IOS__
+ _displayLink?.Dispose();
+#else
+ _timer?.Dispose();
+#endif
+ }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/FloatValueAnimator.iOSmacOS.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/FloatValueAnimator.iOSmacOS.cs
index 33bf26a0e607..eb9000a524b4 100644
--- a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/FloatValueAnimator.iOSmacOS.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/FloatValueAnimator.iOSmacOS.cs
@@ -9,119 +9,41 @@ namespace Windows.UI.Xaml.Media.Animation
///
/// Animates a float property using Xaml property setters.
///
- internal class FloatValueAnimator : IValueAnimator
+ internal class FloatValueAnimator : DisplayLinkValueAnimator
{
- private const double MillisecondsPerSecond = 1000d;
- private const long MaxNumberOfFrames = 10000;// 10,000 frames is enough 40kb of floats
- private const long MinNumberOfFrames = 1;// At least 1 frame please
-
private float _from;
private float _to;
- private long _duration;
private float[] _animatedValues;
- private const int FrameRate = 50;
-
- private long _numberOfFrames;
-#if __IOS__
- private CADisplayLink _displayLink;
-#else
- private NSTimer _timer;
-#endif
-
- private double _startTime = 0;
-
- private bool _isDisposed;
- private bool _isAttachedToLooper;
-
- private IEasingFunction _easingFunction = null;
public FloatValueAnimator(float from, float to)
{
_to = to;
_from = from;
-
- StartDelay = 0;
- CurrentPlayTime = 0;
}
- public void Start()
- {
- if (_isDisposed)
- {
- throw new ObjectDisposedException(GetType().Name);
- }
-
- if (_animatedValues == null)
- {
- PrebuildFrames();//Preload as much of the animation as possible
- }
-
- IsRunning = true;
+ protected override object GetFinalUpdate() => _to;
- _startTime = 0;// Start time will be set if it is equal to zero
+ protected override object GetUpdate(int frame) => _animatedValues[frame];
- if (!_isAttachedToLooper)
+ protected override void PrebuildFrames(long numberOfFrames)
+ {
+ if (_animatedValues != null)
{
-#if __IOS__
- //http://www.bigspaceship.com/ios-animation-intervals/
- _displayLink = CADisplayLink.Create(OnFrame);//OnFrame is called when an animation frame is ready
-
- //Need to attach the _displayLink to the MainLoop (uiThread) so that the call back will be called on the UI thread
- //Default == normal UI updates
- _displayLink.AddToRunLoop(NSRunLoop.Main, NSRunLoopMode.Default);
- //UITracking == updates during scrolling
- _displayLink.AddToRunLoop(NSRunLoop.Main, NSRunLoopMode.UITracking);
-#else
- ScheduleTimer();
-#endif
- _isAttachedToLooper = true;
+ return;
}
- }
-#if __MACOS__
- private void ScheduleTimer()
- {
- if (_timer?.IsValid == true) return;
- UnscheduleTimer();
- _timer = NSTimer.CreateRepeatingScheduledTimer(0.001, OnTimerTick);
- }
-
- private void UnscheduleTimer()
- {
- _timer?.Invalidate();
- _timer?.Dispose();
- _timer = null;
- }
-
- private void OnTimerTick(NSTimer obj)
- {
- OnFrame();
- }
-#endif
-
- ///
- /// Precalculates the frame values.
- ///
- private void PrebuildFrames()
- {
- var frames = (FrameRate * _duration) / MillisecondsPerSecond;
-
- _numberOfFrames = (int)Math.Max(
- Math.Min(frames, MaxNumberOfFrames),
- MinNumberOfFrames);
-
- _animatedValues = new float[_numberOfFrames];
+ _animatedValues = new float[numberOfFrames];
var by = _to - _from; //how much to change the value
- var interpolation = by / _numberOfFrames; //step size
+ var interpolation = by / numberOfFrames; //step size
- for (int f = 0; f < _numberOfFrames; f++)
+ for (int f = 0; f < numberOfFrames; f++)
{
//Modifies the frame values of the animation depending on the easing function
if (_easingFunction != null)
{
- _animatedValues[f] = (float)_easingFunction.Ease(f, _from, _to, _numberOfFrames);//frame value
+ _animatedValues[f] = (float)_easingFunction.Ease(f, _from, _to, numberOfFrames);//frame value
}
else
{
@@ -130,178 +52,5 @@ private void PrebuildFrames()
}
}
}
-
- ///
- /// When a frame is free update the value
- ///
- private void OnFrame()
- {
- if (_isDisposed || !IsRunning)
- {
- return;
- }
-#if __IOS__
- var currentTime = _displayLink.Timestamp; // current time in seconds
-#else
- var currentTime = NSProcessInfo.ProcessInfo.SystemUptime;
-#endif
-
- if (_startTime == 0)
- { // if start time is not set, set it
- _startTime = currentTime - (CurrentPlayTime / MillisecondsPerSecond); // Remove current play time as an offset (lie: tell the animation it started x seconds ago)
- }
-
- var delta = ((currentTime - _startTime) * MillisecondsPerSecond) - StartDelay; // Remove the start Delay (the delta may be negative)
-
- if (delta >= _duration)
- {//animation is done
-
- SendUpdate(_to);//Set the final value
-
- if (AnimationEnd != null)
- {
- AnimationEnd(this, EventArgs.Empty); //Send an animation ended
- }
-
- Stop();
- return;
- }
-
- if (delta < 0)
- { // Start Delay can cause this - wait till the start delay is spent before continuing
- return;
- }
-
- CurrentPlayTime = (long)delta; //Update play time
-
- var percent = delta / _duration;
- var frame = (int)(percent * _numberOfFrames); // get the appropriate frame
-
- SendUpdate(_animatedValues[frame]); // send an update
- }
-
- private void SendUpdate(float value)
- {
- AnimatedValue = value;
- Update?.Invoke(this, EventArgs.Empty);
- }
-
- public void Resume()
- {
- Start();
- }
-
- public void Pause()
- {
- IsRunning = false;
-
- AnimationPause?.Invoke(this, EventArgs.Empty);
- }
-
- public void Cancel()
- {
- ReleaseDisplayLink();
-
- IsRunning = false;
- AnimationCancel?.Invoke(this, EventArgs.Empty);
- }
-
- ///
- /// Stop this instance.
- ///
- private void Stop()
- {
- ReleaseDisplayLink();
-
- IsRunning = false;
- }
-
- private void ReleaseDisplayLink()
- {
- if (_isAttachedToLooper)
- {
-#if __IOS__
- //Detach the _displayLink to the MainLoop (uiThread).
- _displayLink?.RemoveFromRunLoop(NSRunLoop.Main, NSRunLoop.NSDefaultRunLoopMode);//detaches from the UI thread
- _displayLink?.RemoveFromRunLoop(NSRunLoop.Main, NSRunLoop.UITrackingRunLoopMode);
- _displayLink = null;
-#endif
- _isAttachedToLooper = false;
- }
- }
-
- public void SetDuration(long duration)
- {
- _duration = duration;
- }
-
- public void SetEasingFunction(IEasingFunction easingFunction)
- {
- _easingFunction = easingFunction;
- }
-
- public event EventHandler AnimationEnd;
-
- public event EventHandler AnimationPause;
-
- public event EventHandler AnimationCancel;
-
- public event EventHandler Update;
-
- public long StartDelay { get; set; }
-
-#if __IOS__
- public bool IsRunning
- {
- get { return !(_displayLink?.Paused ?? false); }
- private set
- {
- if (_displayLink != null)
- {
- _displayLink.Paused = !value;
- }
- }
- }
-#else
- public bool IsRunning
- {
- get => _timer?.IsValid ?? false;
- private set
- {
- if (_timer != null)
- {
- if (value && !_timer.IsValid)
- {
- ScheduleTimer();
- }
- else
- {
- UnscheduleTimer();
- }
- }
- }
- }
-#endif
-
- public long CurrentPlayTime { get; set; }
-
- public object AnimatedValue { get; private set; }
-
- public long Duration => _duration;
-
- public void Dispose()
- {
- _isDisposed = true;
- Update = null;
- AnimationEnd = null;
- AnimationCancel = null;
-
- Stop();
-#if __IOS__
- _displayLink?.Dispose();
-#else
- _timer?.Dispose();
-#endif
- }
}
-}
\ No newline at end of file
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/ImmediateAnimator.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/ImmediateAnimator.cs
index b1c2aff62be4..12d41c0bf887 100644
--- a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/ImmediateAnimator.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/ImmediateAnimator.cs
@@ -4,13 +4,13 @@
namespace Windows.UI.Xaml.Media.Animation
{
- internal sealed class ImmediateAnimator : IValueAnimator, IDisposable
+ internal sealed class ImmediateAnimator : IValueAnimator, IDisposable where T : struct
{
- private float _from;
- private float _to;
+ private T _from;
+ private T _to;
private long _duration;
- public ImmediateAnimator(float from, float to)
+ public ImmediateAnimator(T from, T to)
{
_to = to;
_from = from;
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopAnimator.wasm.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopAnimator.wasm.cs
new file mode 100644
index 000000000000..a420775366bb
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopAnimator.wasm.cs
@@ -0,0 +1,200 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using Uno;
+using Uno.Extensions;
+using Uno.Foundation;
+using Uno.Foundation.Interop;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ internal abstract class RenderingLoopAnimator : CPUBoundAnimator, IJSObject where T : struct
+ {
+ protected RenderingLoopAnimator(T from, T to)
+ : base(from, to)
+ {
+ Handle = JSObjectHandle.Create(this, Metadata.Instance);
+ }
+
+ public JSObjectHandle Handle { get; }
+
+
+ protected override void EnableFrameReporting() => WebAssemblyRuntime.InvokeJSWithInterop($"{this}.EnableFrameReporting();");
+
+ protected override void DisableFrameReporting() => WebAssemblyRuntime.InvokeJSWithInterop($"{this}.DisableFrameReporting();");
+
+ protected override void SetStartFrameDelay(long delayMs) => WebAssemblyRuntime.InvokeJSWithInterop($"{this}.SetStartFrameDelay({delayMs});");
+
+ protected override void SetAnimationFramesInterval() => WebAssemblyRuntime.InvokeJSWithInterop($"{this}.SetAnimationFramesInterval();");
+
+ private void OnFrame() => OnFrame(null, null);
+
+ private class Metadata : IJSObjectMetadata
+ {
+ public static Metadata Instance {get;} = new Metadata();
+ private Metadata() { }
+
+ private static long _handles = 0L;
+ private bool _isPrototypeExported;
+
+ ///
+ public long CreateNativeInstance(IntPtr managedHandle)
+ {
+ if (!_isPrototypeExported)
+ {
+ // Makes type visible to javascript
+ WebAssemblyRuntime.InvokeJS(_prototype);
+ _isPrototypeExported = true;
+ }
+
+ var id = Interlocked.Increment(ref _handles);
+ WebAssemblyRuntime.InvokeJS($"Windows.UI.Xaml.Media.Animation.RenderingLoopFloatAnimator.createInstance(\"{managedHandle}\", \"{id}\")");
+
+ return id;
+ }
+
+ ///
+ public string GetNativeInstance(IntPtr managedHandle, long jsHandle)
+ => $"Windows.UI.Xaml.Media.Animation.RenderingLoopFloatAnimator.getInstance(\"{managedHandle}\", \"{jsHandle}\")";
+
+ ///
+ public void DestroyNativeInstance(IntPtr managedHandle, long jsHandle)
+ => WebAssemblyRuntime.InvokeJS($"Windows.UI.Xaml.Media.Animation.RenderingLoopFloatAnimator.destroyInstance(\"{managedHandle}\", \"{jsHandle}\")");
+
+ ///
+ public object InvokeManaged(object instance, string method, string parameters)
+ {
+ switch (method)
+ {
+ case "OnFrame":
+ ((RenderingLoopAnimator)instance).OnFrame();
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(method));
+ }
+
+ return null;
+ }
+
+ // Note: This should be written in TypeScript and embedded in the package.
+ private const string _prototype = @"(function() {
+ var Windows = window.Windows;
+ (function (Windows) {
+ var UI = window.UI;
+ (function (UI) {
+ var Xaml = window.Xaml;
+ (function (Xaml) {
+ var Media = window.Media;
+ (function (Media) {
+ var Animation = window.Animation;
+ (function (Animation) {
+ var RenderingLoopFloatAnimator = (function() {
+
+ RenderingLoopFloatAnimator.activeInstances = {};
+
+ RenderingLoopFloatAnimator.createInstance = function(managedId, jsId) {
+ this.activeInstances[jsId] = new RenderingLoopFloatAnimator(managedId);
+
+ return ""ok"";
+ }
+
+ RenderingLoopFloatAnimator.getInstance = function(managedId, jsId) {
+ return this.activeInstances[jsId];
+ }
+
+ RenderingLoopFloatAnimator.destroyInstance = function(managedId, jsId) {
+ delete this.activeInstances[jsId];
+
+ return ""ok"";
+ }
+
+ function RenderingLoopFloatAnimator(managedHandle) {
+ this.__managedHandle = managedHandle;
+ };
+
+ RenderingLoopFloatAnimator.prototype.SetStartFrameDelay = function(delay) {
+ this.unscheduleFrame();
+
+ if (this._isEnabled) {
+ this.scheduleDelayedFrame(delay);
+ }
+ };
+
+ RenderingLoopFloatAnimator.prototype.SetAnimationFramesInterval = function() {
+ this.unscheduleFrame();
+
+ if (this._isEnabled) {
+ this.onFrame();
+ }
+ };
+
+ RenderingLoopFloatAnimator.prototype.EnableFrameReporting = function() {
+ if (this._isEnabled) {
+ return;
+ }
+
+ this._isEnabled = true;
+ this.scheduleAnimationFrame();
+ };
+
+ RenderingLoopFloatAnimator.prototype.DisableFrameReporting = function() {
+ this._isEnabled = false;
+ this.unscheduleFrame();
+ };
+
+ RenderingLoopFloatAnimator.prototype.onFrame = function(timestamp) {
+ Uno.Foundation.Interop.ManagedObject.dispatch(this.__managedHandle, ""OnFrame"");
+
+ // Schedule a new frame only if still enabled and no frame was scheduled by the managed OnFrame
+ if (this._isEnabled && this._frameRequestId == null && this._delayRequestId == null) {
+ this.scheduleAnimationFrame();
+ }
+ };
+
+ RenderingLoopFloatAnimator.prototype.unscheduleFrame = function(timestamp) {
+ if (this._delayRequestId != null) {
+ clearTimeout(this._delayRequestId);
+ this._delayRequestId = null;
+ }
+ if (this._frameRequestId != null) {
+ window.cancelAnimationFrame(this._frameRequestId);
+ this._frameRequestId = null;
+ }
+ };
+
+ RenderingLoopFloatAnimator.prototype.scheduleDelayedFrame = function(delay) {
+ var that = this;
+ this._delayRequestId = setTimeout(function() {
+ that._delayRequestId = null;
+ that.onFrame();
+ },
+ delay);
+ };
+
+
+ RenderingLoopFloatAnimator.prototype.scheduleAnimationFrame = function() {
+ var that = this;
+ this._frameRequestId = window.requestAnimationFrame(function(ts) {
+ that._frameRequestId = null;
+ that.onFrame(ts);
+ });
+ };
+
+ return RenderingLoopFloatAnimator;
+
+ }());
+
+ Animation.RenderingLoopFloatAnimator = RenderingLoopFloatAnimator;
+ })(Animation = Media.Animation || (Media.Animation = {}));
+ })(Media = Xaml.Media || (Xaml.Media = {}));
+ })(Xaml = UI.Xaml || (UI.Xaml = {}));
+ })(UI = Windows.UI || (Windows.UI = {}));
+ })(Windows || (Windows = {}));
+window.Windows = Windows;
+
+return ""ok"";})();";
+ }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopColorAnimator.wasm.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopColorAnimator.wasm.cs
new file mode 100644
index 000000000000..d3450058ffcf
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopColorAnimator.wasm.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ internal sealed class RenderingLoopColorAnimator : RenderingLoopAnimator
+ {
+ public RenderingLoopColorAnimator(ColorOffset from, ColorOffset to) : base(from, to)
+ {
+ }
+
+ protected override ColorOffset GetUpdatedValue(long frame, ColorOffset from, ColorOffset to)
+ {
+ // TODO: apply easing, if any - https://github.com/unoplatform/uno/issues/2948
+
+ var by = to - from;
+ var currentFrame = (float)frame / Duration;
+ var currentOffset = currentFrame * by;
+
+ return from + currentOffset;
+ }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopFloatAnimator.wasm.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopFloatAnimator.wasm.cs
index 83bfe704931b..0d8a89f31914 100644
--- a/src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopFloatAnimator.wasm.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Animators/RenderingLoopFloatAnimator.wasm.cs
@@ -9,192 +9,12 @@
namespace Windows.UI.Xaml.Media.Animation
{
- internal sealed class RenderingLoopFloatAnimator : CPUBoundFloatAnimator, IJSObject
+ internal sealed class RenderingLoopFloatAnimator : RenderingLoopAnimator
{
- public RenderingLoopFloatAnimator(float from, float to)
- : base(from, to)
+ public RenderingLoopFloatAnimator(float from, float to) : base(from, to)
{
- Handle = JSObjectHandle.Create(this, Metadata.Instance);
}
- public JSObjectHandle Handle { get; }
-
-
- protected override void EnableFrameReporting() => WebAssemblyRuntime.InvokeJSWithInterop($"{this}.EnableFrameReporting();");
-
- protected override void DisableFrameReporting() => WebAssemblyRuntime.InvokeJSWithInterop($"{this}.DisableFrameReporting();");
-
- protected override void SetStartFrameDelay(long delayMs) => WebAssemblyRuntime.InvokeJSWithInterop($"{this}.SetStartFrameDelay({delayMs});");
-
- protected override void SetAnimationFramesInterval() => WebAssemblyRuntime.InvokeJSWithInterop($"{this}.SetAnimationFramesInterval();");
-
- private void OnFrame() => OnFrame(null, null);
-
- private class Metadata : IJSObjectMetadata
- {
- public static Metadata Instance {get;} = new Metadata();
- private Metadata() { }
-
- private static long _handles = 0L;
- private bool _isPrototypeExported;
-
- ///
- public long CreateNativeInstance(IntPtr managedHandle)
- {
- if (!_isPrototypeExported)
- {
- // Makes type visible to javascript
- WebAssemblyRuntime.InvokeJS(_prototype);
- _isPrototypeExported = true;
- }
-
- var id = Interlocked.Increment(ref _handles);
- WebAssemblyRuntime.InvokeJS($"Windows.UI.Xaml.Media.Animation.RenderingLoopFloatAnimator.createInstance(\"{managedHandle}\", \"{id}\")");
-
- return id;
- }
-
- ///
- public string GetNativeInstance(IntPtr managedHandle, long jsHandle)
- => $"Windows.UI.Xaml.Media.Animation.RenderingLoopFloatAnimator.getInstance(\"{managedHandle}\", \"{jsHandle}\")";
-
- ///
- public void DestroyNativeInstance(IntPtr managedHandle, long jsHandle)
- => WebAssemblyRuntime.InvokeJS($"Windows.UI.Xaml.Media.Animation.RenderingLoopFloatAnimator.destroyInstance(\"{managedHandle}\", \"{jsHandle}\")");
-
- ///
- public object InvokeManaged(object instance, string method, string parameters)
- {
- switch (method)
- {
- case "OnFrame":
- ((RenderingLoopFloatAnimator)instance).OnFrame();
- break;
-
- default:
- throw new ArgumentOutOfRangeException(nameof(method));
- }
-
- return null;
- }
-
- // Note: This should be written in TypeScript and embedded in the package.
- private const string _prototype = @"(function() {
- var Windows = window.Windows;
- (function (Windows) {
- var UI = window.UI;
- (function (UI) {
- var Xaml = window.Xaml;
- (function (Xaml) {
- var Media = window.Media;
- (function (Media) {
- var Animation = window.Animation;
- (function (Animation) {
- var RenderingLoopFloatAnimator = (function() {
-
- RenderingLoopFloatAnimator.activeInstances = {};
-
- RenderingLoopFloatAnimator.createInstance = function(managedId, jsId) {
- this.activeInstances[jsId] = new RenderingLoopFloatAnimator(managedId);
-
- return ""ok"";
- }
-
- RenderingLoopFloatAnimator.getInstance = function(managedId, jsId) {
- return this.activeInstances[jsId];
- }
-
- RenderingLoopFloatAnimator.destroyInstance = function(managedId, jsId) {
- delete this.activeInstances[jsId];
-
- return ""ok"";
- }
-
- function RenderingLoopFloatAnimator(managedHandle) {
- this.__managedHandle = managedHandle;
- };
-
- RenderingLoopFloatAnimator.prototype.SetStartFrameDelay = function(delay) {
- this.unscheduleFrame();
-
- if (this._isEnabled) {
- this.scheduleDelayedFrame(delay);
- }
- };
-
- RenderingLoopFloatAnimator.prototype.SetAnimationFramesInterval = function() {
- this.unscheduleFrame();
-
- if (this._isEnabled) {
- this.onFrame();
- }
- };
-
- RenderingLoopFloatAnimator.prototype.EnableFrameReporting = function() {
- if (this._isEnabled) {
- return;
- }
-
- this._isEnabled = true;
- this.scheduleAnimationFrame();
- };
-
- RenderingLoopFloatAnimator.prototype.DisableFrameReporting = function() {
- this._isEnabled = false;
- this.unscheduleFrame();
- };
-
- RenderingLoopFloatAnimator.prototype.onFrame = function(timestamp) {
- Uno.Foundation.Interop.ManagedObject.dispatch(this.__managedHandle, ""OnFrame"");
-
- // Schedule a new frame only if still enabled and no frame was scheduled by the managed OnFrame
- if (this._isEnabled && this._frameRequestId == null && this._delayRequestId == null) {
- this.scheduleAnimationFrame();
- }
- };
-
- RenderingLoopFloatAnimator.prototype.unscheduleFrame = function(timestamp) {
- if (this._delayRequestId != null) {
- clearTimeout(this._delayRequestId);
- this._delayRequestId = null;
- }
- if (this._frameRequestId != null) {
- window.cancelAnimationFrame(this._frameRequestId);
- this._frameRequestId = null;
- }
- };
-
- RenderingLoopFloatAnimator.prototype.scheduleDelayedFrame = function(delay) {
- var that = this;
- this._delayRequestId = setTimeout(function() {
- that._delayRequestId = null;
- that.onFrame();
- },
- delay);
- };
-
-
- RenderingLoopFloatAnimator.prototype.scheduleAnimationFrame = function() {
- var that = this;
- this._frameRequestId = window.requestAnimationFrame(function(ts) {
- that._frameRequestId = null;
- that.onFrame(ts);
- });
- };
-
- return RenderingLoopFloatAnimator;
-
- }());
-
- Animation.RenderingLoopFloatAnimator = RenderingLoopFloatAnimator;
- })(Animation = Media.Animation || (Media.Animation = {}));
- })(Media = Xaml.Media || (Xaml.Media = {}));
- })(Xaml = UI.Xaml || (UI.Xaml = {}));
- })(UI = Windows.UI || (Windows.UI = {}));
- })(Windows || (Windows = {}));
-window.Windows = Windows;
-
-return ""ok"";})();";
- }
+ protected override float GetUpdatedValue(long frame, float from, float to) => (float)_easing.Ease(frame, from, to, Duration);
}
}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/ColorAnimation.cs b/src/Uno.UI/UI/Xaml/Media/Animation/ColorAnimation.cs
new file mode 100644
index 000000000000..c1bc1eefd6ec
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/ColorAnimation.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Uno;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ public partial class ColorAnimation : Timeline, ITimeline, IAnimation
+ {
+ private readonly AnimationImplementation _animationImplementation;
+
+ public ColorAnimation()
+ {
+ _animationImplementation = new AnimationImplementation(this);
+ }
+
+ public Color? To
+ {
+ get => (Color?)this.GetValue(ToProperty);
+ set
+ {
+ this.SetValue(ToProperty, value);
+ }
+ }
+
+ public Color? From
+ {
+ get
+ {
+ return (Color?)this.GetValue(FromProperty);
+ }
+ set
+ {
+ this.SetValue(FromProperty, value);
+ }
+ }
+
+ public bool EnableDependentAnimation
+ {
+ get
+ {
+ return (bool)this.GetValue(EnableDependentAnimationProperty);
+ }
+ set
+ {
+ this.SetValue(EnableDependentAnimationProperty, value);
+ }
+ }
+
+ bool IAnimation.EnableDependentAnimation => EnableDependentAnimation;
+
+ public Color? By
+ {
+ get
+ {
+ return (Color?)this.GetValue(ByProperty);
+ }
+ set
+ {
+ this.SetValue(ByProperty, value);
+ }
+ }
+
+ public static DependencyProperty ByProperty { get; } =
+ DependencyProperty.Register(
+ "By", typeof(Color?),
+ typeof(ColorAnimation),
+ new FrameworkPropertyMetadata(default(Color?)));
+
+ public static DependencyProperty EnableDependentAnimationProperty { get; } =
+ DependencyProperty.Register(
+ "EnableDependentAnimation", typeof(bool),
+ typeof(ColorAnimation),
+ new FrameworkPropertyMetadata(default(bool)));
+
+ public static DependencyProperty FromProperty { get; } =
+ DependencyProperty.Register(
+ "From", typeof(Color?),
+ typeof(ColorAnimation),
+ new FrameworkPropertyMetadata(default(Color?)));
+
+ public static DependencyProperty ToProperty { get; } =
+ DependencyProperty.Register(
+ "To", typeof(Color?),
+ typeof(ColorAnimation),
+ new FrameworkPropertyMetadata(default(Color?)));
+
+ ColorOffset? IAnimation.To => (ColorOffset?)To;
+
+ ColorOffset? IAnimation.From => (ColorOffset?)From;
+
+ ColorOffset? IAnimation.By => (ColorOffset?)By;
+
+ [NotImplemented]
+ IEasingFunction IAnimation.EasingFunction => null;
+
+ void ITimeline.Begin() => _animationImplementation.Begin();
+
+ void ITimeline.Stop() => _animationImplementation.Stop();
+
+ void ITimeline.Resume() => _animationImplementation.Resume();
+
+ void ITimeline.Pause() => _animationImplementation.Pause();
+
+ void ITimeline.Seek(TimeSpan offset) => _animationImplementation.Seek(offset);
+
+ void ITimeline.SeekAlignedToLastTick(TimeSpan offset) => _animationImplementation.SeekAlignedToLastTick(offset);
+
+ void ITimeline.SkipToFill() => _animationImplementation.SkipToFill();
+
+ void ITimeline.Deactivate() => _animationImplementation.Deactivate();
+
+ ///
+ /// Dispose the Double animation.
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _animationImplementation.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ ColorOffset IAnimation.Subtract(ColorOffset minuend, ColorOffset subtrahend) => minuend - subtrahend;
+
+ ColorOffset IAnimation.Add(ColorOffset first, ColorOffset second) => first + second;
+
+ ColorOffset IAnimation.Multiply(float multiplier, ColorOffset color) => multiplier * color;
+
+ ColorOffset IAnimation.Convert(object value)
+ {
+ if (value is string s)
+ {
+ return (ColorOffset)Colors.Parse(s);
+ }
+
+ // TODO: handle int?
+ return default(ColorOffset);
+ }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.Android.cs b/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.Android.cs
deleted file mode 100644
index 93d968df8b41..000000000000
--- a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.Android.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using Android.Animation;
-using Android.Views;
-using Android.Views.Animations;
-using Uno.UI;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace Windows.UI.Xaml.Media.Animation
-{
- public partial class DoubleAnimation
- {
- partial void OnFrame()
- {
- var currentValue = _startingValue + (((NativeValueAnimatorAdapter)_animator).AnimatedFraction * (_endValue - _startingValue));
- SetValue(currentValue);
- }
-
- partial void UseHardware()
- {
- var view = Target as View ?? (Target as Transform)?.View;
- if (view != null)
- {
- view.SetLayerType(LayerType.Hardware, null);
- _animator.AnimationEnd += (sender, e) =>
- {
- view.SetLayerType(LayerType.None, null);
- };
- }
- }
-
- // For performance considerations, do not report each frame if we are GPU bound
- // Frame will be repported on Pause or End (cf. InitializeAnimator)
- private bool ReportEachFrame() => this.GetIsDependantAnimation() || this.GetIsDurationZero();
- }
-}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.cs b/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.cs
index 526d198e7124..b7001f1c8412 100644
--- a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.cs
@@ -10,31 +10,15 @@
namespace Windows.UI.Xaml.Media.Animation
{
- public partial class DoubleAnimation : Timeline, ITimeline
+ public partial class DoubleAnimation : Timeline, ITimeline, IAnimation
{
- private readonly static IEventProvider _trace = Tracing.Get(TraceProvider.Id);
- private EventActivity _traceActivity;
+ private readonly AnimationImplementation _animationImplementation;
- public static class TraceProvider
+ public DoubleAnimation()
{
- public readonly static Guid Id = Guid.Parse("{CC14F7B2-D92B-429D-81A4-E1E7A1B13D3D}");
-
- public const int Start = 1;
- public const int Stop = 2;
- public const int Pause = 3;
- public const int Resume = 4;
+ _animationImplementation = new AnimationImplementation(this);
}
- private DateTimeOffset _lastBeginTime;
- private int _replayCount = 1;
- private float? _startingValue = null;
- private float? _endValue = null;
-
- // Initialize the field with zero capacity, as it may stay empty more often than it is being used.
- private CompositeDisposable _subscriptions = new CompositeDisposable(0);
-
- private IValueAnimator _animator;
-
public double? By
{
get => (double?)GetValue(ByProperty);
@@ -68,6 +52,8 @@ public bool EnableDependentAnimation
set => SetValue(EnableDependentAnimationProperty, value);
}
+ bool IAnimation.EnableDependentAnimation => EnableDependentAnimation;
+
public static readonly DependencyProperty EnableDependentAnimationProperty =
DependencyProperty.Register("EnableDependentAnimation", typeof(bool), typeof(DoubleAnimation), new PropertyMetadata(false));
@@ -77,383 +63,32 @@ public IEasingFunction EasingFunction
set => SetValue(EasingFunctionProperty, value);
}
- public static readonly DependencyProperty EasingFunctionProperty =
- DependencyProperty.Register("EasingFunction", typeof(IEasingFunction), typeof(DoubleAnimation), new PropertyMetadata(null));
-
-
- void ITimeline.Begin()
- {
- if (_trace.IsEnabled)
- {
- _traceActivity = _trace.WriteEventActivity(
- TraceProvider.Start,
- EventOpcode.Start,
- payload: GetTraceProperties()
- );
- }
-
- _subscriptions.Clear(); //Dispose all and start a new
-
- _lastBeginTime = DateTimeOffset.Now;
- _replayCount = 1;
-
- //Start the animation
- Play();
- }
-
- void ITimeline.Stop()
- {
- if (_trace.IsEnabled)
- {
- _trace.WriteEventActivity(
- TraceProvider.Stop,
- EventOpcode.Stop,
- _traceActivity,
- payload: GetTraceProperties()
- );
- }
-
- _animator?.Cancel(); // stop could be called before the initialization
- _startingValue = null;
- ClearValue();
- State = TimelineState.Stopped;
- }
-
- void ITimeline.Resume()
- {
- if (State != TimelineState.Paused)
- {
- return;
- }
-
- if (_trace.IsEnabled)
- {
- _trace.WriteEventActivity(
- TraceProvider.Resume,
- EventOpcode.Send,
- _traceActivity,
- payload: GetTraceProperties()
- );
- }
-
- _animator.Resume();
-
- State = TimelineState.Active;
- }
-
- void ITimeline.Pause()
- {
- if (State == TimelineState.Paused)
- {
- return;
- }
-
- if (_trace.IsEnabled)
- {
- _trace.WriteEventActivity(
- TraceProvider.Pause,
- EventOpcode.Send,
- _traceActivity,
- payload: GetTraceProperties()
- );
- }
-
- _animator.Pause();
-
- State = TimelineState.Paused;
- }
-
- void ITimeline.Seek(TimeSpan offset)
- {
- _animator.CurrentPlayTime = (long)offset.TotalMilliseconds; //Offset is CurrentPlayTime (starting point for animation)
-
- if (State == TimelineState.Active || State == TimelineState.Paused)
- {
- CoreDispatcher.Main.RunAsync(
- CoreDispatcherPriority.Normal,
- () =>
- {
- OnFrame();
- _animator.Pause();
- });
- }
- }
-
- void ITimeline.SeekAlignedToLastTick(TimeSpan offset)
- {
- // Same as Seek
- ((ITimeline)this).Seek(offset);
- }
-
- void ITimeline.SkipToFill()
- {
- if (_animator != null && _animator.IsRunning)
- {
- _animator.Cancel();//Stop the animator if it is running
- _startingValue = null;
- }
-
- SetValue(ComputeToValue());//Set property to its final value
-
- OnEnd();
- }
-
- void ITimeline.Deactivate()
- {
- _animator?.Cancel();//Stop the animator if it is running
- _startingValue = null;
-
- State = TimelineState.Stopped;
- }
-
- ///
- /// Replay this animation.
- ///
- private void Replay()
- {
- _replayCount++;
-
- Play();
- }
-
- ///
- /// Initializes the animator and Events
- ///
- private void InitializeAnimator()
- {
- _startingValue = ComputeFromValue();
-
- _endValue = ComputeToValue();
-
- _animator = AnimatorFactory.Create(this, _startingValue.Value, _endValue.Value);
-
- _animator.SetEasingFunction(this.EasingFunction); //Set the Easing Function of the animator
-
- SetAnimatorDuration();//Set the duration of the animator
-
- _animator.DisposeWith(_subscriptions);
-
- if (ReportEachFrame())
- {
- //Called each frame
- _animator.Update += OnAnimatorUpdate;
- }
- else
- {
-#if __ANDROID_19__
- if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Kitkat)
- {
- _animator.AnimationPause += OnAnimatorAnimationPause;
- }
-#endif
-
- _animator.AnimationEnd += OnAnimatorAnimationEndFrame;
- }
-
- //Called at the end
- _animator.AnimationEnd += OnAnimatorAnimationEnd;
-
- _animator.AnimationCancel += OnAnimatorCancelled;
- }
-
- private void OnAnimatorAnimationEnd(object sender, EventArgs e)
- {
- if (this.Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
- {
- this.Log().Debug("DoubleAnimation has ended.");
- }
+ IEasingFunction IAnimation.EasingFunction => EasingFunction;
- OnEnd();
- _startingValue = null;
- }
+ float? IAnimation.To => (float?)To;
- private void OnAnimatorAnimationEndFrame(object sender, EventArgs e) => OnFrame();
+ float? IAnimation.From => (float?)From;
- private void OnAnimatorAnimationPause(object sender, EventArgs e) => OnFrame();
+ float? IAnimation.By => (float?)By;
- private void OnAnimatorUpdate(object sender, EventArgs e) => OnFrame();
-
- ///
- /// Creates a new animator and animates the view
- ///
- private void Play()
- {
- InitializeAnimator();//Create the animator
-
- if (!EnableDependentAnimation && this.GetIsDependantAnimation())
- { // Don't start the animator its a dependent animation
- return;
- }
-
- UseHardware();//Ensure that the GPU is used for animations
-
- if (BeginTime.HasValue)
- { // Set the start delay
- _animator.StartDelay = (long)BeginTime.Value.TotalMilliseconds;
- }
-
- _animator.Start();
- State = TimelineState.Active;
-
-#if XAMARIN_IOS
- // On iOS, animations started while the app is in the background will lead to properties in an incoherent state (native static
- // values out of syc with native presented values and Xaml values). As a workaround we fast-forward the animation to its final
- // state. (The ideal would probably be to restart the animation when the app resumes.)
- if (Application.Current?.IsSuspended ?? false)
- {
- ((ITimeline)this).SkipToFill();
- }
-#endif
- }
-
- ///
- /// Sets the duration of the animator.
- ///
- private void SetAnimatorDuration()
- {
- switch (Duration.Type)
- {
- case DurationType.Automatic:
- _animator.SetDuration(1000);
- //Default 1 sec animations
- break;
- case DurationType.Forever:
- _animator.SetDuration(long.MaxValue);
- //Nothing is forever, but this is pretty close
- break;
- case DurationType.TimeSpan:
- //If the duration is a timespan use it
- _animator.SetDuration((long)Duration.TimeSpan.TotalMilliseconds);
- break;
- }
- }
-
- ///
- /// Replays the Animation if required, Sets the final state, Raises the Completed event.
- ///
- private void OnEnd()
- {
- // If the animation was GPU based, remove the animated value
- if (NeedsRepeat(_lastBeginTime, _replayCount))
- {
- Replay(); // replay the animation
- return;
- }
-
- if (FillBehavior == FillBehavior.HoldEnd)//Two types of fill behaviors : HoldEnd - Keep displaying the last frame
- {
-#if __IOS__ || __MACOS__
- // iOS && macOS: Here we make sure that the final frame is applied properly (it may have been skipped by animator)
- // Note: The value is applied using the "Animations" precedence, which means that the user won't be able to alter
- // it from application code. Instead we should set the value using a lower precedence
- // (possibly "Local" with PropertyInfo.SetLocalValue(ComputeToValue())) but we must keep the
- // original "Local" value, so will be able to rollback the animation when
- // going to another VisualState (if the storyboard ran in that context).
- // In that case we should also do "ClearValue();" to remove the "Animations" value, even if using "HoldEnd"
- // cf. https://github.com/unoplatform/uno/issues/631
- PropertyInfo.Value = ComputeToValue();
-#endif
-
- State = TimelineState.Filling;
- }
- else // Stop -Put back the initial state
- {
- State = TimelineState.Stopped;
-
- ClearValue();
- }
-
- OnCompleted();
- }
-
- ///
- /// Sets the final state
- ///
- private void OnAnimatorCancelled(object sender, EventArgs e)
- {
- if (this.Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
- {
- this.Log().Debug("DoubleAnimation was cancelled.");
- }
-
- // Means the animation is stopped because the animator was cancelled.
- // This may happen either if someone stops the animation beforehand or
- // if the animator is stopped at deactivation. This means that even though
- // we should consider the animation to be stopped, we shouldn't clear any animated
- // value in order to support deactivation scenarios.
- State = TimelineState.Stopped;
-
-#if XAMARIN_IOS || __MACOS__
- _startingValue = null;
-
- // On Android, AnimationEnd is always called after AnimationCancel. We don't unset _startingValue yet to be able to calculate
- // the final value correctly.
-#endif
- }
-
- ///
- /// Calculates the From value of the animation
- /// For simplicity, animations are based on to and from values
- ///
- private float ComputeFromValue()
- {
- if (From.HasValue)
- {
- return (float)From.Value;
- }
+ public static readonly DependencyProperty EasingFunctionProperty =
+ DependencyProperty.Register("EasingFunction", typeof(IEasingFunction), typeof(DoubleAnimation), new PropertyMetadata(null));
- if (By.HasValue && To.HasValue)
- {
- return (float)(To.Value - By.Value);
- }
+ void ITimeline.Begin() => _animationImplementation.Begin();
- return GetDefaultTargetValue() ?? 0f;
- }
+ void ITimeline.Stop() => _animationImplementation.Stop();
- private float? GetDefaultTargetValue()
- {
- if (_startingValue != null)
- {
- return _startingValue;
- }
- else
- {
- var value = GetValue();
+ void ITimeline.Resume() => _animationImplementation.Resume();
- if (value != null)
- {
- return Convert.ToSingle(value);
- }
- }
+ void ITimeline.Pause() => _animationImplementation.Pause();
- return null;
- }
+ void ITimeline.Seek(TimeSpan offset) => _animationImplementation.Seek(offset);
- ///
- /// Calculates the To value of the animation
- /// For simplicity, animations are based on to and from values
- ///
- private float ComputeToValue()
- {
- if (To.HasValue)
- {
- return (float)To.Value;
- }
+ void ITimeline.SeekAlignedToLastTick(TimeSpan offset) => _animationImplementation.SeekAlignedToLastTick(offset);
- if (By.HasValue)
- {
- if (From.HasValue)
- {
- return (float)(From.Value + By.Value);
- }
- else
- {
- return (GetDefaultTargetValue() ?? 0f) + (float)By.Value;
- }
- }
+ void ITimeline.SkipToFill() => _animationImplementation.SkipToFill();
- return GetDefaultTargetValue() ?? 0f;
- }
+ void ITimeline.Deactivate() => _animationImplementation.Deactivate();
///
/// Dispose the Double animation.
@@ -462,19 +97,19 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
- _subscriptions.Dispose();
-
- DisposePartial();
+ _animationImplementation.Dispose();
}
base.Dispose(disposing);
}
- partial void DisposePartial();
+ float IAnimation.Subtract(float minuend, float subtrahend) => minuend - subtrahend;
+
+ float IAnimation.Add(float first, float second) => first + second;
+
+ float IAnimation.Convert(object value) => Convert.ToSingle(value);
- partial void OnFrame();
- partial void HoldValue();
- partial void UseHardware();
+ float IAnimation.Multiply(float multiplier, float t) => multiplier * t;
}
}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.iOSmacOS.cs b/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.iOSmacOS.cs
deleted file mode 100644
index c2987ef60fda..000000000000
--- a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.iOSmacOS.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using Uno.Extensions;
-using Uno.Logging;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace Windows.UI.Xaml.Media.Animation
-{
- public partial class DoubleAnimation
- {
- ///
- /// Ensures that the animation is running on the GPU
- ///
- partial void UseHardware()
- {
- //Do nothing
- //Already runs on gpu
- }
-
- ///
- /// Set the value on a new frame
- ///
- partial void OnFrame()
- {
- if (!this.GetIsHardwareAnimated())
- {
- SetValue(_animator.AnimatedValue);
- }
- else
- {
- this.SetValueBypassPropagation(_animator.AnimatedValue);
- }
- }
-
- private bool ReportEachFrame() => true;
-
- partial void DisposePartial()
- {
- _animator?.Dispose();
- }
- }
-}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.net.cs b/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.net.cs
deleted file mode 100644
index b7918046e7bf..000000000000
--- a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.net.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Windows.UI.Xaml.Media.Animation
-{
- public partial class DoubleAnimation
- {
- private bool ReportEachFrame() => true;
-
- partial void OnFrame()
- {
- SetValue(_animator.AnimatedValue);
- }
- }
-}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.wasm.cs b/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.wasm.cs
deleted file mode 100644
index b7918046e7bf..000000000000
--- a/src/Uno.UI/UI/Xaml/Media/Animation/DoubleAnimation.wasm.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Windows.UI.Xaml.Media.Animation
-{
- public partial class DoubleAnimation
- {
- private bool ReportEachFrame() => true;
-
- partial void OnFrame()
- {
- SetValue(_animator.AnimatedValue);
- }
- }
-}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/IAnimation.cs b/src/Uno.UI/UI/Xaml/Media/Animation/IAnimation.cs
new file mode 100644
index 000000000000..41a287bdf823
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/IAnimation.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ ///
+ /// Contract for a animation which is backed by .
+ ///
+ ///
+ internal interface IAnimation where T : struct
+ {
+ T? To { get; }
+ T? From { get; }
+ T? By { get; }
+ bool EnableDependentAnimation { get; }
+ IEasingFunction EasingFunction { get; }
+ T Subtract(T minuend, T subtrahend);
+ T Add(T first, T second);
+ T Multiply(float multiplier, T t);
+ T Convert(object value);
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.Android.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.Android.cs
new file mode 100644
index 000000000000..2e195c20669e
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.Android.cs
@@ -0,0 +1,50 @@
+using Android.Animation;
+using Android.Views;
+using Android.Views.Animations;
+using Uno.UI;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ partial class Timeline
+ {
+ partial class AnimationImplementation
+ {
+ partial void OnFrame()
+ {
+ if (_endValue == null || _startingValue == null)
+ {
+ SetValue(null);
+ return;
+ }
+
+ // TODO: apply easing function - https://github.com/unoplatform/uno/issues/2948
+
+ var totalDiff = AnimationOwner.Subtract(_endValue.Value, _startingValue.Value);
+ var currentDiff = AnimationOwner.Multiply(((NativeValueAnimatorAdapter)_animator).AnimatedFraction, totalDiff);
+ var currentValue = AnimationOwner.Add(_startingValue.Value, currentDiff);
+ SetValue(currentValue);
+ }
+
+ partial void UseHardware()
+ {
+ var view = Target as View ?? (Target as Transform)?.View;
+ if (view != null)
+ {
+ view.SetLayerType(LayerType.Hardware, null);
+ _animator.AnimationEnd += (sender, e) =>
+ {
+ view.SetLayerType(LayerType.None, null);
+ };
+ }
+ }
+
+ // For performance considerations, do not report each frame if we are GPU bound
+ // Frame will be reported on Pause or End (cf. InitializeAnimator)
+ private bool ReportEachFrame() => _owner.GetIsDependantAnimation() || _owner.GetIsDurationZero();
+ }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.cs
new file mode 100644
index 000000000000..af4ead599494
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.cs
@@ -0,0 +1,473 @@
+using Uno.Extensions;
+using System;
+using System.Collections.Generic;
+using Uno.Disposables;
+using System.Text;
+using System.Linq;
+using Uno.Diagnostics.Eventing;
+using Windows.UI.Core;
+using Uno.Logging;
+using Uno.UI.DataBinding;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ public partial class Timeline
+ {
+ private protected partial class AnimationImplementation : IDisposable where T : struct
+ {
+ private readonly static IEventProvider _trace = Tracing.Get(TraceProvider.Id);
+ private Timeline _owner;
+ private IAnimation AnimationOwner => _owner as IAnimation;
+ private EventActivity _traceActivity;
+
+ public static class TraceProvider
+ {
+ public readonly static Guid Id = Guid.Parse("{CC14F7B2-D92B-429D-81A4-E1E7A1B13D3D}");
+
+ public const int Start = 1;
+ public const int Stop = 2;
+ public const int Pause = 3;
+ public const int Resume = 4;
+ }
+
+ private DateTimeOffset _lastBeginTime;
+ private int _replayCount = 1;
+ private T? _startingValue = null;
+ private T? _endValue = null;
+
+ // Initialize the field with zero capacity, as it may stay empty more often than it is being used.
+ private readonly CompositeDisposable _subscriptions = new CompositeDisposable(0);
+
+ private IValueAnimator _animator;
+
+ public AnimationImplementation(Timeline owner)
+ {
+ this._owner = owner;
+ }
+
+ private TimelineState State
+ {
+ get => _owner?.State ?? default(TimelineState);
+ set
+ {
+ if (_owner != null)
+ {
+ _owner.State = value;
+ }
+ }
+ }
+ private TimeSpan? BeginTime => _owner?.BeginTime;
+ private Duration Duration => _owner?.Duration ?? default(Duration);
+ private FillBehavior FillBehavior => _owner?.FillBehavior ?? default(FillBehavior);
+
+ private T? From => AnimationOwner?.From;
+ private T? To => AnimationOwner?.To;
+ private T? By => AnimationOwner?.By;
+ private IEasingFunction EasingFunction => AnimationOwner?.EasingFunction;
+ private bool EnableDependentAnimation => AnimationOwner?.EnableDependentAnimation ?? false;
+ private DependencyObject Target => _owner?.Target;
+ private BindingPath PropertyInfo => _owner?.PropertyInfo;
+
+ private string[] GetTraceProperties() => _owner?.GetTraceProperties();
+ private void ClearValue() => _owner?.ClearValue();
+ private void SetValue(object value) => _owner?.SetValue(value);
+ private bool NeedsRepeat(DateTimeOffset lastBeginTime, int replayCount) => _owner?.NeedsRepeat(lastBeginTime, replayCount) ?? false;
+ private object GetValue() => _owner?.GetValue();
+
+ public void Begin()
+ {
+ if (_trace.IsEnabled)
+ {
+ _traceActivity = _trace.WriteEventActivity(
+ TraceProvider.Start,
+ EventOpcode.Start,
+ payload: GetTraceProperties()
+ );
+ }
+
+ PropertyInfo?.CloneShareableObjectsInPath();
+
+ _subscriptions.Clear(); //Dispose all and start a new
+
+ _lastBeginTime = DateTimeOffset.Now;
+ _replayCount = 1;
+
+ //Start the animation
+ Play();
+ }
+
+ public void Stop()
+ {
+ if (_trace.IsEnabled)
+ {
+ _trace.WriteEventActivity(
+ TraceProvider.Stop,
+ EventOpcode.Stop,
+ _traceActivity,
+ payload: GetTraceProperties()
+ );
+ }
+
+ _animator?.Cancel(); // stop could be called before the initialization
+ _startingValue = null;
+ ClearValue();
+ State = TimelineState.Stopped;
+ }
+
+ public void Resume()
+ {
+ if (State != TimelineState.Paused)
+ {
+ return;
+ }
+
+ if (_trace.IsEnabled)
+ {
+ _trace.WriteEventActivity(
+ TraceProvider.Resume,
+ EventOpcode.Send,
+ _traceActivity,
+ payload: GetTraceProperties()
+ );
+ }
+
+ _animator.Resume();
+
+ State = TimelineState.Active;
+ }
+
+ public void Pause()
+ {
+ if (State == TimelineState.Paused)
+ {
+ return;
+ }
+
+ if (_trace.IsEnabled)
+ {
+ _trace.WriteEventActivity(
+ TraceProvider.Pause,
+ EventOpcode.Send,
+ _traceActivity,
+ payload: GetTraceProperties()
+ );
+ }
+
+ _animator.Pause();
+
+ State = TimelineState.Paused;
+ }
+
+ public void Seek(TimeSpan offset)
+ {
+ _animator.CurrentPlayTime = (long)offset.TotalMilliseconds; //Offset is CurrentPlayTime (starting point for animation)
+
+ if (State == TimelineState.Active || State == TimelineState.Paused)
+ {
+ CoreDispatcher.Main.RunAsync(
+ CoreDispatcherPriority.Normal,
+ () =>
+ {
+ OnFrame();
+ _animator.Pause();
+ });
+ }
+ }
+
+ public void SeekAlignedToLastTick(TimeSpan offset)
+ {
+ // Same as Seek
+ ((ITimeline)this).Seek(offset);
+ }
+
+ public void SkipToFill()
+ {
+ if (_animator != null && _animator.IsRunning)
+ {
+ _animator.Cancel();//Stop the animator if it is running
+ _startingValue = null;
+ }
+
+ SetValue(ComputeToValue());//Set property to its final value
+
+ OnEnd();
+ }
+
+ public void Deactivate()
+ {
+ _animator?.Cancel();//Stop the animator if it is running
+ _startingValue = null;
+
+ State = TimelineState.Stopped;
+ }
+
+ ///
+ /// Replay this animation.
+ ///
+ private void Replay()
+ {
+ _replayCount++;
+
+ Play();
+ }
+
+ ///
+ /// Initializes the animator and Events
+ ///
+ private void InitializeAnimator()
+ {
+ _startingValue = ComputeFromValue();
+
+ _endValue = ComputeToValue();
+
+ _animator = AnimatorFactory.Create(_owner, _startingValue.Value, _endValue.Value);
+
+ _animator.SetEasingFunction(this.EasingFunction); //Set the Easing Function of the animator
+
+ SetAnimatorDuration();//Set the duration of the animator
+
+ _animator.DisposeWith(_subscriptions);
+
+ if (ReportEachFrame())
+ {
+ //Called each frame
+ _animator.Update += OnAnimatorUpdate;
+ }
+ else
+ {
+#if __ANDROID_19__
+ if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Kitkat)
+ {
+ _animator.AnimationPause += OnAnimatorAnimationPause;
+ }
+#endif
+
+ _animator.AnimationEnd += OnAnimatorAnimationEndFrame;
+ }
+
+ //Called at the end
+ _animator.AnimationEnd += OnAnimatorAnimationEnd;
+
+ _animator.AnimationCancel += OnAnimatorCancelled;
+ }
+
+ private void OnAnimatorAnimationEnd(object sender, EventArgs e)
+ {
+ if (this.Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
+ {
+ this.Log().Debug("DoubleAnimation has ended.");
+ }
+
+ OnEnd();
+ _startingValue = null;
+ }
+
+ private void OnAnimatorAnimationEndFrame(object sender, EventArgs e) => OnFrame();
+
+ private void OnAnimatorAnimationPause(object sender, EventArgs e) => OnFrame();
+
+ private void OnAnimatorUpdate(object sender, EventArgs e) => OnFrame();
+
+ ///
+ /// Creates a new animator and animates the view
+ ///
+ private void Play()
+ {
+ InitializeAnimator();//Create the animator
+
+ if (!EnableDependentAnimation && _owner.GetIsDependantAnimation())
+ { // Don't start the animator its a dependent animation
+ return;
+ }
+
+ UseHardware();//Ensure that the GPU is used for animations
+
+ if (BeginTime.HasValue)
+ { // Set the start delay
+ _animator.StartDelay = (long)BeginTime.Value.TotalMilliseconds;
+ }
+
+ _animator.Start();
+ State = TimelineState.Active;
+
+#if XAMARIN_IOS
+ // On iOS, animations started while the app is in the background will lead to properties in an incoherent state (native static
+ // values out of syc with native presented values and Xaml values). As a workaround we fast-forward the animation to its final
+ // state. (The ideal would probably be to restart the animation when the app resumes.)
+ if (Application.Current?.IsSuspended ?? false)
+ {
+ ((ITimeline)this).SkipToFill();
+ }
+#endif
+ }
+
+ ///
+ /// Sets the duration of the animator.
+ ///
+ private void SetAnimatorDuration()
+ {
+ switch (Duration.Type)
+ {
+ case DurationType.Automatic:
+ _animator.SetDuration(1000);
+ //Default 1 sec animations
+ break;
+ case DurationType.Forever:
+ _animator.SetDuration(long.MaxValue);
+ //Nothing is forever, but this is pretty close
+ break;
+ case DurationType.TimeSpan:
+ //If the duration is a timespan use it
+ _animator.SetDuration((long)Duration.TimeSpan.TotalMilliseconds);
+ break;
+ }
+ }
+
+ ///
+ /// Replays the Animation if required, Sets the final state, Raises the Completed event.
+ ///
+ private void OnEnd()
+ {
+ // If the animation was GPU based, remove the animated value
+ if (NeedsRepeat(_lastBeginTime, _replayCount))
+ {
+ Replay(); // replay the animation
+ return;
+ }
+
+ if (FillBehavior == FillBehavior.HoldEnd)//Two types of fill behaviors : HoldEnd - Keep displaying the last frame
+ {
+ #if __IOS__ || __MACOS__
+ // iOS && macOS: Here we make sure that the final frame is applied properly (it may have been skipped by animator)
+ // Note: The value is applied using the "Animations" precedence, which means that the user won't be able to alter
+ // it from application code. Instead we should set the value using a lower precedence
+ // (possibly "Local" with PropertyInfo.SetLocalValue(ComputeToValue())) but we must keep the
+ // original "Local" value, so will be able to rollback the animation when
+ // going to another VisualState (if the storyboard ran in that context).
+ // In that case we should also do "ClearValue();" to remove the "Animations" value, even if using "HoldEnd"
+ // cf. https://github.com/unoplatform/uno/issues/631
+ PropertyInfo.Value = ComputeToValue();
+#endif
+
+ State = TimelineState.Filling;
+ }
+ else // Stop -Put back the initial state
+ {
+ State = TimelineState.Stopped;
+
+ ClearValue();
+ }
+
+ _owner.OnCompleted();
+ }
+
+ ///
+ /// Sets the final state
+ ///
+ private void OnAnimatorCancelled(object sender, EventArgs e)
+ {
+ if (this.Log().IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
+ {
+ this.Log().Debug("DoubleAnimation was cancelled.");
+ }
+
+ // Means the animation is stopped because the animator was cancelled.
+ // This may happen either if someone stops the animation beforehand or
+ // if the animator is stopped at deactivation. This means that even though
+ // we should consider the animation to be stopped, we shouldn't clear any animated
+ // value in order to support deactivation scenarios.
+ State = TimelineState.Stopped;
+
+#if XAMARIN_IOS || __MACOS__
+ _startingValue = null;
+
+ // On Android, AnimationEnd is always called after AnimationCancel. We don't unset _startingValue yet to be able to calculate
+ // the final value correctly.
+#endif
+ }
+
+ ///
+ /// Calculates the From value of the animation
+ /// For simplicity, animations are based on to and from values
+ ///
+ private T ComputeFromValue()
+ {
+ if (From.HasValue)
+ {
+ return (T)From.Value;
+ }
+
+ if (By.HasValue && To.HasValue)
+ {
+ return AnimationOwner.Subtract(To.Value, By.Value);
+ }
+
+ return GetDefaultTargetValue() ?? default(T);
+ }
+
+ private T? GetDefaultTargetValue()
+ {
+ if (_startingValue != null)
+ {
+ return _startingValue;
+ }
+ else
+ {
+ var value = GetValue();
+
+ if (value != null)
+ {
+ return AnimationOwner.Convert(value);
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Calculates the To value of the animation
+ /// For simplicity, animations are based on to and from values
+ ///
+ private T ComputeToValue()
+ {
+ if (To.HasValue)
+ {
+ return (T)To.Value;
+ }
+
+ if (By.HasValue)
+ {
+ if (From.HasValue)
+ {
+ return AnimationOwner.Add(From.Value, By.Value);
+ }
+ else
+ {
+ return AnimationOwner.Add(GetDefaultTargetValue() ?? default(T), By.Value);
+ }
+ }
+
+ return GetDefaultTargetValue() ?? default(T);
+ }
+
+ ///
+ /// Dispose the Double animation.
+ ///
+ public void Dispose()
+ {
+ _subscriptions.Dispose();
+
+ _owner = null;
+
+ DisposePartial();
+ }
+
+ partial void DisposePartial();
+
+ partial void OnFrame();
+ partial void HoldValue();
+ partial void UseHardware();
+ }
+ }
+}
+
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.iOSmacOS.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.iOSmacOS.cs
new file mode 100644
index 000000000000..e4bfcdb8fe45
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.iOSmacOS.cs
@@ -0,0 +1,46 @@
+using Uno.Extensions;
+using Uno.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ partial class Timeline
+ {
+ partial class AnimationImplementation
+ {
+ ///
+ /// Ensures that the animation is running on the GPU
+ ///
+ partial void UseHardware()
+ {
+ //Do nothing
+ //Already runs on gpu
+ }
+
+ ///
+ /// Set the value on a new frame
+ ///
+ partial void OnFrame()
+ {
+ if (!_owner.GetIsHardwareAnimated())
+ {
+ SetValue(_animator.AnimatedValue);
+ }
+ else
+ {
+ _owner.SetValueBypassPropagation(_animator.AnimatedValue);
+ }
+ }
+
+ private bool ReportEachFrame() => true;
+
+ partial void DisposePartial()
+ {
+ _animator?.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.net.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.net.cs
new file mode 100644
index 000000000000..0ae7434a23bc
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.net.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ partial class Timeline
+ {
+ partial class AnimationImplementation
+ {
+ private bool ReportEachFrame() => true;
+
+ partial void OnFrame()
+ {
+ SetValue(_animator.AnimatedValue);
+ }
+ }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.wasm.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.wasm.cs
new file mode 100644
index 000000000000..0ae7434a23bc
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.animation.wasm.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Windows.UI.Xaml.Media.Animation
+{
+ partial class Timeline
+ {
+ partial class AnimationImplementation
+ {
+ private bool ReportEachFrame() => true;
+
+ partial void OnFrame()
+ {
+ SetValue(_animator.AnimatedValue);
+ }
+ }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.cs b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.cs
index 32c8d0a6488d..846ddbdec783 100644
--- a/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.cs
+++ b/src/Uno.UI/UI/Xaml/Media/Animation/Timeline.cs
@@ -293,6 +293,8 @@ void ITimeline.Deactivate()
Foundation.Metadata.ApiInformation.TryRaiseNotImplemented(GetType().FullName, "void Deactivate()");
}
+ private protected IValueAnimator InitializeAnimator() => throw new NotSupportedException(); // Should be implemented by classes which use AnimationImplementation
+
///
/// Checks if the Timeline will repeat.
@@ -325,7 +327,7 @@ internal bool IsTargetPropertyDependant()
//TODO Projection, Clip, Canvas.Left or Canvas.Top
if (boundProperty.PropertyName.EndsWith("Opacity")
- || (boundProperty.DataContext is SolidColorBrush && boundProperty.PropertyName == "Color")
+ || (boundProperty.DataContext is SolidColorBrush && boundProperty.PropertyName.EndsWith("Color"))
|| (boundProperty.DataContext is Transform)
)
{
diff --git a/src/Uno.UI/UI/Xaml/Media/SolidColorBrush.cs b/src/Uno.UI/UI/Xaml/Media/SolidColorBrush.cs
index 8e9dd73867a3..076353ee4268 100644
--- a/src/Uno.UI/UI/Xaml/Media/SolidColorBrush.cs
+++ b/src/Uno.UI/UI/Xaml/Media/SolidColorBrush.cs
@@ -23,6 +23,7 @@
namespace Windows.UI.Xaml.Media
{
public partial class SolidColorBrush : Brush, IEquatable
+ , IShareableDependencyObject //TODO: should be implemented on Brush
{
///
/// Blends the Color set on the SolidColorBrush with its Opacity. Should generally be used for rendering rather than the Color property itself.
@@ -32,6 +33,9 @@ internal Color ColorWithOpacity
get; private set;
}
+ private readonly bool _isClone;
+ bool IShareableDependencyObject.IsClone => _isClone;
+
public SolidColorBrush()
{
IsAutoPropertyInheritanceEnabled = false;
@@ -43,6 +47,18 @@ public SolidColorBrush(Color color) : this()
UpdateColorWithOpacity(color);
}
+ private SolidColorBrush(SolidColorBrush original) : this()
+ {
+ _isClone = true;
+
+ Color = original.Color;
+ UpdateColorWithOpacity(Color);
+
+ Opacity = original.Opacity;
+ Transform = original.Transform;
+ RelativeTransform = original.RelativeTransform;
+ }
+
///
/// This method is required for performance. Creating a native Color
/// requires a round-trip with Objective-C, so updating this value only when opacity
@@ -76,7 +92,7 @@ public Color Color
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColorProperty =
DependencyProperty.Register(
- "Color",
+ "Color",
typeof(Color),
typeof(SolidColorBrush),
new PropertyMetadata(
@@ -89,6 +105,8 @@ public Color Color
#endregion
+ DependencyObject IShareableDependencyObject.Clone() => new SolidColorBrush(this);
+
public override string ToString() => "[SolidColorBrush {0}]".InvariantCultureFormat(Color);
public override bool Equals(object obj) => Equals(obj as SolidColorBrush);
@@ -100,10 +118,11 @@ public bool Equals(SolidColorBrush other)
return false;
}
- return ReferenceEquals(this, other) || ColorWithOpacity.Equals(other.ColorWithOpacity);
+ return ReferenceEquals(this, other)
+ || (ColorWithOpacity.Equals(other.ColorWithOpacity) && this._isClone == other._isClone);
}
- public override int GetHashCode() => ColorWithOpacity.GetHashCode();
+ public override int GetHashCode() => ColorWithOpacity.GetHashCode() ^ _isClone.GetHashCode();
public static bool operator ==(SolidColorBrush left, SolidColorBrush right) => Equals(left, right);
diff --git a/src/Uno.UWP/UI/ColorOffset.cs b/src/Uno.UWP/UI/ColorOffset.cs
new file mode 100644
index 000000000000..c7a1c870d95e
--- /dev/null
+++ b/src/Uno.UWP/UI/ColorOffset.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Windows.UI
+{
+ ///
+ /// Mapping of a which permits negative values, which is convenient for calculations during animation.
+ ///
+ internal struct ColorOffset
+ {
+ public int A { get; set; }
+
+ public int B { get; set; }
+
+ public int G { get; set; }
+
+ public int R { get; set; }
+
+
+
+ private ColorOffset(int a, int r, int g, int b)
+ {
+ A = a;
+ R = r;
+ G = g;
+ B = b;
+ }
+
+ public static ColorOffset FromArgb(int a, int r, int g, int b) => new ColorOffset(a, r, g, b);
+
+ public static explicit operator Color(ColorOffset colorOffset) => Color.FromArgb((byte)colorOffset.A, (byte)colorOffset.R, (byte)colorOffset.G, (byte)colorOffset.B);
+
+ public static explicit operator ColorOffset(Color color) => FromArgb(color.A, color.R, color.G, color.B);
+
+ public static ColorOffset operator -(ColorOffset minuend, ColorOffset subtrahend)
+ => FromArgb(minuend.A - subtrahend.A, minuend.R - subtrahend.R, minuend.G - subtrahend.G, minuend.B - subtrahend.B);
+
+ public static ColorOffset operator +(ColorOffset first, ColorOffset second)
+ => FromArgb(first.A + second.A, first.R + second.R, first.G + second.G, first.B + second.B);
+
+ public static ColorOffset operator *(float multiplier, ColorOffset color)
+ => FromArgb(multiplier * color.A, multiplier * color.R, multiplier * color.G, multiplier * color.B);
+
+ private static ColorOffset FromArgb(float a, float r, float g, float b) => FromArgb((int)a, (int)r, (int)g, (int)b);
+
+ }
+}