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.
 
 
 

169 lines
5.8 KiB

using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Collections;
using System.ComponentModel;
using Avalonia.Animation.Utils;
using System.Reactive.Linq;
using System.Linq;
using Avalonia.Data;
using System.Reactive.Disposables;
namespace Avalonia.Animation.Keyframes
{
/// <summary>
/// Base class for KeyFrames objects
/// </summary>
public abstract class KeyFrames<T> : AvaloniaList<KeyFrame>, IKeyFrames
{
/// <summary>
/// Target property.
/// </summary>
public AvaloniaProperty Property { get; set; }
/// <summary>
/// List of type-converted keyframes.
/// </summary>
public Dictionary<double, T> ConvertedKeyframes = new Dictionary<double, T>();
private bool IsVerfifiedAndConverted;
/// <inheritdoc/>
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
{
if (!IsVerfifiedAndConverted)
VerifyConvertKeyFrames(animation, typeof(T));
return obsMatch
.Where(p => p == true)
// Ignore triggers when global timers are paused.
.Where(p=> Timing.GetGlobalPlayState() != PlayState.Paused)
.Subscribe(_ =>
{
var interp = DoInterpolation(animation, control)
.Select(p => (object)p);
control.Bind(Property, interp, BindingPriority.Animation);
});
}
/// <summary>
/// Get the nearest pair of cue-time ordered keyframes
/// according to the given time parameter.
/// </summary>
public KeyFramePair<T> GetKeyFramePairByTime(double t)
{
KeyValuePair<double, T> firstCue, lastCue;
int kvCount = ConvertedKeyframes.Count();
if (kvCount > 2)
{
if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
{
firstCue = ConvertedKeyframes.First();
lastCue = ConvertedKeyframes.Skip(1).First();
}
else if (DoubleUtils.AboutEqual(t, 1.0) || t > 1.0)
{
firstCue = ConvertedKeyframes.Skip(kvCount - 2).First();
lastCue = ConvertedKeyframes.Last();
}
else
{
firstCue = ConvertedKeyframes.Where(j => j.Key <= t).Last();
lastCue = ConvertedKeyframes.Where(j => j.Key >= t).First();
}
}
else
{
firstCue = ConvertedKeyframes.First();
lastCue = ConvertedKeyframes.Last();
}
return new KeyFramePair<T>(firstCue, lastCue);
}
/// <summary>
/// Returns an observable timer with the specific Animation
/// duration and delay and applies the Animation's easing function.
/// </summary>
public IObservable<(double Time, Animatable Target)>
SetupAnimation(Animation animation, Animatable control) =>
Timing.GetAnimationsTimer(control, animation.Duration, animation.Delay)
.Select(t => (animation.Easing.Ease(t), control));
/// <summary>
/// Interpolates the given keyframes to the control.
/// </summary>
public abstract IObservable<T> DoInterpolation(Animation animation,
Animatable control);
/// <summary>
/// Verifies and converts keyframe values according to this class type parameter.
/// </summary>
private void VerifyConvertKeyFrames(Animation animation, Type type)
{
var typeConv = TypeDescriptor.GetConverter(type);
foreach (KeyFrame k in this)
{
if (k.Value == null)
{
throw new ArgumentNullException($"KeyFrame value can't be null.");
}
if (!typeConv.CanConvertTo(k.Value.GetType()))
{
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
T convertedValue = (T)typeConv.ConvertTo(k.Value, type);
Cue _normalizedCue = k.Cue;
if (k.timeSpanSet)
{
_normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks);
}
ConvertedKeyframes.Add(_normalizedCue.CueValue, convertedValue);
}
SortKeyFrameCues(ConvertedKeyframes);
IsVerfifiedAndConverted = true;
}
private void SortKeyFrameCues(Dictionary<double, T> convertedValues)
{
bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false;
// this can be optional later, by making the default start/end keyframes
// to have a neutral value (a.k.a. the value prior to the animation).
foreach (var converted in ConvertedKeyframes.Keys)
{
if (DoubleUtils.AboutEqual(converted, 0.0))
{
hasStartKey = true;
}
else if (DoubleUtils.AboutEqual(converted, 1.0))
{
hasEndKey = true;
}
}
if (!hasStartKey && !hasEndKey)
throw new InvalidOperationException
($"{this.GetType().Name} must have a starting (0% cue) and ending (100% cue) keyframe.");
// Sort Cues, in case they don't order it by themselves.
ConvertedKeyframes = ConvertedKeyframes.OrderBy(p => p.Key)
.ToDictionary((k) => k.Key, (v) => v.Value);
}
}
}