Browse Source

Add PlayState support.

Redoing the main algorithm yet again.
pull/1793/head
Jumar Macato 8 years ago
parent
commit
714606b2ad
No known key found for this signature in database GPG Key ID: B19884DAC3A5BF3F
  1. 10
      samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs
  2. 21
      src/Avalonia.Animation/Animatable.cs
  3. 87
      src/Avalonia.Animation/Animation.cs
  4. 92
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  5. 21
      src/Avalonia.Animation/Animator`1.cs
  6. 59
      src/Avalonia.Animation/Timing.cs

10
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; }
}
}
}

21
src/Avalonia.Animation/Animatable.cs

@ -23,25 +23,8 @@ namespace Avalonia.Animation
/// </summary>
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();
}
/// <summary>
/// The specific animations timer for this control.
/// </summary>
/// <returns></returns>
public IObservable<(PlayState, long)> AnimatableTimer;
Transitions = new Transitions();
}
/// <summary>
/// Defines the <see cref="PlayState"/> property.

87
src/Avalonia.Animation/Animation.cs

@ -21,70 +21,81 @@ namespace Avalonia.Animation
/// </summary>
public class Animation : AvaloniaList<KeyFrame>, IAnimation
{
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
{
( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) )
};
public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> 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<IAnimator> _animators { get; set; } = new AvaloniaList<IAnimator>();
/// <summary>
/// Run time of this animation.
/// Gets or sets the active time of this animation.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Delay time for this animation.
/// </summary>
public TimeSpan Delay { get; set; }
/// <summary>
/// The repeat count for this animation.
/// Gets or sets the repeat count for this animation.
/// </summary>
public RepeatCount RepeatCount { get; set; }
/// <summary>
/// The playback direction for this animation.
/// Gets or sets the playback direction for this animation.
/// </summary>
public PlaybackDirection PlaybackDirection { get; set; }
/// <summary>
/// The value fill mode for this animation.
/// Gets or sets the value fill mode for this animation.
/// </summary>
public FillMode FillMode { get; set; }
/// <summary>
/// Easing function to be used.
/// Gets or sets the easing function to be used for this animation.
/// </summary>
public Easing Easing { get; set; } = new LinearEasing();
/// <summary>
/// Sets the speed multiple for this animation.
/// Gets or sets the speed multiple for this animation.
/// </summary>
public double SpeedRatio { get; set; } = 1d;
/// <summary>
/// Sets the behavior for having a delay between repeats for this animation.
/// </summary>
public bool DelayBetweenRepeats { get; set; }
/// <summary>
/// Gets or sets the delay time for this animation.
/// </summary>
/// <remarks>
/// Describes a delay to be added before the animation starts, and optionally between
/// repeats of the animation if <see cref="DelayBetweenIterations"/> is set.
/// </remarks>
public TimeSpan Delay { get; set; }
/// <summary>
/// Gets or sets a value indicating whether <see cref="Delay"/> will be applied between
/// iterations of the animation.
/// </summary>
/// <remarks>
/// If this property is not set, then <see cref="Delay"/> will only be applied to the first
/// iteration of the animation.
/// </remarks>
public bool DelayBetweenIterations { get; set; }
private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
{
( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) )
};
public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> 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<IAnimator> Animators, IList<IDisposable> subscriptions) InterpretKeyframes(Animatable control)
{

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

@ -9,7 +9,7 @@ namespace Avalonia.Animation
/// Provides statefulness for an iteration of a keyframe animation.
/// </summary>
internal class AnimatorStateMachine<T> : IObservable<T>, 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<T> 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<double, T, T> Interpolator)
public void Step(long frameTick, Func<double, T, T> 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<double, T, T> Interpolator)
private void InternalStep(long time, Func<double, T, T> 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;

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

@ -21,7 +21,7 @@ namespace Avalonia.Animation
/// </summary>
private readonly SortedList<double, (AnimatorKeyFrame, bool isNeutral)> _convertedKeyframes = new SortedList<double, (AnimatorKeyFrame, bool)>();
private bool _isVerfifiedAndConverted;
private bool isVerfifiedAndConverted;
/// <summary>
/// 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; };
}
/// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch, Action onComplete)
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> 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<T>(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<T>((AvaloniaProperty<T>)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))

59
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;
/// <summary>
/// Gets or sets the animation play state for all animations
/// </summary>
public static PlayState GlobalPlayState { get; set; } = PlayState.Run;
/// <summary>
/// The number of frames per second.
/// </summary>
public const int FramesPerSecond = 60;
/// <summary>
/// The time span of each frame.
/// </summary>
@ -36,45 +38,20 @@ namespace Avalonia.Animation
/// </summary>
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();
}
/// <summary>
/// Sets the animation play state for all animations
/// </summary>
public static void SetGlobalPlayState(PlayState playState)
{
Dispatcher.UIThread.VerifyAccess();
_globalState = playState;
}
/// <summary>
/// Gets the animation play state for all animations
/// </summary>
public static PlayState GetGlobalPlayState()
{
Dispatcher.UIThread.VerifyAccess();
return _globalState;
}
/// <summary>
/// Gets the animation timer.
@ -84,21 +61,7 @@ namespace Avalonia.Animation
/// defined in <see cref="FramesPerSecond"/>.
/// The parameter passed to a subsciber is the current playstate of the animation.
/// </remarks>
internal static IObservable<(PlayState, long)> AnimationStateTimer
{
get;
}
/// <summary>
/// Gets the transitions timer.
/// </summary>
/// <remarks>
/// The transitions timer increments usually 60 times per second as
/// defined in <see cref="FramesPerSecond"/>.
/// The parameter passed to a subsciber is the number of frames since the animation system was
/// initialized.
/// </remarks>
public static IObservable<long> TransitionsTimer
internal static IObservable<long> AnimationsTimer
{
get;
}
@ -116,10 +79,12 @@ namespace Avalonia.Animation
/// </remarks>
public static IObservable<double> 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)

Loading…
Cancel
Save