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.
194 lines
6.2 KiB
194 lines
6.2 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 Avalonia.Animation.Easings;
|
|
using Avalonia.Animation;
|
|
using Avalonia.Collections;
|
|
using Avalonia.Metadata;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Reflection;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using System.Reactive.Linq;
|
|
|
|
namespace Avalonia.Animation
|
|
{
|
|
/// <summary>
|
|
/// Tracks the progress of an animation.
|
|
/// </summary>
|
|
public class Animation : AvaloniaList<KeyFrame>, IDisposable, 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;
|
|
}
|
|
|
|
private bool _isChildrenChanged = false;
|
|
private List<IDisposable> _subscription = new List<IDisposable>();
|
|
public AvaloniaList<IAnimator> _animators { get; set; } = new AvaloniaList<IAnimator>();
|
|
|
|
/// <summary>
|
|
/// Run 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.
|
|
/// </summary>
|
|
public RepeatCount RepeatCount { get; set; }
|
|
|
|
/// <summary>
|
|
/// The playback direction for this animation.
|
|
/// </summary>
|
|
public PlaybackDirection PlaybackDirection { get; set; }
|
|
|
|
/// <summary>
|
|
/// The value fill mode for this animation.
|
|
/// </summary>
|
|
public FillMode FillMode { get; set; }
|
|
|
|
/// <summary>
|
|
/// Easing function to be used.
|
|
/// </summary>
|
|
public Easing Easing { get; set; } = new LinearEasing();
|
|
|
|
/// <summary>
|
|
/// Triggers when the animation is completed.
|
|
/// </summary>
|
|
public event EventHandler Done;
|
|
|
|
public Animation()
|
|
{
|
|
this.CollectionChanged += delegate { _isChildrenChanged = true; };
|
|
}
|
|
|
|
private IList<IAnimator> InterpretKeyframes(Animatable control)
|
|
{
|
|
var handlerList = new List<(Type type, AvaloniaProperty property)>();
|
|
var animatorKeyFrames = new List<AnimatorKeyFrame>();
|
|
|
|
foreach (var keyframe in this)
|
|
{
|
|
foreach (var setter in keyframe)
|
|
{
|
|
var handler = GetAnimatorType(setter.Property);
|
|
|
|
if (handler == null)
|
|
{
|
|
throw new InvalidOperationException($"No animator registered for the property {setter.Property}. Add an animator to the Animation.Animators collection that matches this property to animate it.");
|
|
}
|
|
|
|
if (!handlerList.Contains((handler, setter.Property)))
|
|
handlerList.Add((handler, setter.Property));
|
|
|
|
var cue = keyframe.Cue;
|
|
|
|
if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan)
|
|
{
|
|
cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks);
|
|
}
|
|
|
|
var newKF = new AnimatorKeyFrame(handler, cue);
|
|
|
|
_subscription.Add(newKF.BindSetter(setter, control));
|
|
|
|
animatorKeyFrames.Add(newKF);
|
|
}
|
|
}
|
|
|
|
var newAnimatorInstances = new List<IAnimator>();
|
|
|
|
foreach (var (handlerType, property) in handlerList)
|
|
{
|
|
var newInstance = (IAnimator)Activator.CreateInstance(handlerType);
|
|
newInstance.Property = property;
|
|
newAnimatorInstances.Add(newInstance);
|
|
}
|
|
|
|
foreach (var keyframe in animatorKeyFrames)
|
|
{
|
|
var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType &&
|
|
a.Property == keyframe.Property);
|
|
animator.Add(keyframe);
|
|
}
|
|
|
|
return newAnimatorInstances;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cancels the animation.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
foreach (var sub in _subscription)
|
|
{
|
|
sub.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <inheritdocs/>
|
|
public IDisposable Apply(Animatable control, IObservable<bool> matchObs)
|
|
{
|
|
foreach (IAnimator animator in InterpretKeyframes(control))
|
|
{
|
|
_subscription.Add(animator.Apply(this, control, matchObs));
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/// <inheritdocs/>
|
|
public Task RunAsync(Animatable control)
|
|
{
|
|
var run = new TaskCompletionSource<object>();
|
|
|
|
if (this.RepeatCount == RepeatCount.Loop)
|
|
run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
|
|
|
|
EventHandler doneCallback = null;
|
|
doneCallback = (sender, args) =>
|
|
{
|
|
if (sender == control)
|
|
{
|
|
run.SetResult(null);
|
|
this.Done -= doneCallback;
|
|
}
|
|
};
|
|
|
|
this.Done += doneCallback;
|
|
|
|
this.Apply(control, Observable.Return(true));
|
|
|
|
return run.Task;
|
|
}
|
|
|
|
internal void SetDone(Animatable control)
|
|
{
|
|
Done?.Invoke(control, null);
|
|
}
|
|
}
|
|
}
|
|
|