Browse Source
*Implement DoubleKeyFrames for Properties such as Opacity, etc. *Implement TransformKeyFrames with initial implementation of specialized logic for selecting RenderTransform of the target control properly. *Ported RenderTest to .NET Core for testing and to remove the crufty old .csproj format. *Replaced AnimationsPage with some samples of hover-activated animated red rectangles.pull/1461/head
65 changed files with 756 additions and 247 deletions
@ -1,2 +1,72 @@ |
|||||
<UserControl xmlns="https://github.com/avaloniaui"> |
<UserControl xmlns="https://github.com/avaloniaui"> |
||||
|
<Grid> |
||||
|
<Grid.Styles> |
||||
|
<Styles> |
||||
|
<Style Selector="Rectangle.Test"> |
||||
|
<Setter Property="Fill" Value="Red"/> |
||||
|
<Setter Property="Margin" Value="15"/> |
||||
|
<Setter Property="Width" Value="100"/> |
||||
|
<Setter Property="Height" Value="100"/> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Rectangle.Rect1:pointerover"> |
||||
|
<Style.Animations> |
||||
|
<Animation Duration="0:0:2.5" Easing="BounceEaseInOut"> |
||||
|
<TransformKeyFrames Property="RotateTransform.Angle"> |
||||
|
<KeyFrame Cue="0%" Value="0"/> |
||||
|
<KeyFrame Cue="100%" Value="360"/> |
||||
|
</TransformKeyFrames> |
||||
|
</Animation> |
||||
|
</Style.Animations> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Rectangle.Rect2:pointerover"> |
||||
|
<Style.Animations> |
||||
|
<Animation Duration="0:0:0.5" Easing="SineEaseInOut"> |
||||
|
<TransformKeyFrames Property="ScaleTransform.ScaleX"> |
||||
|
<KeyFrame Cue="0%" Value="0.8"/> |
||||
|
<KeyFrame Cue="100%" Value="1"/> |
||||
|
</TransformKeyFrames> |
||||
|
<TransformKeyFrames Property="ScaleTransform.ScaleY"> |
||||
|
<KeyFrame Cue="0%" Value="0.8"/> |
||||
|
<KeyFrame Cue="100%" Value="1"/> |
||||
|
</TransformKeyFrames> |
||||
|
</Animation> |
||||
|
</Style.Animations> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="Rectangle.Rect3:pointerover"> |
||||
|
<Style.Animations> |
||||
|
<Animation Duration="0:0:3" Easing="BounceEaseInOut"> |
||||
|
<TransformKeyFrames Property="TranslateTransform.Y"> |
||||
|
<KeyFrame Cue="0%" Value="0"/> |
||||
|
<KeyFrame Cue="50%" Value="-100"/> |
||||
|
<KeyFrame Cue="100%" Value="0"/> |
||||
|
</TransformKeyFrames> |
||||
|
</Animation> |
||||
|
</Style.Animations> |
||||
|
</Style> |
||||
|
</Styles> |
||||
|
</Grid.Styles> |
||||
|
<StackPanel VerticalAlignment="Center" |
||||
|
HorizontalAlignment="Center" |
||||
|
Orientation="Horizontal" |
||||
|
ClipToBounds="False"> |
||||
|
<Rectangle Classes="Test Rect1"> |
||||
|
<Rectangle.RenderTransform> |
||||
|
<RotateTransform/> |
||||
|
</Rectangle.RenderTransform> |
||||
|
</Rectangle> |
||||
|
<Rectangle Classes="Test Rect2"> |
||||
|
<Rectangle.RenderTransform> |
||||
|
<ScaleTransform ScaleX="0.8" ScaleY="0.8"/> |
||||
|
</Rectangle.RenderTransform> |
||||
|
</Rectangle> |
||||
|
<Rectangle Classes="Test Rect3"> |
||||
|
<Rectangle.RenderTransform> |
||||
|
<TranslateTransform/> |
||||
|
</Rectangle.RenderTransform> |
||||
|
</Rectangle> |
||||
|
</StackPanel> |
||||
|
</Grid> |
||||
</UserControl> |
</UserControl> |
||||
@ -1,55 +1,72 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
// 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.
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using Avalonia.Animation.Easings; |
||||
|
using Avalonia.Animation.Keyframes; |
||||
|
using Avalonia.Collections; |
||||
|
using Avalonia.Metadata; |
||||
using System; |
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
namespace Avalonia.Animation |
namespace Avalonia.Animation |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Tracks the progress of an animation.
|
/// Tracks the progress of an animation.
|
||||
/// </summary>
|
/// </summary>
|
||||
public class Animation : IObservable<object>, IDisposable |
public class Animation : IDisposable, IAnimation |
||||
{ |
{ |
||||
|
private List<IDisposable>_subscription = new List<IDisposable>(); |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// The animation being tracked.
|
/// Run time of this animation.
|
||||
/// </summary>
|
/// </summary>
|
||||
private readonly IObservable<object> _inner; |
public TimeSpan Duration { get; set; } |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// The disposable used to cancel the animation.
|
/// Delay time for animation.
|
||||
/// </summary>
|
/// </summary>
|
||||
private readonly IDisposable _subscription; |
public TimeSpan Delay { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Easing function to be used.
|
||||
|
/// </summary>
|
||||
|
public Easing Easing { get; set; } = new LinearEasing(); |
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Animation"/> class.
|
/// A list of <see cref="IKeyFrames"/> objects.
|
||||
/// </summary>
|
/// </summary>
|
||||
/// <param name="inner">The animation observable being tracked.</param>
|
[Content] |
||||
/// <param name="subscription">A disposable used to cancel the animation.</param>
|
public AvaloniaList<IKeyFrames> Children { get; set; } = new AvaloniaList<IKeyFrames>(); |
||||
public Animation(IObservable<object> inner, IDisposable subscription) |
|
||||
{ |
|
||||
_inner = inner; |
|
||||
_subscription = subscription; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
/// <summary>
|
||||
/// Cancels the animation.
|
/// Cancels the animation.
|
||||
/// </summary>
|
/// </summary>
|
||||
public void Dispose() |
public void Dispose() |
||||
{ |
{ |
||||
_subscription.Dispose(); |
foreach(var sub in _subscription) sub.Dispose(); |
||||
} |
} |
||||
|
|
||||
/// <summary>
|
/// <inheritdocs/>
|
||||
/// Notifies the provider that an observer is to receive notifications.
|
public IDisposable Apply(Animatable control, IObservable<bool> matchObs) |
||||
/// </summary>
|
|
||||
/// <param name="observer">The observer.</param>
|
|
||||
/// <returns>
|
|
||||
/// A reference to an interface that allows observers to stop receiving notifications
|
|
||||
/// before the provider has finished sending them.
|
|
||||
/// </returns>
|
|
||||
public IDisposable Subscribe(IObserver<object> observer) |
|
||||
{ |
{ |
||||
return _inner.Subscribe(observer); |
foreach (IKeyFrames keyframes in Children) |
||||
|
{ |
||||
|
_subscription.Add(keyframes.Apply(this, control, matchObs)); |
||||
|
} |
||||
|
return this; |
||||
} |
} |
||||
|
|
||||
|
///// <summary>
|
||||
|
///// Notifies the provider that an observer is to receive notifications.
|
||||
|
///// </summary>
|
||||
|
///// <param name="observer">The observer.</param>
|
||||
|
///// <returns>
|
||||
|
///// A reference to an interface that allows observers to stop receiving notifications
|
||||
|
///// before the provider has finished sending them.
|
||||
|
///// </returns>
|
||||
|
//public IDisposable Subscribe(IObserver<object> observer)
|
||||
|
//{
|
||||
|
// return _inner.Subscribe(observer);
|
||||
|
//}
|
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,17 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Avalonia.Animation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Interface for Animation objects
|
||||
|
/// </summary>
|
||||
|
public interface IAnimation |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Apply the animation to the specified control
|
||||
|
/// </summary>
|
||||
|
IDisposable Apply(Animatable control, IObservable<bool> match); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,88 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.ComponentModel; |
||||
|
using System.Globalization; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Avalonia.Animation.Keyframes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A Cue object for <see cref="KeyFrame"/>.
|
||||
|
/// </summary>
|
||||
|
[TypeConverter(typeof(CueTypeConverter))] |
||||
|
public struct Cue : IEquatable<Cue>, IEquatable<double> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The normalized percent value, ranging from 0.0 to 1.0
|
||||
|
/// </summary>
|
||||
|
public double CueValue { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets a new <see cref="Cue"/> object.
|
||||
|
/// </summary>
|
||||
|
/// <param name="value"></param>
|
||||
|
public Cue(double value) |
||||
|
{ |
||||
|
if (value <= 1 && value >= 0) |
||||
|
CueValue = value; |
||||
|
else |
||||
|
throw new ArgumentException($"This cue object's value should be within or equal to 0.0 and 1.0"); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses a string to a <see cref="Cue"/> object.
|
||||
|
/// </summary>
|
||||
|
public static object Parse(string value, CultureInfo culture) |
||||
|
{ |
||||
|
string v = value; |
||||
|
|
||||
|
if (value.EndsWith("%")) |
||||
|
{ |
||||
|
v = v.TrimEnd('%'); |
||||
|
} |
||||
|
|
||||
|
if (double.TryParse(v, NumberStyles.Float, culture, out double res)) |
||||
|
{ |
||||
|
return new Cue(res / 100d); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new FormatException($"Invalid Cue string \"{value}\""); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Checks for equality between two <see cref="Cue"/>s.
|
||||
|
/// </summary>
|
||||
|
/// <param name="other">The second cue.</param>
|
||||
|
public bool Equals(Cue other) |
||||
|
{ |
||||
|
return CueValue == other.CueValue; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Checks for equality between a <see cref="Cue"/>
|
||||
|
/// and a <see cref="double"/> value.
|
||||
|
/// </summary>
|
||||
|
/// <param name="other"></param>
|
||||
|
/// <returns></returns>
|
||||
|
public bool Equals(double other) |
||||
|
{ |
||||
|
return CueValue == other; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class CueTypeConverter : TypeConverter |
||||
|
{ |
||||
|
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
||||
|
{ |
||||
|
return sourceType == typeof(string); |
||||
|
} |
||||
|
|
||||
|
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
||||
|
{ |
||||
|
return Cue.Parse((string)value, culture); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
using System.Linq; |
||||
|
using System.Reactive.Linq; |
||||
|
using System.Diagnostics; |
||||
|
using Avalonia.Animation.Utils; |
||||
|
using Avalonia.Data; |
||||
|
|
||||
|
namespace Avalonia.Animation.Keyframes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Key frames that handles <see cref="double"/> properties.
|
||||
|
/// </summary>
|
||||
|
public class DoubleKeyFrames : KeyFrames<double> |
||||
|
{ |
||||
|
/// <inheritdocs/>
|
||||
|
public override IDisposable DoInterpolation(Animation animation, Animatable control, Dictionary<double, double> sortedkeyValues) |
||||
|
{ |
||||
|
var timer = Timing.GetTimer(animation.Duration, animation.Delay); |
||||
|
|
||||
|
|
||||
|
var interp = timer.Select(p => |
||||
|
{ |
||||
|
// Handle the errors rather naively, for now.
|
||||
|
try |
||||
|
{ |
||||
|
var x = animation.Easing.Ease(p); |
||||
|
|
||||
|
// Get a pair of keyframes to make the interpolation.
|
||||
|
KeyValuePair<double, double> firstCue, lastCue; |
||||
|
|
||||
|
firstCue = sortedkeyValues.First(); |
||||
|
lastCue = sortedkeyValues.Last(); |
||||
|
|
||||
|
// This should be changed later for a much more efficient one
|
||||
|
if (sortedkeyValues.Count() > 2) |
||||
|
{ |
||||
|
bool isWithinRange_Start = DoubleUtils.AboutEqual(x, 0.0) || x > 0.0; |
||||
|
bool isWithinRange_End = DoubleUtils.AboutEqual(x, 1.0) || x < 1.0; |
||||
|
|
||||
|
if (isWithinRange_Start && isWithinRange_End) |
||||
|
{ |
||||
|
|
||||
|
firstCue = sortedkeyValues.Where(j => j.Key <= x).Last(); |
||||
|
lastCue = sortedkeyValues.Where(j=> j.Key >= firstCue.Key).First(); |
||||
|
} |
||||
|
else if (!isWithinRange_Start) |
||||
|
{ |
||||
|
firstCue = sortedkeyValues.First(); |
||||
|
lastCue = sortedkeyValues.Skip(1).First(); |
||||
|
} |
||||
|
else if (!isWithinRange_End) |
||||
|
{ |
||||
|
firstCue = sortedkeyValues.Skip(sortedkeyValues.Count() - 1).First(); |
||||
|
lastCue = sortedkeyValues.Last(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new InvalidOperationException |
||||
|
($"Can't find KeyFrames within the specified Easing time {x}"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Piecewise Linear interpolation, courtesy of wikipedia
|
||||
|
var y0 = firstCue.Value; |
||||
|
var x0 = firstCue.Key; |
||||
|
var y1 = lastCue.Value; |
||||
|
var x1 = lastCue.Key; |
||||
|
var y = ((y0 * (x1 - x)) + (y1 * (x - x0))) / x1 - x0; |
||||
|
|
||||
|
return y; |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
Debug.WriteLine(e); |
||||
|
return 1; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
return control.Bind(Property, interp.Select(p => (object)p), BindingPriority.Animation); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Avalonia.Animation.Keyframes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Interface for Keyframe group object
|
||||
|
/// </summary>
|
||||
|
public interface IKeyFrames |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Applies the current KeyFrame group to the specified control.
|
||||
|
/// </summary>
|
||||
|
IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
using System.ComponentModel; |
||||
|
|
||||
|
namespace Avalonia.Animation.Keyframes |
||||
|
{ |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Stores data regarding a specific key
|
||||
|
/// point and value in an animation.
|
||||
|
/// </summary>
|
||||
|
public class KeyFrame |
||||
|
{ |
||||
|
internal bool timeSpanSet, cueSet; |
||||
|
|
||||
|
private TimeSpan _ktimeSpan; |
||||
|
private Cue _kCue; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the key time of this <see cref="KeyFrame"/>.
|
||||
|
/// </summary>
|
||||
|
/// <value>The key time.</value>
|
||||
|
public TimeSpan KeyTime |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
|
||||
|
return _ktimeSpan; |
||||
|
} |
||||
|
set |
||||
|
{ |
||||
|
if (cueSet) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); |
||||
|
} |
||||
|
timeSpanSet = true; |
||||
|
_ktimeSpan = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the cue of this <see cref="KeyFrame"/>.
|
||||
|
/// </summary>
|
||||
|
/// <value>The cue.</value>
|
||||
|
public Cue Cue |
||||
|
{ |
||||
|
get |
||||
|
{ |
||||
|
|
||||
|
return _kCue; |
||||
|
} |
||||
|
set |
||||
|
{ |
||||
|
if (timeSpanSet) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); |
||||
|
} |
||||
|
cueSet = true; |
||||
|
_kCue = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public object Value { get; set; } |
||||
|
|
||||
|
|
||||
|
///// <summary>
|
||||
|
///// Initializes a new instance of the <see cref="KeyFrame"/> class.
|
||||
|
///// </summary>
|
||||
|
//public KeyFrame()
|
||||
|
//{
|
||||
|
|
||||
|
//}
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,122 @@ |
|||||
|
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; |
||||
|
|
||||
|
namespace Avalonia.Animation.Keyframes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Base class for KeyFrames
|
||||
|
/// </summary>
|
||||
|
public abstract class KeyFrames<T> : AvaloniaList<KeyFrame>, IKeyFrames |
||||
|
{ |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Target property.
|
||||
|
/// </summary>
|
||||
|
public AvaloniaProperty Property { get; set; } |
||||
|
|
||||
|
/// Enable if the derived class will do the verification of
|
||||
|
/// its keyframes.
|
||||
|
internal bool IsVerfifiedAndConverted; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch) |
||||
|
{ |
||||
|
if(obsMatch == null) return null; |
||||
|
|
||||
|
if (!IsVerfifiedAndConverted) |
||||
|
VerifyKeyFrames(animation, typeof(T)); |
||||
|
|
||||
|
return obsMatch |
||||
|
.Where(p => p == true) |
||||
|
.Subscribe(_ => DoInterpolation(animation, control, ConvertedValues)); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Interpolates the given keyframes to the control.
|
||||
|
/// </summary>
|
||||
|
public abstract IDisposable DoInterpolation(Animation animation, |
||||
|
Animatable control, |
||||
|
Dictionary<double, T> keyValues); |
||||
|
|
||||
|
internal Dictionary<double, T> ConvertedValues = new Dictionary<double, T>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies keyframe value types.
|
||||
|
/// </summary>
|
||||
|
private void VerifyKeyFrames(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); |
||||
|
} |
||||
|
|
||||
|
ConvertedValues.Add(_normalizedCue.CueValue, convertedValue); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
// This can be optional if we ever try to make
|
||||
|
// the default start and end values to be the
|
||||
|
// property's prior value.
|
||||
|
SortKeyFrameCues(ConvertedValues); |
||||
|
|
||||
|
IsVerfifiedAndConverted = true; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
private void SortKeyFrameCues(Dictionary<double, T> convertedValues) |
||||
|
{ |
||||
|
SortKeyFrameCues(convertedValues.ToDictionary((k) => k.Key, (v) => (object)v.Value)); |
||||
|
} |
||||
|
|
||||
|
internal void SortKeyFrameCues(Dictionary<double, object> convertedValues) |
||||
|
{ |
||||
|
bool hasStartKey, hasEndKey; |
||||
|
hasStartKey = hasEndKey = false; |
||||
|
|
||||
|
foreach (var converted in ConvertedValues.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.
|
||||
|
ConvertedValues = ConvertedValues.OrderBy(p => p.Key) |
||||
|
.ToDictionary((k) => k.Key, (v) => v.Value); |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,12 +1,12 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
// 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.
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
namespace Avalonia.Animation |
namespace Avalonia.Animation.Utils |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Helper static class for BounceEase classes.
|
/// Helper static class for BounceEase classes.
|
||||
/// </summary>
|
/// </summary>
|
||||
internal static class BounceEaseHelper |
internal static class BounceEaseUtils |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Returns the consequent <see cref="double"/> value of
|
/// Returns the consequent <see cref="double"/> value of
|
||||
@ -0,0 +1,77 @@ |
|||||
|
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.Media; |
||||
|
|
||||
|
namespace Avalonia.Animation.Keyframes |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Key frames that handles <see cref="double"/> properties.
|
||||
|
/// </summary>
|
||||
|
public class TransformKeyFrames : KeyFrames<double> |
||||
|
{ |
||||
|
DoubleKeyFrames childKeyFrames; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch) |
||||
|
{ |
||||
|
var ctrl = (Visual)control; |
||||
|
|
||||
|
// Check if the AvaloniaProperty is Transform derived.
|
||||
|
if (typeof(Transform).IsAssignableFrom(Property.OwnerType)) |
||||
|
{ |
||||
|
var renderTransformType = ctrl.RenderTransform.GetType(); |
||||
|
|
||||
|
// It's only 1 transform object so let's target that.
|
||||
|
if (renderTransformType == Property.OwnerType) |
||||
|
{ |
||||
|
var targetTransform = Convert.ChangeType(ctrl.RenderTransform, Property.OwnerType); |
||||
|
|
||||
|
if (childKeyFrames == null) |
||||
|
{ |
||||
|
childKeyFrames = new DoubleKeyFrames(); |
||||
|
|
||||
|
foreach (KeyFrame k in this) |
||||
|
{ |
||||
|
childKeyFrames.Add(k); |
||||
|
} |
||||
|
|
||||
|
childKeyFrames.Property = Property; |
||||
|
} |
||||
|
|
||||
|
return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch); |
||||
|
} |
||||
|
if (renderTransformType == typeof(TransformGroup)) |
||||
|
{ |
||||
|
foreach (Transform t in ((TransformGroup)ctrl.RenderTransform).Children) |
||||
|
{ |
||||
|
if (renderTransformType == Property.OwnerType) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// not existing in the transform
|
||||
|
|
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw new InvalidProgramException($"Unsupported property {Property}"); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdocs/>
|
||||
|
public override IDisposable DoInterpolation(Animation animation, Animatable control, Dictionary<double, double> keyValues) |
||||
|
{ |
||||
|
return Timing.GetTimer(animation.Duration, animation.Delay).Subscribe(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue