Browse Source

Merge pull request #1768 from jkoritzinsky/animation-setter-binding

Allow bindings in KeyFrames. Re-implement Indeterminate ProgressBar in XAML.
pull/1777/head
Jumar Macato 8 years ago
committed by GitHub
parent
commit
a39739b24d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 65
      src/Avalonia.Animation/Animation.cs
  2. 66
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  3. 188
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  4. 75
      src/Avalonia.Animation/Animator`1.cs
  5. 2
      src/Avalonia.Animation/Cue.cs
  6. 8
      src/Avalonia.Animation/DoubleAnimator.cs
  7. 2
      src/Avalonia.Animation/IAnimationSetter.cs
  8. 16
      src/Avalonia.Animation/KeyFrame.cs
  9. 8
      src/Avalonia.Animation/KeyFramePair`1.cs
  10. 90
      src/Avalonia.Controls/ProgressBar.cs
  11. 6
      src/Avalonia.Styling/Styling/Setter.cs
  12. 45
      src/Avalonia.Themes.Default/ProgressBar.xaml
  13. 4
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs
  14. 5
      src/Markup/Avalonia.Markup/Data/Binding.cs

65
src/Avalonia.Animation/Animation.cs

@ -68,7 +68,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// The value fill mode for this animation. /// The value fill mode for this animation.
/// </summary> /// </summary>
public FillMode FillMode { get; set; } public FillMode FillMode { get; set; }
/// <summary> /// <summary>
/// Easing function to be used. /// Easing function to be used.
@ -80,10 +80,10 @@ namespace Avalonia.Animation
this.CollectionChanged += delegate { _isChildrenChanged = true; }; this.CollectionChanged += delegate { _isChildrenChanged = true; };
} }
private void InterpretKeyframes() private IList<IAnimator> InterpretKeyframes(Animatable control)
{ {
var handlerList = new List<(Type, AvaloniaProperty)>(); var handlerList = new List<(Type type, AvaloniaProperty property)>();
var kfList = new List<AnimatorKeyFrame>(); var animatorKeyFrames = new List<AnimatorKeyFrame>();
foreach (var keyframe in this) foreach (var keyframe in this)
{ {
@ -99,41 +99,38 @@ namespace Avalonia.Animation
if (!handlerList.Contains((handler, setter.Property))) if (!handlerList.Contains((handler, setter.Property)))
handlerList.Add((handler, setter.Property)); handlerList.Add((handler, setter.Property));
var newKF = new AnimatorKeyFrame() var cue = keyframe.Cue;
if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan)
{ {
Handler = handler, cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks);
Property = setter.Property, }
Cue = keyframe.Cue,
KeyTime = keyframe.KeyTime, var newKF = new AnimatorKeyFrame(handler, cue);
timeSpanSet = keyframe.timeSpanSet,
cueSet = keyframe.cueSet, _subscription.Add(newKF.BindSetter(setter, control));
Value = setter.Value
}; animatorKeyFrames.Add(newKF);
kfList.Add(newKF);
} }
} }
var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>(); var newAnimatorInstances = new List<IAnimator>();
foreach (var handler in handlerList) foreach (var (handlerType, property) in handlerList)
{ {
var newInstance = (IAnimator)Activator.CreateInstance(handler.Item1); var newInstance = (IAnimator)Activator.CreateInstance(handlerType);
newInstance.Property = handler.Item2; newInstance.Property = property;
newAnimatorInstances.Add((handler.Item1, handler.Item2, newInstance)); newAnimatorInstances.Add(newInstance);
} }
foreach (var kf in kfList) foreach (var keyframe in animatorKeyFrames)
{ {
var parent = newAnimatorInstances.Where(p => p.handler == kf.Handler && var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType &&
p.prop == kf.Property) a.Property == keyframe.Property);
.First(); animator.Add(keyframe);
parent.inst.Add(kf);
} }
foreach(var instance in newAnimatorInstances) return newAnimatorInstances;
_animators.Add(instance.inst);
} }
/// <summary> /// <summary>
@ -150,17 +147,11 @@ namespace Avalonia.Animation
/// <inheritdocs/> /// <inheritdocs/>
public IDisposable Apply(Animatable control, IObservable<bool> matchObs) public IDisposable Apply(Animatable control, IObservable<bool> matchObs)
{ {
if (_isChildrenChanged) foreach (IAnimator animator in InterpretKeyframes(control))
{
InterpretKeyframes();
_isChildrenChanged = false;
}
foreach (IAnimator keyframes in _animators)
{ {
_subscription.Add(keyframes.Apply(this, control, matchObs)); _subscription.Add(animator.Apply(this, control, matchObs));
} }
return this; return this;
} }
} }
} }

