// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Animation.Utils;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation
{
///
/// Base class for KeyFrames objects
///
public abstract class Animator : AvaloniaList, IAnimator
{
///
/// List of type-converted keyframes.
///
private readonly List _convertedKeyframes = new List();
private bool _isVerifiedAndConverted;
///
/// Gets or sets the target property for the keyframe.
///
public AvaloniaProperty Property { get; set; }
public Animator()
{
// Invalidate keyframes when changed.
this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
}
///
public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable match, Action onComplete)
{
if (!_isVerifiedAndConverted)
VerifyConvertKeyFrames();
var subject = new DisposeAnimationInstanceSubject(this, animation, control, clock, onComplete);
return match.Subscribe(subject);
}
///
/// Get the nearest pair of cue-time ordered keyframes
/// according to the given time parameter that is relative to the
/// total animation time and the normalized intra-keyframe pair time
/// (i.e., the normalized time between the selected keyframes, relative to the
/// time parameter).
///
/// The time parameter, relative to the total animation time
protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double animationTime)
{
AnimatorKeyFrame firstKeyframe, lastKeyframe;
int kvCount = _convertedKeyframes.Count;
if (kvCount > 2)
{
if (animationTime <= 0.0)
{
firstKeyframe = _convertedKeyframes[0];
lastKeyframe = _convertedKeyframes[1];
}
else if (animationTime >= 1.0)
{
firstKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 2];
lastKeyframe = _convertedKeyframes[_convertedKeyframes.Count - 1];
}
else
{
int index = FindClosestBeforeKeyFrame(animationTime);
firstKeyframe = _convertedKeyframes[index];
lastKeyframe = _convertedKeyframes[index + 1];
}
}
else
{
firstKeyframe = _convertedKeyframes[0];
lastKeyframe = _convertedKeyframes[1];
}
double t0 = firstKeyframe.Cue.CueValue;
double t1 = lastKeyframe.Cue.CueValue;
var intraframeTime = (animationTime - t0) / (t1 - t0);
var firstFrameData = (firstKeyframe.GetTypedValue(), firstKeyframe.isNeutral);
var lastFrameData = (lastKeyframe.GetTypedValue(), lastKeyframe.isNeutral);
return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData));
}
private int FindClosestBeforeKeyFrame(double time)
{
int FindClosestBeforeKeyFrame(int startIndex, int length)
{
if (length == 0 || length == 1)
{
return startIndex;
}
int middle = startIndex + (length / 2);
if (_convertedKeyframes[middle].Cue.CueValue < time)
{
return FindClosestBeforeKeyFrame(middle, length - middle);
}
else if (_convertedKeyframes[middle].Cue.CueValue > time)
{
return FindClosestBeforeKeyFrame(startIndex, middle - startIndex);
}
else
{
return middle;
}
}
return FindClosestBeforeKeyFrame(0, _convertedKeyframes.Count);
}
///
/// Runs the KeyFrames Animation.
///
internal IDisposable Run(Animation animation, Animatable control, IClock clock, Action onComplete)
{
var instance = new AnimationInstance(
animation,
control,
this,
clock ?? control.Clock ?? Clock.GlobalClock,
onComplete,
DoInterpolation);
return control.Bind((AvaloniaProperty)Property, instance, BindingPriority.Animation);
}
///
/// Interpolates a value given the desired time.
///
protected abstract T DoInterpolation(double time, T neutralValue);
///
/// Verifies, converts and sorts keyframe values according to this class's target type.
///
private void VerifyConvertKeyFrames()
{
foreach (AnimatorKeyFrame keyframe in this)
{
_convertedKeyframes.Add(keyframe);
}
AddNeutralKeyFramesIfNeeded();
_isVerifiedAndConverted = true;
}
private void AddNeutralKeyFramesIfNeeded()
{
bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false;
// Check if there's start and end keyframes.
foreach (var frame in _convertedKeyframes)
{
if (frame.Cue.CueValue == 0.0d)
{
hasStartKey = true;
}
else if (frame.Cue.CueValue == 1.0d)
{
hasEndKey = true;
}
}
if (!hasStartKey || !hasEndKey)
AddNeutralKeyFrames(hasStartKey, hasEndKey);
}
private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey)
{
if (!hasStartKey)
{
_convertedKeyframes.Insert(0, new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true });
}
if (!hasEndKey)
{
_convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(1.0d)) { Value = default(T), isNeutral = true });
}
}
}
}