diff --git a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs index 626a3e7c77..c76d4db513 100644 --- a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs +++ b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs @@ -10,21 +10,21 @@ namespace RenderDemo.ViewModels public AnimationsPageViewModel() { - ToggleGlobalPlayState = ReactiveCommand.Create(()=>TogglePlayState()); + ToggleGlobalPlayState = ReactiveCommand.Create(() => TogglePlayState()); } void TogglePlayState() { - switch (Timing.GetGlobalPlayState()) + switch (Timing.GlobalPlayState) { case PlayState.Run: PlayStateText = "Resume all animations"; - Timing.SetGlobalPlayState(PlayState.Pause); + Timing.GlobalPlayState = PlayState.Pause; break; case PlayState.Pause: PlayStateText = "Pause all animations"; - Timing.SetGlobalPlayState(PlayState.Run); + Timing.GlobalPlayState = PlayState.Run; break; } } @@ -36,5 +36,5 @@ namespace RenderDemo.ViewModels } public ReactiveCommand ToggleGlobalPlayState { get; } - } + } } diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 85317af1a8..303e01aed8 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -23,25 +23,8 @@ namespace Avalonia.Animation /// public Animatable() { - Transitions = new Transitions(); - AnimatableTimer = Timing.AnimationStateTimer - .Select(p => - { - if (this._playState == PlayState.Pause) - { - return (PlayState.Pause, p.Item2); - } - else return p; - }) - .Publish() - .RefCount(); - } - - /// - /// The specific animations timer for this control. - /// - /// - public IObservable<(PlayState, long)> AnimatableTimer; + Transitions = new Transitions(); + } /// /// Defines the property. diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 344b63d15c..da2fc75c0b 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -21,70 +21,81 @@ namespace Avalonia.Animation /// public class Animation : AvaloniaList, IAnimation { - private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> - { - ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ) - }; - - public static void RegisterAnimator(Func condition) - where TAnimator : IAnimator - { - Animators.Insert(0, (condition, typeof(TAnimator))); - } - - private static Type GetAnimatorType(AvaloniaProperty property) - { - foreach (var (condition, type) in Animators) - { - if (condition(property)) - { - return type; - } - } - return null; - } public AvaloniaList _animators { get; set; } = new AvaloniaList(); /// - /// Run time of this animation. + /// Gets or sets the active time of this animation. /// public TimeSpan Duration { get; set; } /// - /// Delay time for this animation. - /// - public TimeSpan Delay { get; set; } - - /// - /// The repeat count for this animation. + /// Gets or sets the repeat count for this animation. /// public RepeatCount RepeatCount { get; set; } /// - /// The playback direction for this animation. + /// Gets or sets the playback direction for this animation. /// public PlaybackDirection PlaybackDirection { get; set; } /// - /// The value fill mode for this animation. + /// Gets or sets the value fill mode for this animation. /// public FillMode FillMode { get; set; } /// - /// Easing function to be used. + /// Gets or sets the easing function to be used for this animation. /// public Easing Easing { get; set; } = new LinearEasing(); - + /// - /// Sets the speed multiple for this animation. + /// Gets or sets the speed multiple for this animation. /// public double SpeedRatio { get; set; } = 1d; - /// - /// Sets the behavior for having a delay between repeats for this animation. - /// - public bool DelayBetweenRepeats { get; set; } + /// + /// Gets or sets the delay time for this animation. + /// + /// + /// Describes a delay to be added before the animation starts, and optionally between + /// repeats of the animation if is set. + /// + public TimeSpan Delay { get; set; } + + /// + /// Gets or sets a value indicating whether will be applied between + /// iterations of the animation. + /// + /// + /// If this property is not set, then will only be applied to the first + /// iteration of the animation. + /// + public bool DelayBetweenIterations { get; set; } + + + private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> + { + ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ) + }; + + public static void RegisterAnimator(Func condition) + where TAnimator : IAnimator + { + Animators.Insert(0, (condition, typeof(TAnimator))); + } + + private static Type GetAnimatorType(AvaloniaProperty property) + { + foreach (var (condition, type) in Animators) + { + if (condition(property)) + { + return type; + } + } + return null; + } private (IList Animators, IList subscriptions) InterpretKeyframes(Animatable control) { diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 38ccce6cb5..47bfa1c321 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -9,7 +9,7 @@ namespace Avalonia.Animation /// Provides statefulness for an iteration of a keyframe animation. /// internal class AnimatorStateMachine : IObservable, IDisposable - { + { T lastInterpValue; T firstKFValue; @@ -34,6 +34,11 @@ namespace Avalonia.Animation internal bool unsubscribe; private bool isDisposed; + private long? internalClock; + + private long? previousClock = null; + private long currentDiscreteTime; + private Easings.Easing EaseFunc; private IObserver targetObserver; private readonly Action onComplete; @@ -55,7 +60,7 @@ namespace Avalonia.Animation speedRatio = animation.SpeedRatio; delayFC = ((animation.Delay.Ticks / Timing.FrameTick.Ticks) * speedRatio); durationFC = ((animation.Duration.Ticks / Timing.FrameTick.Ticks) * speedRatio); - delayBetweenIterations = animation.DelayBetweenRepeats; + delayBetweenIterations = animation.DelayBetweenIterations; switch (animation.RepeatCount.RepeatType) { @@ -72,15 +77,15 @@ namespace Avalonia.Animation } animationDirection = animation.PlaybackDirection; - fillMode = animation.FillMode; + fillMode = animation.FillMode; this.onComplete = onComplete; } - public void Step(PlayState playState, long frameTick, Func Interpolator) + public void Step(long frameTick, Func Interpolator) { try { - InternalStep(playState, frameTick, Interpolator); + InternalStep(frameTick, Interpolator); } catch (Exception e) { @@ -107,8 +112,31 @@ namespace Avalonia.Animation targetObserver.OnNext(lastInterpValue); } - private void InternalStep(PlayState playState, long frameTick, Func Interpolator) + private void InternalStep(long time, Func Interpolator) { + if (Timing.GlobalPlayState == PlayState.Stop || targetControl.PlayState == PlayState.Stop) + DoComplete(); + + if (!previousClock.HasValue) + { + previousClock = time; + internalClock = 0; + } + else + { + if (Timing.GlobalPlayState == PlayState.Pause || targetControl.PlayState == PlayState.Pause) + { + previousClock = time; + return; + } + var delta = time - previousClock; + internalClock += delta; + previousClock = time; + } + + // currentDiscreteTime = internalClock.Value; + currentDiscreteTime++; + if (!gotFirstKFValue) { firstKFValue = (T)parent.First().Value; @@ -117,42 +145,23 @@ namespace Avalonia.Animation if (!gotFirstFrameCount) { - firstFrameCount = frameTick; + firstFrameCount = currentDiscreteTime; gotFirstFrameCount = true; } if (isDisposed) throw new InvalidProgramException("This KeyFrames Animation is already disposed."); - if (playState == PlayState.Stop) - DoComplete(); - - // Save state and pause the machine - // if (playState == PlayState.Pause && currentState != KeyFramesStates.Pause) - // { - // savedState = currentState; - // currentState = KeyFramesStates.Pause; - // } - - // // Resume the previous state - // if (playState != PlayState.Pause && currentState == KeyFramesStates.Pause) - // currentState = savedState; - // get the time with the initial fc as point of origin. - double t = (frameTick - firstFrameCount); + double t = (currentDiscreteTime - firstFrameCount); // check if t is within the zeroth iteration - if (t <= (delayFC + durationFC)) - { - currentIteration = 0; - t = t % (delayFC + durationFC + 1); - } - else - { - var totalDur = (double)((delayBetweenIterations ? delayFC : 0) + durationFC + 1); - currentIteration = Math.Floor(t / totalDur); - t = t % totalDur; - } + + double delayEndpoint = delayFC; + double iterationEndpoint = delayEndpoint + durationFC; + + currentIteration = Math.Floor(t / iterationEndpoint); + t = t % iterationEndpoint; // check if it's over the repeat count if (currentIteration > (repeatCount - 1) && !isLooping) @@ -166,19 +175,17 @@ namespace Avalonia.Animation animationDirection == PlaybackDirection.AlternateReverse ? (currentIteration % 2 == 0) ? true : false : animationDirection == PlaybackDirection.Reverse ? true : false; - double delayEndpoint = delayFC; - double iterationEndpoint = delayEndpoint + durationFC; - if (delayFC > 0 & t >= 0 & t <= delayEndpoint) + if (delayFC > 0 & t <= delayEndpoint) { - if (currentIteration == 0 || delayBetweenIterations) + if (currentIteration == 0) DoDelay(); - } - else if (t >= delayEndpoint & t <= iterationEndpoint) + else if (t > delayEndpoint & t < iterationEndpoint) { - var interpVal = t / durationFC; - + double k = t - delayFC; + var interpVal = k / (double)durationFC; + if (isCurIterReverse) interpVal = 1 - interpVal; @@ -187,7 +194,7 @@ namespace Avalonia.Animation lastInterpValue = Interpolator(easedTime, neutralValue); targetObserver.OnNext(lastInterpValue); } - else if (t > iterationEndpoint & (currentIteration + 1 > repeatCount & !isLooping)) + else if (t > iterationEndpoint && !isLooping) { DoComplete(); } @@ -198,7 +205,6 @@ namespace Avalonia.Animation targetObserver = observer; return this; } - public void Dispose() { unsubscribe = true; diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 607ccee947..a8b5ce7a27 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -21,7 +21,7 @@ namespace Avalonia.Animation /// private readonly SortedList _convertedKeyframes = new SortedList(); - private bool _isVerfifiedAndConverted; + private bool isVerfifiedAndConverted; /// /// Gets or sets the target property for the keyframe. @@ -31,18 +31,17 @@ namespace Avalonia.Animation public Animator() { // Invalidate keyframes when changed. - this.CollectionChanged += delegate { _isVerfifiedAndConverted = false; }; + this.CollectionChanged += delegate { isVerfifiedAndConverted = false; }; } /// - public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete) + public virtual IDisposable Apply(Animation animation, Animatable control, IObservable Match, Action onComplete) { - if (!_isVerfifiedAndConverted) + if (!isVerfifiedAndConverted) VerifyConvertKeyFrames(); - return obsMatch - // Ignore triggers when global timers are paused. - .Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause) + return Match + .Where(p => p) .Subscribe(_ => { var timerObs = RunKeyFrames(animation, control, onComplete); @@ -101,9 +100,9 @@ namespace Avalonia.Animation { var stateMachine = new AnimatorStateMachine(animation, control, this, onComplete); - Timing.AnimationStateTimer + Timing.AnimationsTimer .TakeWhile(_ => !stateMachine.unsubscribe) - .Subscribe(p => stateMachine.Step(p.Item1, p.Item2, DoInterpolation)); + .Subscribe(p => stateMachine.Step(p, DoInterpolation)); return control.Bind((AvaloniaProperty)Property, stateMachine, BindingPriority.Animation); } @@ -124,7 +123,7 @@ namespace Avalonia.Animation } AddNeutralKeyFramesIfNeeded(); - _isVerfifiedAndConverted = true; + isVerfifiedAndConverted = true; } @@ -133,7 +132,7 @@ namespace Avalonia.Animation bool hasStartKey, hasEndKey; hasStartKey = hasEndKey = false; - // Make start and end keyframe mandatory. + // Check if there's start and end keyframes. foreach (var converted in _convertedKeyframes.Keys) { if (DoubleUtils.AboutEqual(converted, 0.0)) diff --git a/src/Avalonia.Animation/Timing.cs b/src/Avalonia.Animation/Timing.cs index fb61e15f05..c6def06b15 100644 --- a/src/Avalonia.Animation/Timing.cs +++ b/src/Avalonia.Animation/Timing.cs @@ -16,16 +16,18 @@ namespace Avalonia.Animation public static class Timing { static long _tickStartTimeStamp; - static PlayState _globalState = PlayState.Run; static long TicksPerFrame = Stopwatch.Frequency / FramesPerSecond; + /// + /// Gets or sets the animation play state for all animations + /// + public static PlayState GlobalPlayState { get; set; } = PlayState.Run; /// /// The number of frames per second. /// public const int FramesPerSecond = 60; - /// /// The time span of each frame. /// @@ -36,45 +38,20 @@ namespace Avalonia.Animation /// static Timing() { - _tickStartTimeStamp = Stopwatch.GetTimestamp(); var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance); - - AnimationStateTimer = globalTimer + AnimationsTimer = globalTimer .Select(_ => { - return (_globalState, (Stopwatch.GetTimestamp() - _tickStartTimeStamp) - / TicksPerFrame); + return (Stopwatch.GetTimestamp() - _tickStartTimeStamp) + / TicksPerFrame * 2; }) .Publish() .RefCount(); - - TransitionsTimer = globalTimer - .Select(p => p) - .Publish() - .RefCount(); - } - - - /// - /// Sets the animation play state for all animations - /// - public static void SetGlobalPlayState(PlayState playState) - { - Dispatcher.UIThread.VerifyAccess(); - _globalState = playState; } - /// - /// Gets the animation play state for all animations - /// - public static PlayState GetGlobalPlayState() - { - Dispatcher.UIThread.VerifyAccess(); - return _globalState; - } /// /// Gets the animation timer. @@ -84,21 +61,7 @@ namespace Avalonia.Animation /// defined in . /// The parameter passed to a subsciber is the current playstate of the animation. /// - internal static IObservable<(PlayState, long)> AnimationStateTimer - { - get; - } - - /// - /// Gets the transitions timer. - /// - /// - /// The transitions timer increments usually 60 times per second as - /// defined in . - /// The parameter passed to a subsciber is the number of frames since the animation system was - /// initialized. - /// - public static IObservable TransitionsTimer + internal static IObservable AnimationsTimer { get; } @@ -116,10 +79,12 @@ namespace Avalonia.Animation /// public static IObservable GetTransitionsTimer(Animatable control, TimeSpan duration, TimeSpan delay = default(TimeSpan)) { + // TODO: Fix this mess. var _duration = (duration.Ticks / FrameTick.Ticks); - var endTime = _duration; + long? endTime = ((Stopwatch.GetTimestamp() - _tickStartTimeStamp) + / TicksPerFrame) + _duration; - return TransitionsTimer + return AnimationsTimer .TakeWhile(x => x < endTime) .Select(x => (double)x / _duration) .StartWith(0.0)