66
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -4,6 +4,8 @@ using System.Text;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Reactive;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
@ -11,13 +13,63 @@ namespace Avalonia.Animation
/// Defines a KeyFrame that is used for /// Defines a KeyFrame that is used for
/// <see cref="Animator{T}"/> objects. /// <see cref="Animator{T}"/> objects.
/// </summary> /// </summary>
public class AnimatorKeyFrame public class AnimatorKeyFrame : AvaloniaObject
{ {
public Type Handler; public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
public Cue Cue; AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(nameof(Value), k => k._value, (k, v) => k._value = v);
public TimeSpan KeyTime;
internal bool timeSpanSet, cueSet; public AnimatorKeyFrame()
public AvaloniaProperty Property; {
public object Value;
}
public AnimatorKeyFrame(Type animatorType, Cue cue)
{
AnimatorType = animatorType;
Cue = cue;
}
public Type AnimatorType { get; }
public Cue Cue { get; }
public AvaloniaProperty Property { get; private set; }
private object _value;
public object Value
{
get => _value;
set => SetAndRaise(ValueProperty, ref _value, value);
}
public IDisposable BindSetter(IAnimationSetter setter, Animatable targetControl)
{
Property = setter.Property;
var value = setter.Value;
if (value is IBinding binding)
{
return this.Bind(ValueProperty, binding, targetControl);
}
else
{
return this.Bind(ValueProperty, ObservableEx.SingleValue(value).ToBinding(), targetControl);
}
}
public T GetTypedValue<T>()
{
var typeConv = TypeDescriptor.GetConverter(typeof(T));
if (Value == null)
{
throw new ArgumentNullException($"KeyFrame value can't be null.");
}
if (!typeConv.CanConvertTo(Value.GetType()))
{
throw new InvalidCastException($"KeyFrame value doesnt match property type.");
}
return (T)typeConv.ConvertTo(Value, typeof(T));
}
} }
} }

