csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
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.
191 lines
6.7 KiB
191 lines
6.7 KiB
// 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
|
|
{
|
|
/// <summary>
|
|
/// Base class for KeyFrames objects
|
|
/// </summary>
|
|
public abstract class Animator<T> : AvaloniaList<AnimatorKeyFrame>, IAnimator
|
|
{
|
|
/// <summary>
|
|
/// List of type-converted keyframes.
|
|
/// </summary>
|
|
private readonly List<AnimatorKeyFrame> _convertedKeyframes = new List<AnimatorKeyFrame>();
|
|
|
|
private bool _isVerifiedAndConverted;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the target property for the keyframe.
|
|
/// </summary>
|
|
public AvaloniaProperty Property { get; set; }
|
|
|
|
public Animator()
|
|
{
|
|
// Invalidate keyframes when changed.
|
|
this.CollectionChanged += delegate { _isVerifiedAndConverted = false; };
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
|
|
{
|
|
if (!_isVerifiedAndConverted)
|
|
VerifyConvertKeyFrames();
|
|
|
|
var subject = new DisposeAnimationInstanceSubject<T>(this, animation, control, clock, onComplete);
|
|
return match.Subscribe(subject);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
/// <param name="animationTime">The time parameter, relative to the total animation time</param>
|
|
protected (double IntraKFTime, KeyFramePair<T> 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<T>(), firstKeyframe.isNeutral);
|
|
var lastFrameData = (lastKeyframe.GetTypedValue<T>(), lastKeyframe.isNeutral);
|
|
return (intraframeTime, new KeyFramePair<T>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs the KeyFrames Animation.
|
|
/// </summary>
|
|
internal IDisposable Run(Animation animation, Animatable control, IClock clock, Action onComplete)
|
|
{
|
|
var instance = new AnimationInstance<T>(
|
|
animation,
|
|
control,
|
|
this,
|
|
clock ?? control.Clock ?? Clock.GlobalClock,
|
|
onComplete,
|
|
DoInterpolation);
|
|
return control.Bind<T>((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Interpolates a value given the desired time.
|
|
/// </summary>
|
|
protected abstract T DoInterpolation(double time, T neutralValue);
|
|
|
|
/// <summary>
|
|
/// Verifies, converts and sorts keyframe values according to this class's target type.
|
|
/// </summary>
|
|
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 });
|
|
}
|
|
}
|
|
}
|
|
}
|