A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

220 lines
7.6 KiB

using System;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Animation.Animators;
using Avalonia.Animation.Utils;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
/// <summary>
/// Handles interpolation and time-related functions
/// for keyframe animations.
/// </summary>
internal class AnimationInstance<T> : SingleSubscriberObservableBase<T>
{
private T _lastInterpValue;
private T _firstKFValue;
private ulong? _iterationCount;
private ulong _currentIteration;
private bool _gotFirstKFValue;
private bool _playbackReversed;
private FillMode _fillMode;
private PlaybackDirection _playbackDirection;
private Animator<T> _animator;
private Animation _animation;
private Animatable _targetControl;
private T _neutralValue;
private double _speedRatioConv;
private TimeSpan _initialDelay;
private TimeSpan _iterationDelay;
private TimeSpan _duration;
private Easings.Easing _easeFunc;
private Action _onCompleteAction;
private Func<double, T, T> _interpolator;
private IDisposable _timerSub;
private readonly IClock _baseClock;
private IClock _clock;
public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action OnComplete, Func<double, T, T> Interpolator)
{
_animator = animator;
_animation = animation;
_targetControl = control;
_onCompleteAction = OnComplete;
_interpolator = Interpolator;
_baseClock = baseClock;
_neutralValue = (T)_targetControl.GetValue(_animator.Property);
FetchProperties();
}
private void FetchProperties()
{
if (_animation.SpeedRatio < 0d)
throw new ArgumentOutOfRangeException("SpeedRatio value should not be negative.");
if (_animation.Duration.TotalSeconds <= 0)
throw new InvalidOperationException("Duration value cannot be negative or zero.");
_easeFunc = _animation.Easing;
_speedRatioConv = 1d / _animation.SpeedRatio;
_initialDelay = _animation.Delay;
_duration = _animation.Duration;
_iterationDelay = _animation.DelayBetweenIterations;
if (_animation.IterationCount.RepeatType == IterationType.Many)
_iterationCount = _animation.IterationCount.Value;
else
_iterationCount = null;
_playbackDirection = _animation.PlaybackDirection;
_fillMode = _animation.FillMode;
}
protected override void Unsubscribed()
{
// Animation may have been stopped before it has finished.
ApplyFinalFill();
_timerSub?.Dispose();
_clock.PlayState = PlayState.Stop;
}
protected override void Subscribed()
{
_clock = new Clock(_baseClock);
_timerSub = _clock.Subscribe(Step);
}
public void Step(TimeSpan frameTick)
{
try
{
InternalStep(frameTick);
}
catch (Exception e)
{
PublishError(e);
}
}
private void ApplyFinalFill()
{
if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both)
_targetControl.SetValue(_animator.Property, _lastInterpValue, BindingPriority.LocalValue);
}
private void DoComplete()
{
ApplyFinalFill();
_onCompleteAction?.Invoke();
PublishCompleted();
}
private void DoDelay()
{
if (_fillMode == FillMode.Backward || _fillMode == FillMode.Both)
if (_currentIteration == 0)
PublishNext(_firstKFValue);
else
PublishNext(_lastInterpValue);
}
private void DoPlayStates()
{
if (_clock.PlayState == PlayState.Stop || _baseClock.PlayState == PlayState.Stop)
DoComplete();
if (!_gotFirstKFValue)
{
_firstKFValue = (T)_animator.First().Value;
_gotFirstKFValue = true;
}
}
private void InternalStep(TimeSpan time)
{
DoPlayStates();
FetchProperties();
// Scale timebases according to speedratio.
var indexTime = time.Ticks;
var iterDuration = _duration.Ticks * _speedRatioConv;
var iterDelay = _iterationDelay.Ticks * _speedRatioConv;
var initDelay = _initialDelay.Ticks * _speedRatioConv;
if (indexTime > 0 & indexTime <= initDelay)
{
DoDelay();
}
else
{
// Calculate timebases.
var iterationTime = iterDuration + iterDelay;
var opsTime = indexTime - initDelay;
var playbackTime = opsTime % iterationTime;
_currentIteration = (ulong)(opsTime / iterationTime);
// Stop animation when the current iteration is beyond the iteration count
// and snap the last iteration value to exact values.
if ((_currentIteration + 1) > _iterationCount)
{
var easedTime = _easeFunc.Ease(_playbackReversed ? 0.0 : 1.0);
_lastInterpValue = _interpolator(easedTime, _neutralValue);
DoComplete();
}
if (playbackTime <= iterDuration)
{
// Normalize time for interpolation.
var normalizedTime = playbackTime / iterDuration;
// Check if normalized time needs to be reversed according to PlaybackDirection
switch (_playbackDirection)
{
case PlaybackDirection.Normal:
_playbackReversed = false;
break;
case PlaybackDirection.Reverse:
_playbackReversed = true;
break;
case PlaybackDirection.Alternate:
_playbackReversed = (_currentIteration % 2 == 0) ? false : true;
break;
case PlaybackDirection.AlternateReverse:
_playbackReversed = (_currentIteration % 2 == 0) ? true : false;
break;
default:
throw new InvalidOperationException($"Animation direction value is unknown: {_playbackDirection}");
}
if (_playbackReversed)
normalizedTime = 1 - normalizedTime;
// Ease and interpolate
var easedTime = _easeFunc.Ease(normalizedTime);
_lastInterpValue = _interpolator(easedTime, _neutralValue);
PublishNext(_lastInterpValue);
}
else if (playbackTime > iterDuration &
playbackTime <= iterationTime &
iterDelay > 0)
{
// The last iteration's trailing delay should be skipped.
if ((_currentIteration + 1) < _iterationCount)
DoDelay();
else
DoComplete();
}
}
}
}
}