188
src/Avalonia.Animation/AnimatorStateMachine`1.cs

@ -51,9 +51,9 @@ namespace Avalonia.Animation
Disposed Disposed
} }
public void Initialize(Animation animation, Animatable control, Animator<T> keyframes) public void Initialize(Animation animation, Animatable control, Animator<T> animator)
{ {
_parent = keyframes; _parent = animator;
_targetAnimation = animation; _targetAnimation = animation;
_targetControl = control; _targetControl = control;
_neutralValue = (T)_targetControl.GetValue(_parent.Property); _neutralValue = (T)_targetControl.GetValue(_parent.Property);
@ -123,121 +123,133 @@ namespace Avalonia.Animation
double _tempDuration = 0d, _easedTime; double _tempDuration = 0d, _easedTime;
checkstate: bool handled = false;
switch (_currentState)
while (!handled)
{ {
case KeyFramesStates.DoDelay: switch (_currentState)
{
case KeyFramesStates.DoDelay:
if (_fillMode == FillMode.Backward if (_fillMode == FillMode.Backward
|| _fillMode == FillMode.Both) || _fillMode == FillMode.Both)
{
if (_currentIteration == 0)
{ {
_targetObserver.OnNext(_firstKFValue); if (_currentIteration == 0)
{
_targetObserver.OnNext(_firstKFValue);
}
else
{
_targetObserver.OnNext(_lastInterpValue);
}
}
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
} }
else else
{ {
_targetObserver.OnNext(_lastInterpValue); handled = true;
_delayFrameCount++;
} }
} break;
if (_delayFrameCount > _delayTotalFrameCount)
{
_currentState = KeyFramesStates.DoRun;
goto checkstate;
}
_delayFrameCount++;
break;
case KeyFramesStates.DoRun:
if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
goto checkstate;
case KeyFramesStates.RunForwards:
if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; case KeyFramesStates.DoRun:
_currentState = KeyFramesStates.RunApplyValue;
goto checkstate; if (_isReversed)
_currentState = KeyFramesStates.RunBackwards;
else
_currentState = KeyFramesStates.RunForwards;
case KeyFramesStates.RunBackwards: break;
if (_durationFrameCount > _durationTotalFrameCount) case KeyFramesStates.RunForwards:
{
_currentState = KeyFramesStates.RunComplete;
goto checkstate;
}
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; if (_durationFrameCount > _durationTotalFrameCount)
_currentState = KeyFramesStates.RunApplyValue; {
_currentState = KeyFramesStates.RunComplete;
}
else
{
_tempDuration = (double)_durationFrameCount / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
goto checkstate; }
break;
case KeyFramesStates.RunApplyValue: case KeyFramesStates.RunBackwards:
_easedTime = _targetAnimation.Easing.Ease(_tempDuration); if (_durationFrameCount > _durationTotalFrameCount)
{
_currentState = KeyFramesStates.RunComplete;
}
else
{
_tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount;
_currentState = KeyFramesStates.RunApplyValue;
}
break;
_durationFrameCount++; case KeyFramesStates.RunApplyValue:
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
break; _easedTime = _targetAnimation.Easing.Ease(_tempDuration);
case KeyFramesStates.RunComplete: _durationFrameCount++;
_lastInterpValue = Interpolator(_easedTime, _neutralValue);
_targetObserver.OnNext(_lastInterpValue);
_currentState = KeyFramesStates.DoRun;
handled = true;
break;
if (_checkLoopAndRepeat) case KeyFramesStates.RunComplete:
{
_delayFrameCount = 0;
_durationFrameCount = 0;
if (_isLooping) if (_checkLoopAndRepeat)
{
_currentState = KeyFramesStates.DoRun;
}
else if (_isRepeating)
{ {
if (_currentIteration >= _repeatCount) _delayFrameCount = 0;
_durationFrameCount = 0;
if (_isLooping)
{ {
_currentState = KeyFramesStates.Stop; _currentState = KeyFramesStates.DoRun;
} }
else else if (_isRepeating)
{ {
_currentState = KeyFramesStates.DoRun; if (_currentIteration >= _repeatCount)
{
_currentState = KeyFramesStates.Stop;
}
else
{
_currentState = KeyFramesStates.DoRun;
}
_currentIteration++;
} }
_currentIteration++;
}
if (_animationDirection == PlaybackDirection.Alternate if (_animationDirection == PlaybackDirection.Alternate
|| _animationDirection == PlaybackDirection.AlternateReverse) || _animationDirection == PlaybackDirection.AlternateReverse)
_isReversed = !_isReversed; _isReversed = !_isReversed;
break; break;
} }
_currentState = KeyFramesStates.Stop; _currentState = KeyFramesStates.Stop;
goto checkstate; break;
case KeyFramesStates.Stop: case KeyFramesStates.Stop:
if (_fillMode == FillMode.Forward if (_fillMode == FillMode.Forward
|| _fillMode == FillMode.Both) || _fillMode == FillMode.Both)
{ {
_targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
} }
_targetObserver.OnCompleted(); _targetObserver.OnCompleted();
break; handled = true;
break;
default:
handled = true;
break;
}
} }
} }
@ -253,4 +265,4 @@ namespace Avalonia.Animation
_currentState = KeyFramesStates.Disposed; _currentState = KeyFramesStates.Disposed;
} }
} }
} }

75
src/Avalonia.Animation/Animator`1.cs

@ -19,7 +19,7 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// List of type-converted keyframes. /// List of type-converted keyframes.
/// </summary> /// </summary>
private Dictionary<double, (T, bool isNeutral)> _convertedKeyframes = new Dictionary<double, (T, bool)>(); private readonly SortedList<double, (AnimatorKeyFrame, bool isNeutral)> _convertedKeyframes = new SortedList<double, (AnimatorKeyFrame, bool)>();
private bool _isVerfifiedAndConverted; private bool _isVerfifiedAndConverted;
@ -38,12 +38,11 @@ namespace Avalonia.Animation
public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch) public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
{ {
if (!_isVerfifiedAndConverted) if (!_isVerfifiedAndConverted)
VerifyConvertKeyFrames(animation, typeof(T)); VerifyConvertKeyFrames();
return obsMatch return obsMatch
.Where(p => p == true)
// Ignore triggers when global timers are paused. // Ignore triggers when global timers are paused.
.Where(p => Timing.GetGlobalPlayState() != PlayState.Pause) .Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause)
.Subscribe(_ => .Subscribe(_ =>
{ {
var timerObs = RunKeyFrames(animation, control); var timerObs = RunKeyFrames(animation, control);
@ -60,8 +59,8 @@ namespace Avalonia.Animation
/// <param name="t">The time parameter, relative to the total animation time</param> /// <param name="t">The time parameter, relative to the total animation time</param>
protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t) protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
{ {
KeyValuePair<double, (T, bool)> firstCue, lastCue; KeyValuePair<double, (AnimatorKeyFrame frame, bool isNeutral)> firstCue, lastCue;
int kvCount = _convertedKeyframes.Count(); int kvCount = _convertedKeyframes.Count;
if (kvCount > 2) if (kvCount > 2)
{ {
if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0) if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0)
@ -76,8 +75,8 @@ namespace Avalonia.Animation
} }
else else
{ {
firstCue = _convertedKeyframes.Where(j => j.Key <= t).Last(); firstCue = _convertedKeyframes.Last(j => j.Key <= t);
lastCue = _convertedKeyframes.Where(j => j.Key >= t).First(); lastCue = _convertedKeyframes.First(j => j.Key >= t);
} }
} }
else else
@ -89,7 +88,9 @@ namespace Avalonia.Animation
double t0 = firstCue.Key; double t0 = firstCue.Key;
double t1 = lastCue.Key; double t1 = lastCue.Key;
var intraframeTime = (t - t0) / (t1 - t0); var intraframeTime = (t - t0) / (t1 - t0);
return (intraframeTime, new KeyFramePair<T>(firstCue, lastCue)); var firstFrameData = (firstCue.Value.frame.GetTypedValue<T>(), firstCue.Value.isNeutral);
var lastFrameData = (lastCue.Value.frame.GetTypedValue<T>(), lastCue.Value.isNeutral);
return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
} }
@ -98,17 +99,14 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
private IDisposable RunKeyFrames(Animation animation, Animatable control) private IDisposable RunKeyFrames(Animation animation, Animatable control)
{ {
var _kfStateMach = new AnimatorStateMachine<T>(); var stateMachine = new AnimatorStateMachine<T>();
_kfStateMach.Initialize(animation, control, this); stateMachine.Initialize(animation, control, this);
Timing.AnimationStateTimer Timing.AnimationStateTimer
.TakeWhile(_ => !_kfStateMach._unsubscribe) .TakeWhile(_ => !stateMachine._unsubscribe)
.Subscribe(p => .Subscribe(p => stateMachine.Step(p, DoInterpolation));
{
_kfStateMach.Step(p, DoInterpolation);
});
return control.Bind(Property, _kfStateMach, BindingPriority.Animation); return control.Bind(Property, stateMachine, BindingPriority.Animation);
} }
/// <summary> /// <summary>
@ -119,39 +117,19 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// Verifies and converts keyframe values according to this class's target type. /// Verifies and converts keyframe values according to this class's target type.
/// </summary> /// </summary>
private void VerifyConvertKeyFrames(Animation animation, Type type) private void VerifyConvertKeyFrames()
{ {
var typeConv = TypeDescriptor.GetConverter(type); foreach (AnimatorKeyFrame keyframe in this)
foreach (AnimatorKeyFrame k in this)
{ {
if (k.Value == null) _convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false));
{
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, false));
} }
SortKeyFrameCues(_convertedKeyframes); AddNeutralKeyFramesIfNeeded();
_isVerfifiedAndConverted = true; _isVerfifiedAndConverted = true;
} }
private void SortKeyFrameCues(Dictionary<double, (T, bool)> convertedValues) private void AddNeutralKeyFramesIfNeeded()
{ {
bool hasStartKey, hasEndKey; bool hasStartKey, hasEndKey;
hasStartKey = hasEndKey = false; hasStartKey = hasEndKey = false;
@ -170,23 +148,20 @@ namespace Avalonia.Animation
} }
if (!hasStartKey || !hasEndKey) if (!hasStartKey || !hasEndKey)
AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes); AddNeutralKeyFrames(hasStartKey, hasEndKey);
_convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key)
.ToDictionary((k) => k.Key, (v) => v.Value);
} }
private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary<double, (T, bool)> convertedKeyframes) private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey)
{ {
if (!hasStartKey) if (!hasStartKey)
{ {
convertedKeyframes.Add(0.0d, (default(T), true)); _convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
} }
if (!hasEndKey) if (!hasEndKey)
{ {
convertedKeyframes.Add(1.0d, (default(T), true)); _convertedKeyframes.Add(1.0d, (new AnimatorKeyFrame { Value = default(T) }, true));
} }
} }
} }
} }

2
src/Avalonia.Animation/Cue.cs

@ -10,7 +10,7 @@ namespace Avalonia.Animation
/// A Cue object for <see cref="KeyFrame"/>. /// A Cue object for <see cref="KeyFrame"/>.
/// </summary> /// </summary>
[TypeConverter(typeof(CueTypeConverter))] [TypeConverter(typeof(CueTypeConverter))]
public struct Cue : IEquatable<Cue>, IEquatable<double> public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
{ {
/// <summary> /// <summary>
/// The normalized percent value, ranging from 0.0 to 1.0 /// The normalized percent value, ranging from 0.0 to 1.0

8
src/Avalonia.Animation/DoubleAnimator.cs

@ -24,15 +24,15 @@ namespace Avalonia.Animation
var firstKF = pair.KFPair.FirstKeyFrame; var firstKF = pair.KFPair.FirstKeyFrame;
var secondKF = pair.KFPair.SecondKeyFrame; var secondKF = pair.KFPair.SecondKeyFrame;
if (firstKF.Value.isNeutral) if (firstKF.isNeutral)
y0 = neutralValue; y0 = neutralValue;
else else
y0 = firstKF.Value.TargetValue; y0 = firstKF.TargetValue;
if (secondKF.Value.isNeutral) if (secondKF.isNeutral)
y1 = neutralValue; y1 = neutralValue;
else else
y1 = secondKF.Value.TargetValue; y1 = secondKF.TargetValue;
// Do linear parametric interpolation // Do linear parametric interpolation
return y0 + (pair.IntraKFTime) * (y1 - y0); return y0 + (pair.IntraKFTime) * (y1 - y0);

2
src/Avalonia.Animation/IAnimationSetter.cs

@ -5,4 +5,4 @@ namespace Avalonia.Animation
AvaloniaProperty Property { get; set; } AvaloniaProperty Property { get; set; }
object Value { get; set; } object Value { get; set; }
} }
} }

16
src/Avalonia.Animation/KeyFrame.cs

@ -7,6 +7,11 @@ using Avalonia.Collections;
namespace Avalonia.Animation namespace Avalonia.Animation
{ {
internal enum KeyFrameTimingMode
{
TimeSpan = 1,
Cue
}
/// <summary> /// <summary>
/// Stores data regarding a specific key /// Stores data regarding a specific key
@ -14,7 +19,6 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
public class KeyFrame : AvaloniaList<IAnimationSetter> public class KeyFrame : AvaloniaList<IAnimationSetter>
{ {
internal bool timeSpanSet, cueSet;
private TimeSpan _ktimeSpan; private TimeSpan _ktimeSpan;
private Cue _kCue; private Cue _kCue;
@ -30,6 +34,8 @@ namespace Avalonia.Animation
{ {
} }
internal KeyFrameTimingMode TimingMode { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the key time of this <see cref="KeyFrame"/>. /// Gets or sets the key time of this <see cref="KeyFrame"/>.
/// </summary> /// </summary>
@ -42,11 +48,11 @@ namespace Avalonia.Animation
} }
set set
{ {
if (cueSet) if (TimingMode == KeyFrameTimingMode.Cue)
{ {
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
} }
timeSpanSet = true; TimingMode = KeyFrameTimingMode.TimeSpan;
_ktimeSpan = value; _ktimeSpan = value;
} }
} }
@ -63,11 +69,11 @@ namespace Avalonia.Animation
} }
set set
{ {
if (timeSpanSet) if (TimingMode == KeyFrameTimingMode.TimeSpan)
{ {
throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}.");
} }
cueSet = true; TimingMode = KeyFrameTimingMode.Cue;
_kCue = value; _kCue = value;
} }
} }

8
src/Avalonia.Animation/KeyFramePair`1.cs

@ -22,7 +22,7 @@ namespace Avalonia.Animation
/// </summary> /// </summary>
/// <param name="FirstKeyFrame"></param> /// <param name="FirstKeyFrame"></param>
/// <param name="LastKeyFrame"></param> /// <param name="LastKeyFrame"></param>
public KeyFramePair(KeyValuePair<double, (T, bool)> FirstKeyFrame, KeyValuePair<double, (T, bool)> LastKeyFrame) : this() public KeyFramePair((T TargetValue, bool isNeutral) FirstKeyFrame, (T TargetValue, bool isNeutral) LastKeyFrame) : this()
{ {
this.FirstKeyFrame = FirstKeyFrame; this.FirstKeyFrame = FirstKeyFrame;
this.SecondKeyFrame = LastKeyFrame; this.SecondKeyFrame = LastKeyFrame;
@ -31,11 +31,11 @@ namespace Avalonia.Animation
/// <summary> /// <summary>
/// First <see cref="KeyFrame"/> object. /// First <see cref="KeyFrame"/> object.
/// </summary> /// </summary>
public KeyValuePair<double, (T TargetValue, bool isNeutral)> FirstKeyFrame { get; private set; } public (T TargetValue, bool isNeutral) FirstKeyFrame { get; }
/// <summary> /// <summary>
/// Second <see cref="KeyFrame"/> object. /// Second <see cref="KeyFrame"/> object.
/// </summary> /// </summary>
public KeyValuePair<double, (T TargetValue, bool isNeutral)> SecondKeyFrame { get; private set; } public (T TargetValue, bool isNeutral) SecondKeyFrame { get; }
} }
} }

90
src/Avalonia.Controls/ProgressBar.cs

@ -21,18 +21,27 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty = public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal); AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal);
private static readonly DirectProperty<ProgressBar, double> IndeterminateStartingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>(
nameof(IndeterminateStartingOffset),
p => p.IndeterminateStartingOffset,
(p, o) => p.IndeterminateStartingOffset = o);
private static readonly DirectProperty<ProgressBar, double> IndeterminateEndingOffsetProperty =
AvaloniaProperty.RegisterDirect<ProgressBar, double>(
nameof(IndeterminateEndingOffset),
p => p.IndeterminateEndingOffset,
(p, o) => p.IndeterminateEndingOffset = o);
private Border _indicator; private Border _indicator;
private IndeterminateAnimation _indeterminateAnimation;
static ProgressBar() static ProgressBar()
{ {
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
PseudoClass(IsIndeterminateProperty, ":indeterminate");
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged); ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged);
IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>(
(p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); });
} }
public bool IsIndeterminate public bool IsIndeterminate
@ -46,6 +55,19 @@ namespace Avalonia.Controls
get => GetValue(OrientationProperty); get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value); set => SetValue(OrientationProperty, value);
} }
private double _indeterminateStartingOffset;
private double IndeterminateStartingOffset
{
get => _indeterminateStartingOffset;
set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value);
}
private double _indeterminateEndingOffset;
private double IndeterminateEndingOffset
{
get => _indeterminateEndingOffset;
set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value);
}
/// <inheritdoc/> /// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
@ -60,7 +82,6 @@ namespace Avalonia.Controls
_indicator = e.NameScope.Get<Border>("PART_Indicator"); _indicator = e.NameScope.Get<Border>("PART_Indicator");
UpdateIndicator(Bounds.Size); UpdateIndicator(Bounds.Size);
UpdateIsIndeterminate(IsIndeterminate);
} }
private void UpdateIndicator(Size bounds) private void UpdateIndicator(Size bounds)
@ -70,9 +91,20 @@ namespace Avalonia.Controls
if (IsIndeterminate) if (IsIndeterminate)
{ {
if (Orientation == Orientation.Horizontal) if (Orientation == Orientation.Horizontal)
_indicator.Width = bounds.Width / 5.0; {
var width = bounds.Width / 5.0;
IndeterminateStartingOffset = -width;
_indicator.Width = width;
IndeterminateEndingOffset = bounds.Width;
}
else else
_indicator.Height = bounds.Height / 5.0; {
var height = bounds.Height / 5.0;
IndeterminateStartingOffset = -bounds.Height;
_indicator.Height = height;
IndeterminateEndingOffset = height;
}
} }
else else
{ {
@ -86,53 +118,9 @@ namespace Avalonia.Controls
} }
} }
private void UpdateIsIndeterminate(bool isIndeterminate)
{
if (isIndeterminate)
{
if (_indeterminateAnimation == null || _indeterminateAnimation.Disposed)
_indeterminateAnimation = IndeterminateAnimation.StartAnimation(this);
}
else
_indeterminateAnimation?.Dispose();
}
private void ValueChanged(AvaloniaPropertyChangedEventArgs e) private void ValueChanged(AvaloniaPropertyChangedEventArgs e)
{ {
UpdateIndicator(Bounds.Size); UpdateIndicator(Bounds.Size);
} }
// TODO: Implement Indeterminate Progress animation
// in xaml (most ideal) or if it's not possible
// then on this class.
private class IndeterminateAnimation : IDisposable
{
private WeakReference<ProgressBar> _progressBar;
private bool _disposed;
public bool Disposed => _disposed;
private IndeterminateAnimation(ProgressBar progressBar)
{
_progressBar = new WeakReference<ProgressBar>(progressBar);
}
public static IndeterminateAnimation StartAnimation(ProgressBar progressBar)
{
return new IndeterminateAnimation(progressBar);
}
private Rect GetAnimationRect(TimeSpan time)
{
return Rect.Empty;
}
public void Dispose()
{
_disposed = true;
}
}
} }
} }

6
src/Avalonia.Styling/Styling/Setter.cs

@ -126,7 +126,7 @@ namespace Avalonia.Styling
if (source != null) if (source != null)
{ {
var cloned = Clone(source, style, activator); var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator);
return BindingOperations.Apply(control, Property, cloned, null); return BindingOperations.Apply(control, Property, cloned, null);
} }
} }
@ -134,13 +134,13 @@ namespace Avalonia.Styling
return Disposable.Empty; return Disposable.Empty;
} }
private InstancedBinding Clone(InstancedBinding sourceInstance, IStyle style, IObservable<bool> activator) private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable<bool> activator)
{ {
if (activator != null) if (activator != null)
{ {
var description = style?.ToString(); var description = style?.ToString();
switch (sourceInstance.Mode) switch (mode)
{ {
case BindingMode.OneTime: case BindingMode.OneTime:
if (sourceInstance.Observable != null) if (sourceInstance.Observable != null)

45
src/Avalonia.Themes.Default/ProgressBar.xaml

@ -7,14 +7,9 @@
<Border Background="{TemplateBinding Background}" <Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"> BorderThickness="{TemplateBinding BorderThickness}">
<Grid> <Border Name="PART_Indicator"
<Border Name="PART_Track" BorderThickness="1"
BorderThickness="1" Background="{TemplateBinding Foreground}"/>
BorderBrush="{TemplateBinding Background}"/>
<Border Name="PART_Indicator"
BorderThickness="1"
Background="{TemplateBinding Foreground}" />
</Grid>
</Border> </Border>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
@ -35,4 +30,36 @@
<Setter Property="MinWidth" Value="14"/> <Setter Property="MinWidth" Value="14"/>
<Setter Property="MinHeight" Value="200"/> <Setter Property="MinHeight" Value="200"/>
</Style> </Style>
</Styles> <Style Selector="ProgressBar:horizontal:indeterminate /template/ Border#PART_Indicator">
<Style.Animations>
<Animation Duration="0:0:3"
RepeatCount="Loop"
Easing="LinearEasing">
<KeyFrame Cue="0%">
<Setter Property="TranslateTransform.X"
Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="TranslateTransform.X"
Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="ProgressBar:vertical:indeterminate /template/ Border#PART_Indicator">
<Style.Animations>
<Animation Duration="0:0:3"
RepeatCount="Loop"
Easing="LinearEasing">
<KeyFrame Cue="0%">
<Setter Property="TranslateTransform.Y"
Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="TranslateTransform.Y"
Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>

4
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs

@ -7,7 +7,6 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.Templates; using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -33,6 +32,7 @@ namespace Avalonia.Markup.Xaml.Context
private static readonly IEnumerable<Assembly> ForcedAssemblies = new[] private static readonly IEnumerable<Assembly> ForcedAssemblies = new[]
{ {
typeof(AvaloniaObject).GetTypeInfo().Assembly, typeof(AvaloniaObject).GetTypeInfo().Assembly,
typeof(Animation.Animation).GetTypeInfo().Assembly,
typeof(Control).GetTypeInfo().Assembly, typeof(Control).GetTypeInfo().Assembly,
typeof(Style).GetTypeInfo().Assembly, typeof(Style).GetTypeInfo().Assembly,
typeof(DataTemplate).GetTypeInfo().Assembly, typeof(DataTemplate).GetTypeInfo().Assembly,
@ -146,4 +146,4 @@ namespace Avalonia.Markup.Xaml.Context
return null; return null;
} }
} }
} }

5
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -132,7 +132,10 @@ namespace Avalonia.Data
} }
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{ {
observer = CreateTemplatedParentObserver(target, Path, enableDataValidation); observer = CreateTemplatedParentObserver(
(target as IStyledElement) ?? (anchor as IStyledElement),
Path,
enableDataValidation);
} }
else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor) else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
{ {

Loading…
Cancel
Save