From 0624d300376d2140b069a53a9808ff59933a44c5 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 2 Aug 2018 01:24:17 +0800 Subject: [PATCH 01/42] Make ASM more Genericized. --- src/Avalonia.Animation/AnimatorStateMachine`1.cs | 12 ++++++------ src/Avalonia.Animation/Animator`1.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 1a51b897c0..414f07190a 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -7,10 +7,10 @@ namespace Avalonia.Animation /// /// Provides statefulness for an iteration of a keyframe animation. /// - internal class AnimatorStateMachine : IObservable, IDisposable + internal class AnimatorStateMachine : IObservable, IDisposable { - object _lastInterpValue; - object _firstKFValue; + T _lastInterpValue; + T _firstKFValue; private ulong _delayTotalFrameCount; private ulong _durationTotalFrameCount; @@ -34,7 +34,7 @@ namespace Avalonia.Animation private Animatable _targetControl; private T _neutralValue; internal bool _unsubscribe = false; - private IObserver _targetObserver; + private IObserver _targetObserver; [Flags] private enum KeyFramesStates @@ -100,7 +100,7 @@ namespace Avalonia.Animation { if (!_gotFirstKFValue) { - _firstKFValue = _parent.First().Value; + _firstKFValue = (T)_parent.First().Value; _gotFirstKFValue = true; } @@ -253,7 +253,7 @@ namespace Avalonia.Animation } } - public IDisposable Subscribe(IObserver observer) + public IDisposable Subscribe(IObserver observer) { _targetObserver = observer; return this; diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index a1eef87e1e..37764b34e8 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -106,7 +106,7 @@ namespace Avalonia.Animation .TakeWhile(_ => !stateMachine._unsubscribe) .Subscribe(p => stateMachine.Step(p, DoInterpolation)); - return control.Bind(Property, stateMachine, BindingPriority.Animation); + return control.Bind((AvaloniaProperty)Property, stateMachine, BindingPriority.Animation); } /// From 87934f2bfda67acb52dd363e910e4864db543c31 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 2 Aug 2018 01:30:43 +0800 Subject: [PATCH 02/42] Remove underscores on ASM fields. --- .../AnimatorStateMachine`1.cs | 218 +++++++++--------- src/Avalonia.Animation/Animator`1.cs | 2 +- 2 files changed, 110 insertions(+), 110 deletions(-) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 414f07190a..923cdd07b3 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -9,32 +9,32 @@ namespace Avalonia.Animation /// internal class AnimatorStateMachine : IObservable, IDisposable { - T _lastInterpValue; - T _firstKFValue; - - private ulong _delayTotalFrameCount; - private ulong _durationTotalFrameCount; - private ulong _delayFrameCount; - private ulong _durationFrameCount; - private ulong _repeatCount; - private ulong _currentIteration; - - private bool _isLooping; - private bool _isRepeating; - private bool _isReversed; - private bool _checkLoopAndRepeat; - private bool _gotFirstKFValue; - - private FillMode _fillMode; - private PlaybackDirection _animationDirection; - private KeyFramesStates _currentState; - private KeyFramesStates _savedState; - private Animator _parent; - private Animation _targetAnimation; - private Animatable _targetControl; - private T _neutralValue; - internal bool _unsubscribe = false; - private IObserver _targetObserver; + T lastInterpValue; + T firstKFValue; + + private ulong delayTotalFrameCount; + private ulong durationTotalFrameCount; + private ulong delayFrameCount; + private ulong durationFrameCount; + private ulong repeatCount; + private ulong currentIteration; + + private bool isLooping; + private bool isRepeating; + private bool isReversed; + private bool checkLoopAndRepeat; + private bool gotFirstKFValue; + + private FillMode fillMode; + private PlaybackDirection animationDirection; + private KeyFramesStates currentState; + private KeyFramesStates savedState; + private Animator parent; + private Animation targetAnimation; + private Animatable targetControl; + private T neutralValue; + internal bool unsubscribe = false; + private IObserver targetObserver; [Flags] private enum KeyFramesStates @@ -53,197 +53,197 @@ namespace Avalonia.Animation public void Initialize(Animation animation, Animatable control, Animator animator) { - _parent = animator; - _targetAnimation = animation; - _targetControl = control; - _neutralValue = (T)_targetControl.GetValue(_parent.Property); + parent = animator; + targetAnimation = animation; + targetControl = control; + neutralValue = (T)targetControl.GetValue(parent.Property); - _delayTotalFrameCount = (ulong)(animation.Delay.Ticks / Timing.FrameTick.Ticks); - _durationTotalFrameCount = (ulong)(animation.Duration.Ticks / Timing.FrameTick.Ticks); + delayTotalFrameCount = (ulong)(animation.Delay.Ticks / Timing.FrameTick.Ticks); + durationTotalFrameCount = (ulong)(animation.Duration.Ticks / Timing.FrameTick.Ticks); switch (animation.RepeatCount.RepeatType) { case RepeatType.Loop: - _isLooping = true; - _checkLoopAndRepeat = true; + isLooping = true; + checkLoopAndRepeat = true; break; case RepeatType.Repeat: - _isRepeating = true; - _checkLoopAndRepeat = true; - _repeatCount = animation.RepeatCount.Value; + isRepeating = true; + checkLoopAndRepeat = true; + repeatCount = animation.RepeatCount.Value; break; } - _isReversed = (animation.PlaybackDirection & PlaybackDirection.Reverse) != 0; - _animationDirection = _targetAnimation.PlaybackDirection; - _fillMode = _targetAnimation.FillMode; + isReversed = (animation.PlaybackDirection & PlaybackDirection.Reverse) != 0; + animationDirection = targetAnimation.PlaybackDirection; + fillMode = targetAnimation.FillMode; - if (_durationTotalFrameCount > 0) - _currentState = KeyFramesStates.DoDelay; + if (durationTotalFrameCount > 0) + currentState = KeyFramesStates.DoDelay; else - _currentState = KeyFramesStates.DoRun; + currentState = KeyFramesStates.DoRun; } - public void Step(PlayState _playState, Func Interpolator) + public void Step(PlayState playState, Func Interpolator) { try { - InternalStep(_playState, Interpolator); + InternalStep(playState, Interpolator); } catch (Exception e) { - _targetObserver?.OnError(e); + targetObserver?.OnError(e); } } - private void InternalStep(PlayState _playState, Func Interpolator) + private void InternalStep(PlayState playState, Func Interpolator) { - if (!_gotFirstKFValue) + if (!gotFirstKFValue) { - _firstKFValue = (T)_parent.First().Value; - _gotFirstKFValue = true; + firstKFValue = (T)parent.First().Value; + gotFirstKFValue = true; } - if (_currentState == KeyFramesStates.Disposed) + if (currentState == KeyFramesStates.Disposed) throw new InvalidProgramException("This KeyFrames Animation is already disposed."); - if (_playState == PlayState.Stop) - _currentState = KeyFramesStates.Stop; + if (playState == PlayState.Stop) + currentState = KeyFramesStates.Stop; - // Save state and pause the machine - if (_playState == PlayState.Pause && _currentState != KeyFramesStates.Pause) - { - _savedState = _currentState; - _currentState = KeyFramesStates.Pause; - } + // // Save state and pause the machine + // if (playState == PlayState.Pause && currentState != KeyFramesStates.Pause) + // { + // savedState = currentState; + // currentState = KeyFramesStates.Pause; + // } - // Resume the previous state - if (_playState != PlayState.Pause && _currentState == KeyFramesStates.Pause) - _currentState = _savedState; + // // Resume the previous state + // if (playState != PlayState.Pause && currentState == KeyFramesStates.Pause) + // currentState = savedState; - double _tempDuration = 0d, _easedTime; + double tempDuration = 0d, easedTime; bool handled = false; while (!handled) { - switch (_currentState) + switch (currentState) { case KeyFramesStates.DoDelay: - if (_fillMode == FillMode.Backward - || _fillMode == FillMode.Both) + if (fillMode == FillMode.Backward + || fillMode == FillMode.Both) { - if (_currentIteration == 0) + if (currentIteration == 0) { - _targetObserver.OnNext(_firstKFValue); + targetObserver.OnNext(firstKFValue); } else { - _targetObserver.OnNext(_lastInterpValue); + targetObserver.OnNext(lastInterpValue); } } - if (_delayFrameCount > _delayTotalFrameCount) + if (delayFrameCount > delayTotalFrameCount) { - _currentState = KeyFramesStates.DoRun; + currentState = KeyFramesStates.DoRun; } else { handled = true; - _delayFrameCount++; + delayFrameCount++; } break; case KeyFramesStates.DoRun: - if (_isReversed) - _currentState = KeyFramesStates.RunBackwards; + if (isReversed) + currentState = KeyFramesStates.RunBackwards; else - _currentState = KeyFramesStates.RunForwards; + currentState = KeyFramesStates.RunForwards; break; case KeyFramesStates.RunForwards: - if (_durationFrameCount > _durationTotalFrameCount) + if (durationFrameCount > durationTotalFrameCount) { - _currentState = KeyFramesStates.RunComplete; + currentState = KeyFramesStates.RunComplete; } else { - _tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; - _currentState = KeyFramesStates.RunApplyValue; + tempDuration = (double)durationFrameCount / durationTotalFrameCount; + currentState = KeyFramesStates.RunApplyValue; } break; case KeyFramesStates.RunBackwards: - if (_durationFrameCount > _durationTotalFrameCount) + if (durationFrameCount > durationTotalFrameCount) { - _currentState = KeyFramesStates.RunComplete; + currentState = KeyFramesStates.RunComplete; } else { - _tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; - _currentState = KeyFramesStates.RunApplyValue; + tempDuration = (double)(durationTotalFrameCount - durationFrameCount) / durationTotalFrameCount; + currentState = KeyFramesStates.RunApplyValue; } break; case KeyFramesStates.RunApplyValue: - _easedTime = _targetAnimation.Easing.Ease(_tempDuration); + easedTime = targetAnimation.Easing.Ease(tempDuration); - _durationFrameCount++; - _lastInterpValue = Interpolator(_easedTime, _neutralValue); - _targetObserver.OnNext(_lastInterpValue); - _currentState = KeyFramesStates.DoRun; + durationFrameCount++; + lastInterpValue = Interpolator(easedTime, neutralValue); + targetObserver.OnNext(lastInterpValue); + currentState = KeyFramesStates.DoRun; handled = true; break; case KeyFramesStates.RunComplete: - if (_checkLoopAndRepeat) + if (checkLoopAndRepeat) { - _delayFrameCount = 0; - _durationFrameCount = 0; + delayFrameCount = 0; + durationFrameCount = 0; - if (_isLooping) + if (isLooping) { - _currentState = KeyFramesStates.DoRun; + currentState = KeyFramesStates.DoRun; } - else if (_isRepeating) + else if (isRepeating) { - if (_currentIteration >= _repeatCount) + if (currentIteration >= repeatCount) { - _currentState = KeyFramesStates.Stop; + currentState = KeyFramesStates.Stop; } else { - _currentState = KeyFramesStates.DoRun; + currentState = KeyFramesStates.DoRun; } - _currentIteration++; + currentIteration++; } - if (_animationDirection == PlaybackDirection.Alternate - || _animationDirection == PlaybackDirection.AlternateReverse) - _isReversed = !_isReversed; + if (animationDirection == PlaybackDirection.Alternate + || animationDirection == PlaybackDirection.AlternateReverse) + isReversed = !isReversed; break; } - _currentState = KeyFramesStates.Stop; + currentState = KeyFramesStates.Stop; break; case KeyFramesStates.Stop: - if (_fillMode == FillMode.Forward - || _fillMode == FillMode.Both) + if (fillMode == FillMode.Forward + || fillMode == FillMode.Both) { - _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); + targetControl.SetValue(parent.Property, lastInterpValue, BindingPriority.LocalValue); } - _targetObserver.OnCompleted(); + targetObserver.OnCompleted(); handled = true; break; default: @@ -255,14 +255,14 @@ namespace Avalonia.Animation public IDisposable Subscribe(IObserver observer) { - _targetObserver = observer; + targetObserver = observer; return this; } public void Dispose() { - _unsubscribe = true; - _currentState = KeyFramesStates.Disposed; + unsubscribe = true; + currentState = KeyFramesStates.Disposed; } } } diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 37764b34e8..654ee327f8 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -103,7 +103,7 @@ namespace Avalonia.Animation stateMachine.Initialize(animation, control, this); Timing.AnimationStateTimer - .TakeWhile(_ => !stateMachine._unsubscribe) + .TakeWhile(_ => !stateMachine.unsubscribe) .Subscribe(p => stateMachine.Step(p, DoInterpolation)); return control.Bind((AvaloniaProperty)Property, stateMachine, BindingPriority.Animation); From 127d060f6611045628ea0afa5eeae78f7d33a207 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 2 Aug 2018 01:47:45 +0800 Subject: [PATCH 03/42] Add new properties for Animations. --- src/Avalonia.Animation/Animation.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 4e777b36ed..eaf6b280bc 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -75,6 +75,16 @@ namespace Avalonia.Animation /// public Easing Easing { get; set; } = new LinearEasing(); + /// + /// Sets the speed multiple for this animation. + /// + public double SpeedRatio { get; set; } = 1d; + + /// + /// Sets the behavior for having a delay between repeats for this animation. + /// + public bool DelayBetweenRepeats { get; set; } + public Animation() { this.CollectionChanged += delegate { _isChildrenChanged = true; }; From b3ebfa574865a5aee54594e6dd48ca38e11e0713 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 2 Aug 2018 02:03:14 +0800 Subject: [PATCH 04/42] Add the global frame count to AnimationStateTimer. --- src/Avalonia.Animation/Timing.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Animation/Timing.cs b/src/Avalonia.Animation/Timing.cs index 10d65cca7f..3eddc0ac26 100644 --- a/src/Avalonia.Animation/Timing.cs +++ b/src/Avalonia.Animation/Timing.cs @@ -16,6 +16,7 @@ namespace Avalonia.Animation public static class Timing { static ulong _transitionsFrameCount; + static long _tickStartTimeStamp; static PlayState _globalState = PlayState.Run; /// @@ -33,12 +34,16 @@ namespace Avalonia.Animation /// static Timing() { + + _tickStartTimeStamp = Stopwatch.GetTimestamp(); + var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance); AnimationStateTimer = globalTimer .Select(_ => { - return _globalState; + return (_globalState, (Stopwatch.GetTimestamp() - _tickStartTimeStamp) + / (Stopwatch.Frequency / FramesPerSecond)); }) .Publish() .RefCount(); @@ -76,7 +81,7 @@ namespace Avalonia.Animation /// defined in . /// The parameter passed to a subsciber is the current playstate of the animation. /// - internal static IObservable AnimationStateTimer + internal static IObservable<(PlayState, long)> AnimationStateTimer { get; } From 5365fd0a41220d840acb7d6d2511f3f3fda8478b Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 2 Aug 2018 02:04:38 +0800 Subject: [PATCH 05/42] Add the new animation algorithm. --- src/Avalonia.Animation/Animatable.cs | 4 +- .../AnimatorStateMachine`1.cs | 284 +++++++----------- src/Avalonia.Animation/Animator`1.cs | 2 +- 3 files changed, 113 insertions(+), 177 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index a27d996301..85317af1a8 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -29,7 +29,7 @@ namespace Avalonia.Animation { if (this._playState == PlayState.Pause) { - return PlayState.Pause; + return (PlayState.Pause, p.Item2); } else return p; }) @@ -41,7 +41,7 @@ namespace Avalonia.Animation /// The specific animations timer for this control. /// /// - public IObservable AnimatableTimer; + public IObservable<(PlayState, long)> AnimatableTimer; /// /// Defines the property. diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 923cdd07b3..6854645cc2 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Avalonia.Animation.Utils; using Avalonia.Data; namespace Avalonia.Animation @@ -12,91 +13,97 @@ namespace Avalonia.Animation T lastInterpValue; T firstKFValue; - private ulong delayTotalFrameCount; - private ulong durationTotalFrameCount; - private ulong delayFrameCount; - private ulong durationFrameCount; - private ulong repeatCount; - private ulong currentIteration; + private long delayFC; + private long durationFC; + private long repeatCount; + private long currentIteration; + private long firstFrameCount; private bool isLooping; private bool isRepeating; - private bool isReversed; - private bool checkLoopAndRepeat; private bool gotFirstKFValue; + private bool gotFirstFrameCount; + private bool delayBetweenIterations; private FillMode fillMode; private PlaybackDirection animationDirection; - private KeyFramesStates currentState; - private KeyFramesStates savedState; private Animator parent; - private Animation targetAnimation; private Animatable targetControl; private T neutralValue; - internal bool unsubscribe = false; - private IObserver targetObserver; + private double speedRatio; + internal bool unsubscribe; + private bool isDisposed; - [Flags] - private enum KeyFramesStates - { - Initialize, - DoDelay, - DoRun, - RunForwards, - RunBackwards, - RunApplyValue, - RunComplete, - Pause, - Stop, - Disposed - } + private Easings.Easing EaseFunc; + private IObserver targetObserver; public void Initialize(Animation animation, Animatable control, Animator animator) { + + if (animation.SpeedRatio <= 0 || DoubleUtils.AboutEqual(animation.SpeedRatio, 0)) + throw new InvalidOperationException("Speed ratio cannot be negative or zero."); + + if (animation.Duration.TotalSeconds <= 0 || DoubleUtils.AboutEqual(animation.Duration.TotalSeconds, 0)) + throw new InvalidOperationException("Animation duration cannot be negative or zero."); + parent = animator; - targetAnimation = animation; + EaseFunc = animation.Easing; targetControl = control; neutralValue = (T)targetControl.GetValue(parent.Property); - delayTotalFrameCount = (ulong)(animation.Delay.Ticks / Timing.FrameTick.Ticks); - durationTotalFrameCount = (ulong)(animation.Duration.Ticks / Timing.FrameTick.Ticks); + speedRatio = animation.SpeedRatio; + delayFC = (long)((animation.Delay.Ticks / Timing.FrameTick.Ticks) * speedRatio); + durationFC = (long)((animation.Duration.Ticks / Timing.FrameTick.Ticks) * speedRatio); switch (animation.RepeatCount.RepeatType) { + case RepeatType.None: + repeatCount = 1; + break; case RepeatType.Loop: isLooping = true; - checkLoopAndRepeat = true; break; case RepeatType.Repeat: isRepeating = true; - checkLoopAndRepeat = true; - repeatCount = animation.RepeatCount.Value; + repeatCount = (long)animation.RepeatCount.Value; break; } - isReversed = (animation.PlaybackDirection & PlaybackDirection.Reverse) != 0; - animationDirection = targetAnimation.PlaybackDirection; - fillMode = targetAnimation.FillMode; + animationDirection = animation.PlaybackDirection; + fillMode = animation.FillMode; - if (durationTotalFrameCount > 0) - currentState = KeyFramesStates.DoDelay; - else - currentState = KeyFramesStates.DoRun; } - public void Step(PlayState playState, Func Interpolator) + public void Step(PlayState playState, long frameTick, Func Interpolator) { try { - InternalStep(playState, Interpolator); + InternalStep(playState, frameTick, Interpolator); } catch (Exception e) { targetObserver?.OnError(e); } } + + private void DoComplete() + { + if (fillMode == FillMode.Forward || fillMode == FillMode.Both) + targetControl.SetValue(parent.Property, lastInterpValue, BindingPriority.LocalValue); + + targetObserver.OnCompleted(); + } - private void InternalStep(PlayState playState, Func Interpolator) + private void DoDelay() + { + if (fillMode == FillMode.Backward || fillMode == FillMode.Both) + if (currentIteration == 0) + targetObserver.OnNext(firstKFValue); + else + targetObserver.OnNext(lastInterpValue); + } + + private void InternalStep(PlayState playState, long frameTick, Func Interpolator) { if (!gotFirstKFValue) { @@ -104,13 +111,19 @@ namespace Avalonia.Animation gotFirstKFValue = true; } - if (currentState == KeyFramesStates.Disposed) + if (!gotFirstFrameCount) + { + firstFrameCount = frameTick; + gotFirstFrameCount = true; + } + + if (isDisposed) throw new InvalidProgramException("This KeyFrames Animation is already disposed."); if (playState == PlayState.Stop) - currentState = KeyFramesStates.Stop; + DoComplete(); - // // Save state and pause the machine + // Save state and pause the machine // if (playState == PlayState.Pause && currentState != KeyFramesStates.Pause) // { // savedState = currentState; @@ -121,135 +134,58 @@ namespace Avalonia.Animation // if (playState != PlayState.Pause && currentState == KeyFramesStates.Pause) // currentState = savedState; - double tempDuration = 0d, easedTime; + // get the time with the initial fc as point of origin. + var t = (frameTick - firstFrameCount); - bool handled = false; + // check if t is within the zeroth iteration + if (t <= (delayFC + durationFC)) + { + currentIteration = 0; + t = t % (delayFC + durationFC); + } + else + { + var totalDur = (double)((delayBetweenIterations ? delayFC : 0) + durationFC + 1); + currentIteration = (long)Math.Floor((double)t / totalDur); + t = t % (long)totalDur; + } + + // check if it's over the repeat count + if (currentIteration > ((long)repeatCount - 1) && !isLooping) + { + DoComplete(); + } + + // check if the current iteration should be reversed or not. + bool isCurIterReverse = animationDirection == PlaybackDirection.Normal ? false : + animationDirection == PlaybackDirection.Alternate ? (currentIteration % 2 == 0) ? false : true : + animationDirection == PlaybackDirection.AlternateReverse ? (currentIteration % 2 == 0) ? true : false : + animationDirection == PlaybackDirection.Reverse ? true : false; - while (!handled) + long x1 = delayFC; + long x2 = x1 + durationFC; + + if (delayFC > 0 & t >= 0 & t <= x1) + { + if (currentIteration == 0 && delayBetweenIterations) + DoDelay(); + + } + else if (t >= x1 & t <= x2) + { + var interpVal = t / (double)durationFC; + + if (isCurIterReverse) + interpVal = 1 - interpVal; + + var easedTime = EaseFunc.Ease(interpVal); + + lastInterpValue = Interpolator(easedTime, neutralValue); + targetObserver.OnNext(lastInterpValue); + } + else if (t > x2 & (currentIteration + 1 > repeatCount & !isLooping)) { - switch (currentState) - { - case KeyFramesStates.DoDelay: - - if (fillMode == FillMode.Backward - || fillMode == FillMode.Both) - { - if (currentIteration == 0) - { - targetObserver.OnNext(firstKFValue); - } - else - { - targetObserver.OnNext(lastInterpValue); - } - } - - if (delayFrameCount > delayTotalFrameCount) - { - currentState = KeyFramesStates.DoRun; - } - else - { - handled = true; - delayFrameCount++; - } - break; - - case KeyFramesStates.DoRun: - - if (isReversed) - currentState = KeyFramesStates.RunBackwards; - else - currentState = KeyFramesStates.RunForwards; - - break; - - case KeyFramesStates.RunForwards: - - if (durationFrameCount > durationTotalFrameCount) - { - currentState = KeyFramesStates.RunComplete; - } - else - { - tempDuration = (double)durationFrameCount / durationTotalFrameCount; - currentState = KeyFramesStates.RunApplyValue; - - } - break; - - case KeyFramesStates.RunBackwards: - - if (durationFrameCount > durationTotalFrameCount) - { - currentState = KeyFramesStates.RunComplete; - } - else - { - tempDuration = (double)(durationTotalFrameCount - durationFrameCount) / durationTotalFrameCount; - currentState = KeyFramesStates.RunApplyValue; - } - break; - - case KeyFramesStates.RunApplyValue: - - easedTime = targetAnimation.Easing.Ease(tempDuration); - - durationFrameCount++; - lastInterpValue = Interpolator(easedTime, neutralValue); - targetObserver.OnNext(lastInterpValue); - currentState = KeyFramesStates.DoRun; - handled = true; - break; - - case KeyFramesStates.RunComplete: - - if (checkLoopAndRepeat) - { - delayFrameCount = 0; - durationFrameCount = 0; - - if (isLooping) - { - currentState = KeyFramesStates.DoRun; - } - else if (isRepeating) - { - if (currentIteration >= repeatCount) - { - currentState = KeyFramesStates.Stop; - } - else - { - currentState = KeyFramesStates.DoRun; - } - currentIteration++; - } - - if (animationDirection == PlaybackDirection.Alternate - || animationDirection == PlaybackDirection.AlternateReverse) - isReversed = !isReversed; - - break; - } - - currentState = KeyFramesStates.Stop; - break; - - case KeyFramesStates.Stop: - - if (fillMode == FillMode.Forward - || fillMode == FillMode.Both) - { - targetControl.SetValue(parent.Property, lastInterpValue, BindingPriority.LocalValue); - } - targetObserver.OnCompleted(); - handled = true; - break; - default: - handled = true; - break; - } + DoComplete(); } } @@ -262,7 +198,7 @@ namespace Avalonia.Animation public void Dispose() { unsubscribe = true; - currentState = KeyFramesStates.Disposed; + isDisposed = true; } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 654ee327f8..cdf15c4c2f 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -104,7 +104,7 @@ namespace Avalonia.Animation Timing.AnimationStateTimer .TakeWhile(_ => !stateMachine.unsubscribe) - .Subscribe(p => stateMachine.Step(p, DoInterpolation)); + .Subscribe(p => stateMachine.Step(p.Item1, p.Item2, DoInterpolation)); return control.Bind((AvaloniaProperty)Property, stateMachine, BindingPriority.Animation); } From 0b248e6bb27509584eebf2b69899282882314d1d Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 2 Aug 2018 02:18:55 +0800 Subject: [PATCH 06/42] Optimize & remove excess numeric castings. --- .../AnimatorStateMachine`1.cs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 6854645cc2..21a9fe7a50 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -13,10 +13,10 @@ namespace Avalonia.Animation T lastInterpValue; T firstKFValue; - private long delayFC; - private long durationFC; + private double delayFC; + private double durationFC; private long repeatCount; - private long currentIteration; + private double currentIteration; private long firstFrameCount; private bool isLooping; @@ -52,8 +52,9 @@ namespace Avalonia.Animation neutralValue = (T)targetControl.GetValue(parent.Property); speedRatio = animation.SpeedRatio; - delayFC = (long)((animation.Delay.Ticks / Timing.FrameTick.Ticks) * speedRatio); - durationFC = (long)((animation.Duration.Ticks / Timing.FrameTick.Ticks) * speedRatio); + delayFC = ((animation.Delay.Ticks / Timing.FrameTick.Ticks) * speedRatio); + durationFC = ((animation.Duration.Ticks / Timing.FrameTick.Ticks) * speedRatio); + delayBetweenIterations = animation.DelayBetweenRepeats; switch (animation.RepeatCount.RepeatType) { @@ -85,7 +86,7 @@ namespace Avalonia.Animation targetObserver?.OnError(e); } } - + private void DoComplete() { if (fillMode == FillMode.Forward || fillMode == FillMode.Both) @@ -135,7 +136,7 @@ namespace Avalonia.Animation // currentState = savedState; // get the time with the initial fc as point of origin. - var t = (frameTick - firstFrameCount); + double t = (frameTick - firstFrameCount); // check if t is within the zeroth iteration if (t <= (delayFC + durationFC)) @@ -146,12 +147,12 @@ namespace Avalonia.Animation else { var totalDur = (double)((delayBetweenIterations ? delayFC : 0) + durationFC + 1); - currentIteration = (long)Math.Floor((double)t / totalDur); - t = t % (long)totalDur; + currentIteration = Math.Floor(t / totalDur); + t = t % totalDur; } // check if it's over the repeat count - if (currentIteration > ((long)repeatCount - 1) && !isLooping) + if (currentIteration > (repeatCount - 1) && !isLooping) { DoComplete(); } @@ -162,8 +163,8 @@ namespace Avalonia.Animation animationDirection == PlaybackDirection.AlternateReverse ? (currentIteration % 2 == 0) ? true : false : animationDirection == PlaybackDirection.Reverse ? true : false; - long x1 = delayFC; - long x2 = x1 + durationFC; + double x1 = delayFC; + double x2 = x1 + durationFC; if (delayFC > 0 & t >= 0 & t <= x1) { @@ -173,7 +174,7 @@ namespace Avalonia.Animation } else if (t >= x1 & t <= x2) { - var interpVal = t / (double)durationFC; + var interpVal = t / durationFC; if (isCurIterReverse) interpVal = 1 - interpVal; From cfabbe9b65b9514c5bf3a4a27701f2af37d32e2b Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 3 Aug 2018 12:46:07 +0800 Subject: [PATCH 07/42] More optimizations. --- .../AnimatorStateMachine`1.cs | 12 +++++------ src/Avalonia.Animation/Timing.cs | 21 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 21a9fe7a50..8bfdf8b04a 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -163,16 +163,16 @@ namespace Avalonia.Animation animationDirection == PlaybackDirection.AlternateReverse ? (currentIteration % 2 == 0) ? true : false : animationDirection == PlaybackDirection.Reverse ? true : false; - double x1 = delayFC; - double x2 = x1 + durationFC; + double delayEndpoint = delayFC; + double iterationEndpoint = delayEndpoint + durationFC; - if (delayFC > 0 & t >= 0 & t <= x1) + if (delayFC > 0 & t >= 0 & t <= delayEndpoint) { - if (currentIteration == 0 && delayBetweenIterations) + if (currentIteration == 0 || delayBetweenIterations) DoDelay(); } - else if (t >= x1 & t <= x2) + else if (t >= delayEndpoint & t <= iterationEndpoint) { var interpVal = t / durationFC; @@ -184,7 +184,7 @@ namespace Avalonia.Animation lastInterpValue = Interpolator(easedTime, neutralValue); targetObserver.OnNext(lastInterpValue); } - else if (t > x2 & (currentIteration + 1 > repeatCount & !isLooping)) + else if (t > iterationEndpoint & (currentIteration + 1 > repeatCount & !isLooping)) { DoComplete(); } diff --git a/src/Avalonia.Animation/Timing.cs b/src/Avalonia.Animation/Timing.cs index 3eddc0ac26..fb61e15f05 100644 --- a/src/Avalonia.Animation/Timing.cs +++ b/src/Avalonia.Animation/Timing.cs @@ -15,15 +15,17 @@ namespace Avalonia.Animation /// public static class Timing { - static ulong _transitionsFrameCount; static long _tickStartTimeStamp; static PlayState _globalState = PlayState.Run; + static long TicksPerFrame = Stopwatch.Frequency / FramesPerSecond; + /// /// The number of frames per second. /// public const int FramesPerSecond = 60; + /// /// The time span of each frame. /// @@ -39,17 +41,18 @@ namespace Avalonia.Animation var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance); + AnimationStateTimer = globalTimer .Select(_ => { return (_globalState, (Stopwatch.GetTimestamp() - _tickStartTimeStamp) - / (Stopwatch.Frequency / FramesPerSecond)); + / TicksPerFrame); }) .Publish() .RefCount(); TransitionsTimer = globalTimer - .Select(p => _transitionsFrameCount++) + .Select(p => p) .Publish() .RefCount(); } @@ -95,7 +98,7 @@ namespace Avalonia.Animation /// The parameter passed to a subsciber is the number of frames since the animation system was /// initialized. /// - public static IObservable TransitionsTimer + public static IObservable TransitionsTimer { get; } @@ -113,16 +116,14 @@ namespace Avalonia.Animation /// public static IObservable GetTransitionsTimer(Animatable control, TimeSpan duration, TimeSpan delay = default(TimeSpan)) { - var startTime = _transitionsFrameCount; - var _duration = (ulong)(duration.Ticks / FrameTick.Ticks); - var endTime = startTime + _duration; + var _duration = (duration.Ticks / FrameTick.Ticks); + var endTime = _duration; return TransitionsTimer .TakeWhile(x => x < endTime) - .Select(x => (double)(x - startTime) / _duration) + .Select(x => (double)x / _duration) .StartWith(0.0) .Concat(Observable.Return(1.0)); } - } -} +} \ No newline at end of file From 1757604dab071f6d09b4e1b63a1eddad1c708f72 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 3 Aug 2018 12:47:01 +0800 Subject: [PATCH 08/42] Fix onComplete assignment. --- src/Avalonia.Animation/AnimatorStateMachine`1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 32acafcfc7..18ad76c51e 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -73,7 +73,7 @@ namespace Avalonia.Animation animationDirection = animation.PlaybackDirection; fillMode = animation.FillMode; - onComplete = onComplete; + this.onComplete = onComplete; } public void Step(PlayState playState, long frameTick, Func Interpolator) From b020bddb20bae6f4ca299cff7858ee9d60af7e95 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 3 Aug 2018 12:47:01 +0800 Subject: [PATCH 09/42] Fix onComplete assignment. --- src/Avalonia.Animation/Animation.cs | 7 +------ src/Avalonia.Animation/AnimatorStateMachine`1.cs | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 46179aa3f4..344b63d15c 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -85,12 +85,7 @@ namespace Avalonia.Animation /// Sets the behavior for having a delay between repeats for this animation. /// public bool DelayBetweenRepeats { get; set; } - - public Animation() - { - this.CollectionChanged += delegate { _isChildrenChanged = true; }; - } - + private (IList Animators, IList subscriptions) InterpretKeyframes(Animatable control) { var handlerList = new List<(Type type, AvaloniaProperty property)>(); diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 32acafcfc7..18ad76c51e 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -73,7 +73,7 @@ namespace Avalonia.Animation animationDirection = animation.PlaybackDirection; fillMode = animation.FillMode; - onComplete = onComplete; + this.onComplete = onComplete; } public void Step(PlayState playState, long frameTick, Func Interpolator) From 1b37397e69b97a1f9a7274a8ceb5644129475ae0 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 3 Aug 2018 17:38:41 +0800 Subject: [PATCH 10/42] Fix bug on PageTransitions by adding 1 to the zeroth iteration frame count modulo. --- src/Avalonia.Animation/AnimatorStateMachine`1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 18ad76c51e..38ccce6cb5 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -145,7 +145,7 @@ namespace Avalonia.Animation if (t <= (delayFC + durationFC)) { currentIteration = 0; - t = t % (delayFC + durationFC); + t = t % (delayFC + durationFC + 1); } else { From bfefeb9e9c63830e3cd17311be7f3c5ef7b6bb7c Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Fri, 3 Aug 2018 17:39:19 +0800 Subject: [PATCH 11/42] Make Transitions Bind strongly typed. --- src/Avalonia.Animation/Transition`1.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Animation/Transition`1.cs b/src/Avalonia.Animation/Transition`1.cs index 5db3082deb..c097b930a5 100644 --- a/src/Avalonia.Animation/Transition`1.cs +++ b/src/Avalonia.Animation/Transition`1.cs @@ -24,17 +24,7 @@ namespace Avalonia.Animation /// /// Gets the easing class to be used. /// - public Easing Easing - { - get - { - return _easing ?? (_easing = new LinearEasing()); - } - set - { - _easing = value; - } - } + public Easing Easing { get; set; } = new LinearEasing(); /// public AvaloniaProperty Property @@ -61,8 +51,8 @@ namespace Avalonia.Animation /// public virtual IDisposable Apply(Animatable control, object oldValue, object newValue) { - var transition = DoTransition(Timing.GetTransitionsTimer(control, Duration, TimeSpan.Zero), (T)oldValue, (T)newValue).Select(p => (object)p); - return control.Bind(Property, transition, Data.BindingPriority.Animation); + var transition = DoTransition(Timing.GetTransitionsTimer(control, Duration, TimeSpan.Zero), (T)oldValue, (T)newValue); + return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); } } } \ No newline at end of file From 714606b2ad8bd0d8adb3acc4d30701a8f195d2f7 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sun, 12 Aug 2018 01:26:03 +0800 Subject: [PATCH 12/42] Add PlayState support. Redoing the main algorithm yet again. --- .../ViewModels/AnimationsPageViewModel.cs | 10 +- src/Avalonia.Animation/Animatable.cs | 21 +---- src/Avalonia.Animation/Animation.cs | 87 ++++++++++-------- .../AnimatorStateMachine`1.cs | 92 ++++++++++--------- src/Avalonia.Animation/Animator`1.cs | 21 ++--- src/Avalonia.Animation/Timing.cs | 59 +++--------- 6 files changed, 127 insertions(+), 163 deletions(-) diff --git a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs index 626a3e7c77..c76d4db513 100644 --- a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs +++ b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs @@ -10,21 +10,21 @@ namespace RenderDemo.ViewModels public AnimationsPageViewModel() { - ToggleGlobalPlayState = ReactiveCommand.Create(()=>TogglePlayState()); + ToggleGlobalPlayState = ReactiveCommand.Create(() => TogglePlayState()); } void TogglePlayState() { - switch (Timing.GetGlobalPlayState()) + switch (Timing.GlobalPlayState) { case PlayState.Run: PlayStateText = "Resume all animations"; - Timing.SetGlobalPlayState(PlayState.Pause); + Timing.GlobalPlayState = PlayState.Pause; break; case PlayState.Pause: PlayStateText = "Pause all animations"; - Timing.SetGlobalPlayState(PlayState.Run); + Timing.GlobalPlayState = PlayState.Run; break; } } @@ -36,5 +36,5 @@ namespace RenderDemo.ViewModels } public ReactiveCommand ToggleGlobalPlayState { get; } - } + } } diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 85317af1a8..303e01aed8 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -23,25 +23,8 @@ namespace Avalonia.Animation /// public Animatable() { - Transitions = new Transitions(); - AnimatableTimer = Timing.AnimationStateTimer - .Select(p => - { - if (this._playState == PlayState.Pause) - { - return (PlayState.Pause, p.Item2); - } - else return p; - }) - .Publish() - .RefCount(); - } - - /// - /// The specific animations timer for this control. - /// - /// - public IObservable<(PlayState, long)> AnimatableTimer; + Transitions = new Transitions(); + } /// /// Defines the property. diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 344b63d15c..da2fc75c0b 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -21,70 +21,81 @@ namespace Avalonia.Animation /// public class Animation : AvaloniaList, IAnimation { - private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> - { - ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ) - }; - - public static void RegisterAnimator(Func 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; - } public AvaloniaList _animators { get; set; } = new AvaloniaList(); /// - /// Run time of this animation. + /// Gets or sets the active time of this animation. /// public TimeSpan Duration { get; set; } /// - /// Delay time for this animation. - /// - public TimeSpan Delay { get; set; } - - /// - /// The repeat count for this animation. + /// Gets or sets the repeat count for this animation. /// public RepeatCount RepeatCount { get; set; } /// - /// The playback direction for this animation. + /// Gets or sets the playback direction for this animation. /// public PlaybackDirection PlaybackDirection { get; set; } /// - /// The value fill mode for this animation. + /// Gets or sets the value fill mode for this animation. /// public FillMode FillMode { get; set; } /// - /// Easing function to be used. + /// Gets or sets the easing function to be used for this animation. /// public Easing Easing { get; set; } = new LinearEasing(); - + /// - /// Sets the speed multiple for this animation. + /// Gets or sets the speed multiple for this animation. /// public double SpeedRatio { get; set; } = 1d; - /// - /// Sets the behavior for having a delay between repeats for this animation. - /// - public bool DelayBetweenRepeats { get; set; } + /// + /// Gets or sets the delay time for this animation. + /// + /// + /// Describes a delay to be added before the animation starts, and optionally between + /// repeats of the animation if is set. + /// + public TimeSpan Delay { get; set; } + + /// + /// Gets or sets a value indicating whether will be applied between + /// iterations of the animation. + /// + /// + /// If this property is not set, then will only be applied to the first + /// iteration of the animation. + /// + public bool DelayBetweenIterations { get; set; } + + + private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> + { + ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ) + }; + + public static void RegisterAnimator(Func 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 (IList Animators, IList subscriptions) InterpretKeyframes(Animatable control) { diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 38ccce6cb5..47bfa1c321 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -9,7 +9,7 @@ namespace Avalonia.Animation /// Provides statefulness for an iteration of a keyframe animation. /// internal class AnimatorStateMachine : IObservable, IDisposable - { + { T lastInterpValue; T firstKFValue; @@ -34,6 +34,11 @@ namespace Avalonia.Animation internal bool unsubscribe; private bool isDisposed; + private long? internalClock; + + private long? previousClock = null; + private long currentDiscreteTime; + private Easings.Easing EaseFunc; private IObserver targetObserver; private readonly Action onComplete; @@ -55,7 +60,7 @@ namespace Avalonia.Animation speedRatio = animation.SpeedRatio; delayFC = ((animation.Delay.Ticks / Timing.FrameTick.Ticks) * speedRatio); durationFC = ((animation.Duration.Ticks / Timing.FrameTick.Ticks) * speedRatio); - delayBetweenIterations = animation.DelayBetweenRepeats; + delayBetweenIterations = animation.DelayBetweenIterations; switch (animation.RepeatCount.RepeatType) { @@ -72,15 +77,15 @@ namespace Avalonia.Animation } animationDirection = animation.PlaybackDirection; - fillMode = animation.FillMode; + fillMode = animation.FillMode; this.onComplete = onComplete; } - public void Step(PlayState playState, long frameTick, Func Interpolator) + public void Step(long frameTick, Func Interpolator) { try { - InternalStep(playState, frameTick, Interpolator); + InternalStep(frameTick, Interpolator); } catch (Exception e) { @@ -107,8 +112,31 @@ namespace Avalonia.Animation targetObserver.OnNext(lastInterpValue); } - private void InternalStep(PlayState playState, long frameTick, Func Interpolator) + private void InternalStep(long time, Func Interpolator) { + if (Timing.GlobalPlayState == PlayState.Stop || targetControl.PlayState == PlayState.Stop) + DoComplete(); + + if (!previousClock.HasValue) + { + previousClock = time; + internalClock = 0; + } + else + { + if (Timing.GlobalPlayState == PlayState.Pause || targetControl.PlayState == PlayState.Pause) + { + previousClock = time; + return; + } + var delta = time - previousClock; + internalClock += delta; + previousClock = time; + } + + // currentDiscreteTime = internalClock.Value; + currentDiscreteTime++; + if (!gotFirstKFValue) { firstKFValue = (T)parent.First().Value; @@ -117,42 +145,23 @@ namespace Avalonia.Animation if (!gotFirstFrameCount) { - firstFrameCount = frameTick; + firstFrameCount = currentDiscreteTime; gotFirstFrameCount = true; } if (isDisposed) throw new InvalidProgramException("This KeyFrames Animation is already disposed."); - if (playState == PlayState.Stop) - DoComplete(); - - // Save state and pause the machine - // if (playState == PlayState.Pause && currentState != KeyFramesStates.Pause) - // { - // savedState = currentState; - // currentState = KeyFramesStates.Pause; - // } - - // // Resume the previous state - // if (playState != PlayState.Pause && currentState == KeyFramesStates.Pause) - // currentState = savedState; - // get the time with the initial fc as point of origin. - double t = (frameTick - firstFrameCount); + double t = (currentDiscreteTime - firstFrameCount); // check if t is within the zeroth iteration - if (t <= (delayFC + durationFC)) - { - currentIteration = 0; - t = t % (delayFC + durationFC + 1); - } - else - { - var totalDur = (double)((delayBetweenIterations ? delayFC : 0) + durationFC + 1); - currentIteration = Math.Floor(t / totalDur); - t = t % totalDur; - } + + double delayEndpoint = delayFC; + double iterationEndpoint = delayEndpoint + durationFC; + + currentIteration = Math.Floor(t / iterationEndpoint); + t = t % iterationEndpoint; // check if it's over the repeat count if (currentIteration > (repeatCount - 1) && !isLooping) @@ -166,19 +175,17 @@ namespace Avalonia.Animation animationDirection == PlaybackDirection.AlternateReverse ? (currentIteration % 2 == 0) ? true : false : animationDirection == PlaybackDirection.Reverse ? true : false; - double delayEndpoint = delayFC; - double iterationEndpoint = delayEndpoint + durationFC; - if (delayFC > 0 & t >= 0 & t <= delayEndpoint) + if (delayFC > 0 & t <= delayEndpoint) { - if (currentIteration == 0 || delayBetweenIterations) + if (currentIteration == 0) DoDelay(); - } - else if (t >= delayEndpoint & t <= iterationEndpoint) + else if (t > delayEndpoint & t < iterationEndpoint) { - var interpVal = t / durationFC; - + double k = t - delayFC; + var interpVal = k / (double)durationFC; + if (isCurIterReverse) interpVal = 1 - interpVal; @@ -187,7 +194,7 @@ namespace Avalonia.Animation lastInterpValue = Interpolator(easedTime, neutralValue); targetObserver.OnNext(lastInterpValue); } - else if (t > iterationEndpoint & (currentIteration + 1 > repeatCount & !isLooping)) + else if (t > iterationEndpoint && !isLooping) { DoComplete(); } @@ -198,7 +205,6 @@ namespace Avalonia.Animation targetObserver = observer; return this; } - public void Dispose() { unsubscribe = true; diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 607ccee947..a8b5ce7a27 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -21,7 +21,7 @@ namespace Avalonia.Animation /// private readonly SortedList _convertedKeyframes = new SortedList(); - private bool _isVerfifiedAndConverted; + private bool isVerfifiedAndConverted; /// /// Gets or sets the target property for the keyframe. @@ -31,18 +31,17 @@ namespace Avalonia.Animation public Animator() { // Invalidate keyframes when changed. - this.CollectionChanged += delegate { _isVerfifiedAndConverted = false; }; + this.CollectionChanged += delegate { isVerfifiedAndConverted = false; }; } /// - public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete) + public virtual IDisposable Apply(Animation animation, Animatable control, IObservable Match, Action onComplete) { - if (!_isVerfifiedAndConverted) + if (!isVerfifiedAndConverted) VerifyConvertKeyFrames(); - return obsMatch - // Ignore triggers when global timers are paused. - .Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause) + return Match + .Where(p => p) .Subscribe(_ => { var timerObs = RunKeyFrames(animation, control, onComplete); @@ -101,9 +100,9 @@ namespace Avalonia.Animation { var stateMachine = new AnimatorStateMachine(animation, control, this, onComplete); - Timing.AnimationStateTimer + Timing.AnimationsTimer .TakeWhile(_ => !stateMachine.unsubscribe) - .Subscribe(p => stateMachine.Step(p.Item1, p.Item2, DoInterpolation)); + .Subscribe(p => stateMachine.Step(p, DoInterpolation)); return control.Bind((AvaloniaProperty)Property, stateMachine, BindingPriority.Animation); } @@ -124,7 +123,7 @@ namespace Avalonia.Animation } AddNeutralKeyFramesIfNeeded(); - _isVerfifiedAndConverted = true; + isVerfifiedAndConverted = true; } @@ -133,7 +132,7 @@ namespace Avalonia.Animation bool hasStartKey, hasEndKey; hasStartKey = hasEndKey = false; - // Make start and end keyframe mandatory. + // Check if there's start and end keyframes. foreach (var converted in _convertedKeyframes.Keys) { if (DoubleUtils.AboutEqual(converted, 0.0)) diff --git a/src/Avalonia.Animation/Timing.cs b/src/Avalonia.Animation/Timing.cs index fb61e15f05..c6def06b15 100644 --- a/src/Avalonia.Animation/Timing.cs +++ b/src/Avalonia.Animation/Timing.cs @@ -16,16 +16,18 @@ namespace Avalonia.Animation public static class Timing { static long _tickStartTimeStamp; - static PlayState _globalState = PlayState.Run; static long TicksPerFrame = Stopwatch.Frequency / FramesPerSecond; + /// + /// Gets or sets the animation play state for all animations + /// + public static PlayState GlobalPlayState { get; set; } = PlayState.Run; /// /// The number of frames per second. /// public const int FramesPerSecond = 60; - /// /// The time span of each frame. /// @@ -36,45 +38,20 @@ namespace Avalonia.Animation /// static Timing() { - _tickStartTimeStamp = Stopwatch.GetTimestamp(); var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance); - - AnimationStateTimer = globalTimer + AnimationsTimer = globalTimer .Select(_ => { - return (_globalState, (Stopwatch.GetTimestamp() - _tickStartTimeStamp) - / TicksPerFrame); + return (Stopwatch.GetTimestamp() - _tickStartTimeStamp) + / TicksPerFrame * 2; }) .Publish() .RefCount(); - - TransitionsTimer = globalTimer - .Select(p => p) - .Publish() - .RefCount(); - } - - - /// - /// Sets the animation play state for all animations - /// - public static void SetGlobalPlayState(PlayState playState) - { - Dispatcher.UIThread.VerifyAccess(); - _globalState = playState; } - /// - /// Gets the animation play state for all animations - /// - public static PlayState GetGlobalPlayState() - { - Dispatcher.UIThread.VerifyAccess(); - return _globalState; - } /// /// Gets the animation timer. @@ -84,21 +61,7 @@ namespace Avalonia.Animation /// defined in . /// The parameter passed to a subsciber is the current playstate of the animation. /// - internal static IObservable<(PlayState, long)> AnimationStateTimer - { - get; - } - - /// - /// Gets the transitions timer. - /// - /// - /// The transitions timer increments usually 60 times per second as - /// defined in . - /// The parameter passed to a subsciber is the number of frames since the animation system was - /// initialized. - /// - public static IObservable TransitionsTimer + internal static IObservable AnimationsTimer { get; } @@ -116,10 +79,12 @@ namespace Avalonia.Animation /// public static IObservable GetTransitionsTimer(Animatable control, TimeSpan duration, TimeSpan delay = default(TimeSpan)) { + // TODO: Fix this mess. var _duration = (duration.Ticks / FrameTick.Ticks); - var endTime = _duration; + long? endTime = ((Stopwatch.GetTimestamp() - _tickStartTimeStamp) + / TicksPerFrame) + _duration; - return TransitionsTimer + return AnimationsTimer .TakeWhile(x => x < endTime) .Select(x => (double)x / _duration) .StartWith(0.0) From dc4c7cf4ca66e3b59580fc09e51cb40f57130e29 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Sun, 12 Aug 2018 13:32:55 +0800 Subject: [PATCH 13/42] Make the algorithm use TimeSpans directly instead of converting to quantized framecounts to avoid/reduce quantization errors in interpolation. --- .../AnimatorStateMachine`1.cs | 81 +++++++++---------- src/Avalonia.Animation/Timing.cs | 30 +++---- 2 files changed, 50 insertions(+), 61 deletions(-) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 47bfa1c321..e5a6864899 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -13,11 +13,8 @@ namespace Avalonia.Animation T lastInterpValue; T firstKFValue; - private double delayFC; - private double durationFC; private long repeatCount; private double currentIteration; - private long firstFrameCount; private bool isLooping; private bool isRepeating; @@ -34,10 +31,11 @@ namespace Avalonia.Animation internal bool unsubscribe; private bool isDisposed; - private long? internalClock; - - private long? previousClock = null; - private long currentDiscreteTime; + private TimeSpan delayFC; + private TimeSpan durationFC; + private TimeSpan firstFrameCount; + private TimeSpan internalClock; + private TimeSpan? previousClock; private Easings.Easing EaseFunc; private IObserver targetObserver; @@ -58,8 +56,10 @@ namespace Avalonia.Animation neutralValue = (T)targetControl.GetValue(parent.Property); speedRatio = animation.SpeedRatio; - delayFC = ((animation.Delay.Ticks / Timing.FrameTick.Ticks) * speedRatio); - durationFC = ((animation.Duration.Ticks / Timing.FrameTick.Ticks) * speedRatio); + + delayFC = animation.Delay; + durationFC = animation.Duration; + delayBetweenIterations = animation.DelayBetweenIterations; switch (animation.RepeatCount.RepeatType) @@ -81,7 +81,7 @@ namespace Avalonia.Animation this.onComplete = onComplete; } - public void Step(long frameTick, Func Interpolator) + public void Step(TimeSpan frameTick, Func Interpolator) { try { @@ -112,31 +112,28 @@ namespace Avalonia.Animation targetObserver.OnNext(lastInterpValue); } - private void InternalStep(long time, Func Interpolator) + private void DoPlayStatesAndTime(TimeSpan systemTime) { if (Timing.GlobalPlayState == PlayState.Stop || targetControl.PlayState == PlayState.Stop) DoComplete(); if (!previousClock.HasValue) { - previousClock = time; - internalClock = 0; + previousClock = systemTime; + internalClock = TimeSpan.Zero; } else { if (Timing.GlobalPlayState == PlayState.Pause || targetControl.PlayState == PlayState.Pause) { - previousClock = time; + previousClock = systemTime; return; } - var delta = time - previousClock; - internalClock += delta; - previousClock = time; + var delta = systemTime - previousClock; + internalClock += delta.Value; + previousClock = systemTime; } - // currentDiscreteTime = internalClock.Value; - currentDiscreteTime++; - if (!gotFirstKFValue) { firstKFValue = (T)parent.First().Value; @@ -145,47 +142,47 @@ namespace Avalonia.Animation if (!gotFirstFrameCount) { - firstFrameCount = currentDiscreteTime; + firstFrameCount = internalClock; gotFirstFrameCount = true; } + } + + private void InternalStep(TimeSpan systemTime, Func Interpolator) + { + DoPlayStatesAndTime(systemTime); if (isDisposed) throw new InvalidProgramException("This KeyFrames Animation is already disposed."); - // get the time with the initial fc as point of origin. - double t = (currentDiscreteTime - firstFrameCount); - - // check if t is within the zeroth iteration + var t = internalClock - firstFrameCount; - double delayEndpoint = delayFC; - double iterationEndpoint = delayEndpoint + durationFC; + var delayEndpoint = delayFC; + var iterationEndpoint = delayEndpoint + durationFC; - currentIteration = Math.Floor(t / iterationEndpoint); - t = t % iterationEndpoint; + currentIteration = (int)Math.Floor((double)t.Ticks / iterationEndpoint.Ticks); + t = TimeSpan.FromTicks(t.Ticks % iterationEndpoint.Ticks); - // check if it's over the repeat count if (currentIteration > (repeatCount - 1) && !isLooping) - { DoComplete(); - } - // check if the current iteration should be reversed or not. + if (t > iterationEndpoint & !isLooping) + DoComplete(); + bool isCurIterReverse = animationDirection == PlaybackDirection.Normal ? false : animationDirection == PlaybackDirection.Alternate ? (currentIteration % 2 == 0) ? false : true : animationDirection == PlaybackDirection.AlternateReverse ? (currentIteration % 2 == 0) ? true : false : animationDirection == PlaybackDirection.Reverse ? true : false; - - if (delayFC > 0 & t <= delayEndpoint) + if (delayFC > TimeSpan.Zero & t < delayEndpoint) { if (currentIteration == 0) DoDelay(); } - else if (t > delayEndpoint & t < iterationEndpoint) + else if (t >= delayEndpoint & t <= iterationEndpoint) { - double k = t - delayFC; - var interpVal = k / (double)durationFC; - + var k = t - delayFC; + var interpVal = (double)k.Ticks / durationFC.Ticks; + if (isCurIterReverse) interpVal = 1 - interpVal; @@ -194,10 +191,7 @@ namespace Avalonia.Animation lastInterpValue = Interpolator(easedTime, neutralValue); targetObserver.OnNext(lastInterpValue); } - else if (t > iterationEndpoint && !isLooping) - { - DoComplete(); - } + } public IDisposable Subscribe(IObserver observer) @@ -205,6 +199,7 @@ namespace Avalonia.Animation targetObserver = observer; return this; } + public void Dispose() { unsubscribe = true; diff --git a/src/Avalonia.Animation/Timing.cs b/src/Avalonia.Animation/Timing.cs index c6def06b15..575cedc620 100644 --- a/src/Avalonia.Animation/Timing.cs +++ b/src/Avalonia.Animation/Timing.cs @@ -15,9 +15,6 @@ namespace Avalonia.Animation /// public static class Timing { - static long _tickStartTimeStamp; - static long TicksPerFrame = Stopwatch.Frequency / FramesPerSecond; - /// /// Gets or sets the animation play state for all animations /// @@ -37,22 +34,18 @@ namespace Avalonia.Animation /// Initializes static members of the class. /// static Timing() - { - _tickStartTimeStamp = Stopwatch.GetTimestamp(); - + { var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance); AnimationsTimer = globalTimer .Select(_ => { - return (Stopwatch.GetTimestamp() - _tickStartTimeStamp) - / TicksPerFrame * 2; + return TimeSpan.FromMilliseconds(Environment.TickCount); }) .Publish() .RefCount(); } - /// /// Gets the animation timer. /// @@ -61,7 +54,7 @@ namespace Avalonia.Animation /// defined in . /// The parameter passed to a subsciber is the current playstate of the animation. /// - internal static IObservable AnimationsTimer + internal static IObservable AnimationsTimer { get; } @@ -80,15 +73,16 @@ namespace Avalonia.Animation public static IObservable GetTransitionsTimer(Animatable control, TimeSpan duration, TimeSpan delay = default(TimeSpan)) { // TODO: Fix this mess. - var _duration = (duration.Ticks / FrameTick.Ticks); - long? endTime = ((Stopwatch.GetTimestamp() - _tickStartTimeStamp) - / TicksPerFrame) + _duration; + // var _duration = (duration.Ticks / FrameTick.Ticks); + // long? endTime = ((Stopwatch.GetTimestamp() - _tickStartTimeStamp) + // / TicksPerFrame) + _duration; - return AnimationsTimer - .TakeWhile(x => x < endTime) - .Select(x => (double)x / _duration) - .StartWith(0.0) - .Concat(Observable.Return(1.0)); + // return AnimationsTimer + // .TakeWhile(x => x < endTime) + // .Select(x => (double)x / _duration) + // .StartWith(0.0) + // .Concat(Observable.Return(1.0)); + return Observable.Empty(); } } } \ No newline at end of file From b6e6b7db48c8c279b74d8b0b8a20d1dc1316970d Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 14 Aug 2018 15:38:55 +0800 Subject: [PATCH 14/42] Fix Delay and Iteration Delay behaviors. AnimatorStateMachine is functionally complete. --- .../AnimatorStateMachine`1.cs | 104 +++++++++++------- 1 file changed, 65 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index e5a6864899..0a785ab496 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -6,7 +6,7 @@ using Avalonia.Data; namespace Avalonia.Animation { /// - /// Provides statefulness for an iteration of a keyframe animation. + /// Provides statefulness for keyframe animations. /// internal class AnimatorStateMachine : IObservable, IDisposable { @@ -17,10 +17,9 @@ namespace Avalonia.Animation private double currentIteration; private bool isLooping; - private bool isRepeating; private bool gotFirstKFValue; private bool gotFirstFrameCount; - private bool delayBetweenIterations; + private bool iterationDelay; private FillMode fillMode; private PlaybackDirection animationDirection; @@ -31,36 +30,34 @@ namespace Avalonia.Animation internal bool unsubscribe; private bool isDisposed; - private TimeSpan delayFC; - private TimeSpan durationFC; + private TimeSpan delay; + private TimeSpan duration; private TimeSpan firstFrameCount; private TimeSpan internalClock; private TimeSpan? previousClock; - private Easings.Easing EaseFunc; + private Easings.Easing easeFunc; private IObserver targetObserver; - private readonly Action onComplete; + private readonly Action onCompleteAction; - public AnimatorStateMachine(Animation animation, Animatable control, Animator animator, Action onComplete) + public AnimatorStateMachine(Animation animation, Animatable control, Animator animator, Action OnComplete) { - if (animation.SpeedRatio <= 0 || DoubleUtils.AboutEqual(animation.SpeedRatio, 0)) throw new InvalidOperationException("Speed ratio cannot be negative or zero."); if (animation.Duration.TotalSeconds <= 0 || DoubleUtils.AboutEqual(animation.Duration.TotalSeconds, 0)) - throw new InvalidOperationException("Animation duration cannot be negative or zero."); - + throw new InvalidOperationException("Duration cannot be negative or zero."); + parent = animator; - EaseFunc = animation.Easing; + easeFunc = animation.Easing; targetControl = control; neutralValue = (T)targetControl.GetValue(parent.Property); speedRatio = animation.SpeedRatio; - delayFC = animation.Delay; - durationFC = animation.Duration; - - delayBetweenIterations = animation.DelayBetweenIterations; + delay = animation.Delay; + duration = animation.Duration; + iterationDelay = animation.DelayBetweenIterations; switch (animation.RepeatCount.RepeatType) { @@ -71,14 +68,13 @@ namespace Avalonia.Animation isLooping = true; break; case RepeatType.Repeat: - isRepeating = true; repeatCount = (long)animation.RepeatCount.Value; break; } animationDirection = animation.PlaybackDirection; fillMode = animation.FillMode; - this.onComplete = onComplete; + onCompleteAction = OnComplete; } public void Step(TimeSpan frameTick, Func Interpolator) @@ -99,7 +95,7 @@ namespace Avalonia.Animation targetControl.SetValue(parent.Property, lastInterpValue, BindingPriority.LocalValue); targetObserver.OnCompleted(); - onComplete?.Invoke(); + onCompleteAction?.Invoke(); Dispose(); } @@ -154,44 +150,74 @@ namespace Avalonia.Animation if (isDisposed) throw new InvalidProgramException("This KeyFrames Animation is already disposed."); - var t = internalClock - firstFrameCount; + var time = internalClock - firstFrameCount; + var delayEndpoint = delay; + var iterationEndpoint = delayEndpoint + duration; - var delayEndpoint = delayFC; - var iterationEndpoint = delayEndpoint + durationFC; + //determine if time is currently in the first iteration. + if (time >= TimeSpan.Zero & time <= iterationEndpoint) + { + currentIteration = 1; + } + else if (time > iterationEndpoint) + { + //Subtract first iteration to properly get the subsequent iteration time + time -= iterationEndpoint; - currentIteration = (int)Math.Floor((double)t.Ticks / iterationEndpoint.Ticks); - t = TimeSpan.FromTicks(t.Ticks % iterationEndpoint.Ticks); + if (!iterationDelay & delayEndpoint > TimeSpan.Zero) + { + delayEndpoint = TimeSpan.Zero; + iterationEndpoint = duration; + } - if (currentIteration > (repeatCount - 1) && !isLooping) - DoComplete(); + //Calculate the current iteration number + currentIteration = (int)Math.Floor((double)time.Ticks / iterationEndpoint.Ticks) + 2; + } + else + { + previousClock = systemTime; + return; + } - if (t > iterationEndpoint & !isLooping) - DoComplete(); - + time = TimeSpan.FromTicks(time.Ticks % iterationEndpoint.Ticks); + + if (!isLooping) + { + if (currentIteration > repeatCount) + DoComplete(); + + if (time > iterationEndpoint) + DoComplete(); + } + + // Determine if the current iteration should have its normalized time inverted. bool isCurIterReverse = animationDirection == PlaybackDirection.Normal ? false : animationDirection == PlaybackDirection.Alternate ? (currentIteration % 2 == 0) ? false : true : animationDirection == PlaybackDirection.AlternateReverse ? (currentIteration % 2 == 0) ? true : false : animationDirection == PlaybackDirection.Reverse ? true : false; - if (delayFC > TimeSpan.Zero & t < delayEndpoint) + if (delayEndpoint > TimeSpan.Zero & time < delayEndpoint) { - if (currentIteration == 0) - DoDelay(); + DoDelay(); } - else if (t >= delayEndpoint & t <= iterationEndpoint) + else { - var k = t - delayFC; - var interpVal = (double)k.Ticks / durationFC.Ticks; + // Offset the delay time + time -= delayEndpoint; + iterationEndpoint -= delayEndpoint; + + // Normalize time + var interpVal = (double)time.Ticks / iterationEndpoint.Ticks; if (isCurIterReverse) interpVal = 1 - interpVal; - var easedTime = EaseFunc.Ease(interpVal); - + // Ease and interpolate + var easedTime = easeFunc.Ease(interpVal); lastInterpValue = Interpolator(easedTime, neutralValue); + targetObserver.OnNext(lastInterpValue); } - } public IDisposable Subscribe(IObserver observer) @@ -199,7 +225,7 @@ namespace Avalonia.Animation targetObserver = observer; return this; } - + public void Dispose() { unsubscribe = true; From 5d49c5f969be55d34408010aed3d1d54a69ed1fc Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 14 Aug 2018 23:27:02 +0800 Subject: [PATCH 15/42] Rename AnimatorStateMachine to AnimationsEngine to properly reflect the functions of the new algorithm. --- .../{AnimatorStateMachine`1.cs => AnimationsEngine`1.cs} | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) rename src/Avalonia.Animation/{AnimatorStateMachine`1.cs => AnimationsEngine`1.cs} (96%) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimationsEngine`1.cs similarity index 96% rename from src/Avalonia.Animation/AnimatorStateMachine`1.cs rename to src/Avalonia.Animation/AnimationsEngine`1.cs index 0a785ab496..169f0a7ae0 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimationsEngine`1.cs @@ -6,9 +6,10 @@ using Avalonia.Data; namespace Avalonia.Animation { /// - /// Provides statefulness for keyframe animations. + /// Handles interpolatoin and time-related functions + /// for keyframe animations. /// - internal class AnimatorStateMachine : IObservable, IDisposable + internal class AnimationsEngine : IObservable, IDisposable { T lastInterpValue; T firstKFValue; @@ -40,7 +41,7 @@ namespace Avalonia.Animation private IObserver targetObserver; private readonly Action onCompleteAction; - public AnimatorStateMachine(Animation animation, Animatable control, Animator animator, Action OnComplete) + public AnimationsEngine(Animation animation, Animatable control, Animator animator, Action OnComplete) { if (animation.SpeedRatio <= 0 || DoubleUtils.AboutEqual(animation.SpeedRatio, 0)) throw new InvalidOperationException("Speed ratio cannot be negative or zero."); From 3d7516cc0c1f82fc7d703f4c3d9add31dc9a0242 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 14 Aug 2018 23:54:48 +0800 Subject: [PATCH 16/42] Implement a new timing engine for Transitions. Transtions are fixed now.Move out some stuff from Timing.cs. --- .../ViewModels/AnimationsPageViewModel.cs | 6 +- src/Avalonia.Animation/Animation.cs | 5 +- src/Avalonia.Animation/AnimationsEngine`1.cs | 4 +- src/Avalonia.Animation/Animator`1.cs | 2 +- src/Avalonia.Animation/Timing.cs | 38 +----------- src/Avalonia.Animation/Transition`1.cs | 5 +- src/Avalonia.Animation/TransitionsEngine.cs | 58 +++++++++++++++++++ 7 files changed, 75 insertions(+), 43 deletions(-) create mode 100644 src/Avalonia.Animation/TransitionsEngine.cs diff --git a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs index c76d4db513..f724baf3c6 100644 --- a/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs +++ b/samples/RenderDemo/ViewModels/AnimationsPageViewModel.cs @@ -15,16 +15,16 @@ namespace RenderDemo.ViewModels void TogglePlayState() { - switch (Timing.GlobalPlayState) + switch (Animation.GlobalPlayState) { case PlayState.Run: PlayStateText = "Resume all animations"; - Timing.GlobalPlayState = PlayState.Pause; + Animation.GlobalPlayState = PlayState.Pause; break; case PlayState.Pause: PlayStateText = "Pause all animations"; - Timing.GlobalPlayState = PlayState.Run; + Animation.GlobalPlayState = PlayState.Run; break; } } diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index da2fc75c0b..204cc9d04d 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -21,6 +21,10 @@ namespace Avalonia.Animation /// public class Animation : AvaloniaList, IAnimation { + /// + /// Gets or sets the animation play state for all animations + /// + public static PlayState GlobalPlayState { get; set; } = PlayState.Run; public AvaloniaList _animators { get; set; } = new AvaloniaList(); @@ -73,7 +77,6 @@ namespace Avalonia.Animation /// public bool DelayBetweenIterations { get; set; } - private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> { ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ) diff --git a/src/Avalonia.Animation/AnimationsEngine`1.cs b/src/Avalonia.Animation/AnimationsEngine`1.cs index 169f0a7ae0..cccb3098c0 100644 --- a/src/Avalonia.Animation/AnimationsEngine`1.cs +++ b/src/Avalonia.Animation/AnimationsEngine`1.cs @@ -111,7 +111,7 @@ namespace Avalonia.Animation private void DoPlayStatesAndTime(TimeSpan systemTime) { - if (Timing.GlobalPlayState == PlayState.Stop || targetControl.PlayState == PlayState.Stop) + if (Animation.GlobalPlayState == PlayState.Stop || targetControl.PlayState == PlayState.Stop) DoComplete(); if (!previousClock.HasValue) @@ -121,7 +121,7 @@ namespace Avalonia.Animation } else { - if (Timing.GlobalPlayState == PlayState.Pause || targetControl.PlayState == PlayState.Pause) + if (Animation.GlobalPlayState == PlayState.Pause || targetControl.PlayState == PlayState.Pause) { previousClock = systemTime; return; diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index a8b5ce7a27..8079ac69b5 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -98,7 +98,7 @@ namespace Avalonia.Animation /// private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete) { - var stateMachine = new AnimatorStateMachine(animation, control, this, onComplete); + var stateMachine = new AnimationsEngine(animation, control, this, onComplete); Timing.AnimationsTimer .TakeWhile(_ => !stateMachine.unsubscribe) diff --git a/src/Avalonia.Animation/Timing.cs b/src/Avalonia.Animation/Timing.cs index 575cedc620..b8282c05d0 100644 --- a/src/Avalonia.Animation/Timing.cs +++ b/src/Avalonia.Animation/Timing.cs @@ -15,11 +15,6 @@ namespace Avalonia.Animation /// public static class Timing { - /// - /// Gets or sets the animation play state for all animations - /// - public static PlayState GlobalPlayState { get; set; } = PlayState.Run; - /// /// The number of frames per second. /// @@ -38,14 +33,13 @@ namespace Avalonia.Animation var globalTimer = Observable.Interval(FrameTick, AvaloniaScheduler.Instance); AnimationsTimer = globalTimer - .Select(_ => - { - return TimeSpan.FromMilliseconds(Environment.TickCount); - }) + .Select(_ => GetTickCount()) .Publish() .RefCount(); } + internal static TimeSpan GetTickCount() => TimeSpan.FromMilliseconds(Environment.TickCount); + /// /// Gets the animation timer. /// @@ -58,31 +52,5 @@ namespace Avalonia.Animation { get; } - - /// - /// Gets a timer that fires every frame for the specified duration with delay. - /// - /// - /// An observable that notifies the subscriber of the progress along the transition. - /// - /// - /// The parameter passed to the subscriber is the progress along the transition, with - /// 0 being the start and 1 being the end. The observable is guaranteed to fire 0 - /// immediately on subscribe and 1 at the end of the duration. - /// - public static IObservable GetTransitionsTimer(Animatable control, TimeSpan duration, TimeSpan delay = default(TimeSpan)) - { - // TODO: Fix this mess. - // var _duration = (duration.Ticks / FrameTick.Ticks); - // long? endTime = ((Stopwatch.GetTimestamp() - _tickStartTimeStamp) - // / TicksPerFrame) + _duration; - - // return AnimationsTimer - // .TakeWhile(x => x < endTime) - // .Select(x => (double)x / _duration) - // .StartWith(0.0) - // .Concat(Observable.Return(1.0)); - return Observable.Empty(); - } } } \ No newline at end of file diff --git a/src/Avalonia.Animation/Transition`1.cs b/src/Avalonia.Animation/Transition`1.cs index c097b930a5..346c328809 100644 --- a/src/Avalonia.Animation/Transition`1.cs +++ b/src/Avalonia.Animation/Transition`1.cs @@ -5,6 +5,7 @@ using Avalonia.Metadata; using System; using System.Reactive.Linq; using Avalonia.Animation.Easings; +using Avalonia.Animation.Utils; namespace Avalonia.Animation { @@ -51,8 +52,10 @@ namespace Avalonia.Animation /// public virtual IDisposable Apply(Animatable control, object oldValue, object newValue) { - var transition = DoTransition(Timing.GetTransitionsTimer(control, Duration, TimeSpan.Zero), (T)oldValue, (T)newValue); + var transition = DoTransition(new TransitionsEngine(Duration), (T)oldValue, (T)newValue); return control.Bind((AvaloniaProperty)Property, transition, Data.BindingPriority.Animation); } + + } } \ No newline at end of file diff --git a/src/Avalonia.Animation/TransitionsEngine.cs b/src/Avalonia.Animation/TransitionsEngine.cs new file mode 100644 index 0000000000..4d52b2bd48 --- /dev/null +++ b/src/Avalonia.Animation/TransitionsEngine.cs @@ -0,0 +1,58 @@ +// 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.Metadata; +using System; +using System.Reactive.Linq; +using Avalonia.Animation.Easings; +using Avalonia.Animation.Utils; + +namespace Avalonia.Animation +{ + public class TransitionsEngine : IObservable, IDisposable + { + private IObserver observer; + private IDisposable timerSubscription; + private readonly TimeSpan startTime; + private readonly TimeSpan duration; + + public TransitionsEngine(TimeSpan Duration) + { + startTime = Timing.GetTickCount(); + duration = Duration; + + timerSubscription = Timing + .AnimationsTimer + .Subscribe(t => TimerTick(t)); + } + + private void TimerTick(TimeSpan t) + { + var interpVal = (double)(t.Ticks - startTime.Ticks) / duration.Ticks; + + if (interpVal > 1d + || interpVal < 0d) + { + this.Dispose(); + return; + } + + observer?.OnNext(interpVal); + } + + public void Dispose() + { + timerSubscription?.Dispose(); + observer?.OnCompleted(); + } + + public IDisposable Subscribe(IObserver Observer) + { + if (Observer is null) + throw new InvalidProgramException("Can only set the subscription once."); + + observer = Observer; + return this; + } + } +} \ No newline at end of file From e7dc15239288ff4e466f585406765269e1cba9c4 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 6 Sep 2018 00:21:24 -0500 Subject: [PATCH 17/42] Fix case where timing lands on exactly a new keyframe. --- src/Avalonia.Animation/Animator`1.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 8079ac69b5..e674ac84e1 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -86,10 +86,18 @@ namespace Avalonia.Animation double t0 = firstCue.Key; double t1 = lastCue.Key; - var intraframeTime = (t - t0) / (t1 - t0); - var firstFrameData = (firstCue.Value.frame.GetTypedValue(), firstCue.Value.isNeutral); - var lastFrameData = (lastCue.Value.frame.GetTypedValue(), lastCue.Value.isNeutral); - return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData)); + if (t0 != t1) + { + var intraframeTime = (t - t0) / (t1 - t0); + var firstFrameData = (firstCue.Value.frame.GetTypedValue(), firstCue.Value.isNeutral); + var lastFrameData = (lastCue.Value.frame.GetTypedValue(), lastCue.Value.isNeutral); + return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData)); + } + else + { + var frameData = (firstCue.Value.frame.GetTypedValue(), firstCue.Value.isNeutral); + return (0.0, new KeyFramePair(frameData, frameData)); + } } From 5f4a38842c37693a783712ba2116efbdeeec7217 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 6 Sep 2018 13:26:17 +0800 Subject: [PATCH 18/42] Finally fixed the flickering heart bug! Simplify the Keyframe selection and removed all LINQ code. Use the Cue property on AnimatorKeyFrame as the time index and integrate the IsNeutral value as an internal property. --- src/Avalonia.Animation/AnimatorKeyFrame.cs | 1 + src/Avalonia.Animation/Animator`1.cs | 61 ++++++++++++++-------- src/Avalonia.Animation/Cue.cs | 5 +- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 0276c6fa92..0dc15da49c 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -29,6 +29,7 @@ namespace Avalonia.Animation Cue = cue; } + internal bool isNeutral; public Type AnimatorType { get; } public Cue Cue { get; } public AvaloniaProperty Property { get; private set; } diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 8079ac69b5..7631a11d9c 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation /// /// List of type-converted keyframes. /// - private readonly SortedList _convertedKeyframes = new SortedList(); + private readonly List _convertedKeyframes = new List(); private bool isVerfifiedAndConverted; @@ -58,41 +58,49 @@ namespace Avalonia.Animation /// The time parameter, relative to the total animation time protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double t) { - KeyValuePair firstCue, lastCue; + AnimatorKeyFrame 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(); + firstCue = _convertedKeyframes[0]; + lastCue = _convertedKeyframes[1]; } else if (DoubleUtils.AboutEqual(t, 1.0) || t > 1.0) { - firstCue = _convertedKeyframes.Skip(kvCount - 2).First(); - lastCue = _convertedKeyframes.Last(); + firstCue = _convertedKeyframes[_convertedKeyframes.Count - 2]; + lastCue = _convertedKeyframes[_convertedKeyframes.Count - 1]; } else { - firstCue = _convertedKeyframes.Last(j => j.Key <= t); - lastCue = _convertedKeyframes.First(j => j.Key >= t); + (double time, int index) maxval = (0.0d, 0); + for (int i = 0; i < _convertedKeyframes.Count; i++) + { + var comp = _convertedKeyframes[i].Cue.CueValue; + if (t >= comp) + { + maxval = (comp, i); + } + } + firstCue = _convertedKeyframes[maxval.index]; + lastCue = _convertedKeyframes[maxval.index + 1]; } } else { - firstCue = _convertedKeyframes.First(); - lastCue = _convertedKeyframes.Last(); + firstCue = _convertedKeyframes[0]; + lastCue = _convertedKeyframes[1]; } - double t0 = firstCue.Key; - double t1 = lastCue.Key; + double t0 = firstCue.Cue.CueValue; + double t1 = lastCue.Cue.CueValue; var intraframeTime = (t - t0) / (t1 - t0); - var firstFrameData = (firstCue.Value.frame.GetTypedValue(), firstCue.Value.isNeutral); - var lastFrameData = (lastCue.Value.frame.GetTypedValue(), lastCue.Value.isNeutral); + var firstFrameData = (firstCue.GetTypedValue(), firstCue.isNeutral); + var lastFrameData = (lastCue.GetTypedValue(), lastCue.isNeutral); return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData)); } - /// /// Runs the KeyFrames Animation. /// @@ -113,16 +121,25 @@ namespace Avalonia.Animation protected abstract T DoInterpolation(double time, T neutralValue); /// - /// Verifies and converts keyframe values according to this class's target type. + /// Verifies, converts and sorts keyframe values according to this class's target type. /// private void VerifyConvertKeyFrames() { foreach (AnimatorKeyFrame keyframe in this) { - _convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false)); + _convertedKeyframes.Add(keyframe); } AddNeutralKeyFramesIfNeeded(); + + var copy = _convertedKeyframes.ToList().OrderBy(p => p.Cue.CueValue); + _convertedKeyframes.Clear(); + + foreach (AnimatorKeyFrame keyframe in copy) + { + _convertedKeyframes.Add(keyframe); + } + isVerfifiedAndConverted = true; } @@ -133,13 +150,13 @@ namespace Avalonia.Animation hasStartKey = hasEndKey = false; // Check if there's start and end keyframes. - foreach (var converted in _convertedKeyframes.Keys) + foreach (var frame in _convertedKeyframes) { - if (DoubleUtils.AboutEqual(converted, 0.0)) + if (DoubleUtils.AboutEqual(frame.Cue.CueValue, 0.0)) { hasStartKey = true; } - else if (DoubleUtils.AboutEqual(converted, 1.0)) + else if (DoubleUtils.AboutEqual(frame.Cue.CueValue, 1.0)) { hasEndKey = true; } @@ -153,12 +170,12 @@ namespace Avalonia.Animation { if (!hasStartKey) { - _convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true)); + _convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(0.0d)) { Value = default(T), isNeutral = true }); } if (!hasEndKey) { - _convertedKeyframes.Add(1.0d, (new AnimatorKeyFrame { Value = default(T) }, true)); + _convertedKeyframes.Add(new AnimatorKeyFrame(null, new Cue(1.0d)) { Value = default(T), isNeutral = true }); } } } diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs index 5a95c108e3..de77475c2c 100644 --- a/src/Avalonia.Animation/Cue.cs +++ b/src/Avalonia.Animation/Cue.cs @@ -7,7 +7,7 @@ using System.Text; namespace Avalonia.Animation { /// - /// A Cue object for . + /// Determines the time index for a . /// [TypeConverter(typeof(CueTypeConverter))] public readonly struct Cue : IEquatable, IEquatable @@ -84,5 +84,4 @@ namespace Avalonia.Animation return Cue.Parse((string)value, culture); } } - -} +} \ No newline at end of file From 8a800cadfa1917d6a34bd023b3ee07ce7e2cbeac Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 6 Sep 2018 19:52:47 +0800 Subject: [PATCH 19/42] Fix nits & polish up some code --- src/Avalonia.Animation/Animatable.cs | 25 +-- src/Avalonia.Animation/Animation.cs | 2 - src/Avalonia.Animation/AnimationsEngine`1.cs | 208 +++++++++---------- src/Avalonia.Animation/Animator`1.cs | 11 +- src/Avalonia.Animation/TransitionsEngine.cs | 30 ++- 5 files changed, 123 insertions(+), 153 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 4176bf01e5..3e030bf765 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -14,15 +14,7 @@ namespace Avalonia.Animation /// Base class for control which can have property transitions. /// public class Animatable : AvaloniaObject - { - /// - /// Initializes this object. - /// - public Animatable() - { - Transitions = new Transitions(); - } - + { /// /// Defines the property. /// @@ -42,27 +34,25 @@ namespace Avalonia.Animation { get { return _playState; } set { SetAndRaise(PlayStateProperty, ref _playState, value); } - } - /// /// Defines the property. /// - public static readonly DirectProperty> TransitionsProperty = - AvaloniaProperty.RegisterDirect>( + public static readonly DirectProperty TransitionsProperty = + AvaloniaProperty.RegisterDirect( nameof(Transitions), o => o.Transitions, (o, v) => o.Transitions = v); - private IEnumerable _transitions = new AvaloniaList(); + private Transitions _transitions; /// /// Gets or sets the property transitions for the control. /// - public IEnumerable Transitions + public Transitions Transitions { - get { return _transitions; } + get { return _transitions ?? (_transitions = new Transitions()); } set { SetAndRaise(TransitionsProperty, ref _transitions, value); } } @@ -83,6 +73,5 @@ namespace Avalonia.Animation } } } - } -} +} \ No newline at end of file diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index e23caf95f1..2c359ecac3 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -22,8 +22,6 @@ namespace Avalonia.Animation /// public static PlayState GlobalPlayState { get; set; } = PlayState.Run; - public AvaloniaList _animators { get; set; } = new AvaloniaList(); - /// /// Gets or sets the active time of this animation. /// diff --git a/src/Avalonia.Animation/AnimationsEngine`1.cs b/src/Avalonia.Animation/AnimationsEngine`1.cs index cccb3098c0..156056d765 100644 --- a/src/Avalonia.Animation/AnimationsEngine`1.cs +++ b/src/Avalonia.Animation/AnimationsEngine`1.cs @@ -1,7 +1,9 @@ using System; using System.Linq; +using System.Reactive.Linq; using Avalonia.Animation.Utils; using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia.Animation { @@ -9,182 +11,184 @@ namespace Avalonia.Animation /// Handles interpolatoin and time-related functions /// for keyframe animations. /// - internal class AnimationsEngine : IObservable, IDisposable + internal class AnimationsEngine : SingleSubscriberObservableBase { - T lastInterpValue; - T firstKFValue; - - private long repeatCount; - private double currentIteration; - - private bool isLooping; - private bool gotFirstKFValue; - private bool gotFirstFrameCount; - private bool iterationDelay; - - private FillMode fillMode; - private PlaybackDirection animationDirection; - private Animator parent; - private Animatable targetControl; - private T neutralValue; - private double speedRatio; - internal bool unsubscribe; - private bool isDisposed; - - private TimeSpan delay; - private TimeSpan duration; - private TimeSpan firstFrameCount; - private TimeSpan internalClock; - private TimeSpan? previousClock; - - private Easings.Easing easeFunc; - private IObserver targetObserver; - private readonly Action onCompleteAction; - - public AnimationsEngine(Animation animation, Animatable control, Animator animator, Action OnComplete) + private T _lastInterpValue; + private T _firstKFValue; + private long _repeatCount; + private double _currentIteration; + private bool _isLooping; + private bool _gotFirstKFValue; + private bool _gotFirstFrameCount; + private bool _iterationDelay; + private FillMode _fillMode; + private PlaybackDirection _animationDirection; + private Animator _parent; + private Animatable _targetControl; + private T _neutralValue; + private double _speedRatio; + private TimeSpan _delay; + private TimeSpan _duration; + private TimeSpan _firstFrameCount; + private TimeSpan _internalClock; + private TimeSpan? _previousClock; + private Easings.Easing _easeFunc; + private Action _onCompleteAction; + private Func _interpolator; + private IDisposable _timerSubscription; + + public AnimationsEngine(Animation animation, Animatable control, Animator animator, Action OnComplete, Func Interpolator) { if (animation.SpeedRatio <= 0 || DoubleUtils.AboutEqual(animation.SpeedRatio, 0)) throw new InvalidOperationException("Speed ratio cannot be negative or zero."); if (animation.Duration.TotalSeconds <= 0 || DoubleUtils.AboutEqual(animation.Duration.TotalSeconds, 0)) throw new InvalidOperationException("Duration cannot be negative or zero."); - - parent = animator; - easeFunc = animation.Easing; - targetControl = control; - neutralValue = (T)targetControl.GetValue(parent.Property); - speedRatio = animation.SpeedRatio; + _parent = animator; + _easeFunc = animation.Easing; + _targetControl = control; + _neutralValue = (T)_targetControl.GetValue(_parent.Property); + + _speedRatio = animation.SpeedRatio; - delay = animation.Delay; - duration = animation.Duration; - iterationDelay = animation.DelayBetweenIterations; + _delay = animation.Delay; + _duration = animation.Duration; + _iterationDelay = animation.DelayBetweenIterations; switch (animation.RepeatCount.RepeatType) { case RepeatType.None: - repeatCount = 1; + _repeatCount = 1; break; case RepeatType.Loop: - isLooping = true; + _isLooping = true; break; case RepeatType.Repeat: - repeatCount = (long)animation.RepeatCount.Value; + _repeatCount = (long)animation.RepeatCount.Value; break; } - animationDirection = animation.PlaybackDirection; - fillMode = animation.FillMode; - onCompleteAction = OnComplete; + _animationDirection = animation.PlaybackDirection; + _fillMode = animation.FillMode; + _onCompleteAction = OnComplete; + _interpolator = Interpolator; + } + + protected override void Unsubscribed() + { + _timerSubscription?.Dispose(); } - public void Step(TimeSpan frameTick, Func Interpolator) + protected override void Subscribed() + { + _timerSubscription = Timing.AnimationsTimer + .Subscribe(p => this.Step(p)); + } + + public void Step(TimeSpan frameTick) { try { - InternalStep(frameTick, Interpolator); + InternalStep(frameTick); } catch (Exception e) { - targetObserver?.OnError(e); + PublishError(e); } } private void DoComplete() { - if (fillMode == FillMode.Forward || fillMode == FillMode.Both) - targetControl.SetValue(parent.Property, lastInterpValue, BindingPriority.LocalValue); + if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both) + _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); - targetObserver.OnCompleted(); - onCompleteAction?.Invoke(); - Dispose(); + _onCompleteAction?.Invoke(); + PublishCompleted(); } private void DoDelay() { - if (fillMode == FillMode.Backward || fillMode == FillMode.Both) - if (currentIteration == 0) - targetObserver.OnNext(firstKFValue); + if (_fillMode == FillMode.Backward || _fillMode == FillMode.Both) + if (_currentIteration == 0) + PublishNext(_firstKFValue); else - targetObserver.OnNext(lastInterpValue); + PublishNext(_lastInterpValue); } private void DoPlayStatesAndTime(TimeSpan systemTime) { - if (Animation.GlobalPlayState == PlayState.Stop || targetControl.PlayState == PlayState.Stop) + if (Animation.GlobalPlayState == PlayState.Stop || _targetControl.PlayState == PlayState.Stop) DoComplete(); - if (!previousClock.HasValue) + if (!_previousClock.HasValue) { - previousClock = systemTime; - internalClock = TimeSpan.Zero; + _previousClock = systemTime; + _internalClock = TimeSpan.Zero; } else { - if (Animation.GlobalPlayState == PlayState.Pause || targetControl.PlayState == PlayState.Pause) + if (Animation.GlobalPlayState == PlayState.Pause || _targetControl.PlayState == PlayState.Pause) { - previousClock = systemTime; + _previousClock = systemTime; return; } - var delta = systemTime - previousClock; - internalClock += delta.Value; - previousClock = systemTime; + var delta = systemTime - _previousClock; + _internalClock += delta.Value; + _previousClock = systemTime; } - if (!gotFirstKFValue) + if (!_gotFirstKFValue) { - firstKFValue = (T)parent.First().Value; - gotFirstKFValue = true; + _firstKFValue = (T)_parent.First().Value; + _gotFirstKFValue = true; } - if (!gotFirstFrameCount) + if (!_gotFirstFrameCount) { - firstFrameCount = internalClock; - gotFirstFrameCount = true; + _firstFrameCount = _internalClock; + _gotFirstFrameCount = true; } } - private void InternalStep(TimeSpan systemTime, Func Interpolator) + private void InternalStep(TimeSpan systemTime) { DoPlayStatesAndTime(systemTime); - - if (isDisposed) - throw new InvalidProgramException("This KeyFrames Animation is already disposed."); - - var time = internalClock - firstFrameCount; - var delayEndpoint = delay; - var iterationEndpoint = delayEndpoint + duration; + + var time = _internalClock - _firstFrameCount; + var delayEndpoint = _delay; + var iterationEndpoint = delayEndpoint + _duration; //determine if time is currently in the first iteration. if (time >= TimeSpan.Zero & time <= iterationEndpoint) { - currentIteration = 1; + _currentIteration = 1; } else if (time > iterationEndpoint) { //Subtract first iteration to properly get the subsequent iteration time time -= iterationEndpoint; - if (!iterationDelay & delayEndpoint > TimeSpan.Zero) + if (!_iterationDelay & delayEndpoint > TimeSpan.Zero) { delayEndpoint = TimeSpan.Zero; - iterationEndpoint = duration; + iterationEndpoint = _duration; } //Calculate the current iteration number - currentIteration = (int)Math.Floor((double)time.Ticks / iterationEndpoint.Ticks) + 2; + _currentIteration = (int)Math.Floor((double)time.Ticks / iterationEndpoint.Ticks) + 2; } else { - previousClock = systemTime; + _previousClock = systemTime; return; } time = TimeSpan.FromTicks(time.Ticks % iterationEndpoint.Ticks); - if (!isLooping) + if (!_isLooping) { - if (currentIteration > repeatCount) + if (_currentIteration > _repeatCount) DoComplete(); if (time > iterationEndpoint) @@ -192,10 +196,10 @@ namespace Avalonia.Animation } // Determine if the current iteration should have its normalized time inverted. - bool isCurIterReverse = animationDirection == PlaybackDirection.Normal ? false : - animationDirection == PlaybackDirection.Alternate ? (currentIteration % 2 == 0) ? false : true : - animationDirection == PlaybackDirection.AlternateReverse ? (currentIteration % 2 == 0) ? true : false : - animationDirection == PlaybackDirection.Reverse ? true : false; + bool isCurIterReverse = _animationDirection == PlaybackDirection.Normal ? false : + _animationDirection == PlaybackDirection.Alternate ? (_currentIteration % 2 == 0) ? false : true : + _animationDirection == PlaybackDirection.AlternateReverse ? (_currentIteration % 2 == 0) ? true : false : + _animationDirection == PlaybackDirection.Reverse ? true : false; if (delayEndpoint > TimeSpan.Zero & time < delayEndpoint) { @@ -214,23 +218,11 @@ namespace Avalonia.Animation interpVal = 1 - interpVal; // Ease and interpolate - var easedTime = easeFunc.Ease(interpVal); - lastInterpValue = Interpolator(easedTime, neutralValue); + var easedTime = _easeFunc.Ease(interpVal); + _lastInterpValue = _interpolator(easedTime, _neutralValue); - targetObserver.OnNext(lastInterpValue); + PublishNext(_lastInterpValue); } } - - public IDisposable Subscribe(IObserver observer) - { - targetObserver = observer; - return this; - } - - public void Dispose() - { - unsubscribe = true; - isDisposed = true; - } } } \ No newline at end of file diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 051b1e50cf..77d761ce4c 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -32,12 +32,12 @@ namespace Avalonia.Animation } /// - public virtual IDisposable Apply(Animation animation, Animatable control, IObservable Match, Action onComplete) + public virtual IDisposable Apply(Animation animation, Animatable control, IObservable match, Action onComplete) { if (!_isVerifiedAndConverted) VerifyConvertKeyFrames(); - return Match + return match .Where(p => p) .Subscribe(_ => { @@ -103,12 +103,7 @@ namespace Avalonia.Animation /// private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete) { - var stateMachine = new AnimationsEngine(animation, control, this, onComplete); - - Timing.AnimationsTimer - .TakeWhile(_ => !stateMachine.unsubscribe) - .Subscribe(p => stateMachine.Step(p, DoInterpolation)); - + var stateMachine = new AnimationsEngine(animation, control, this, onComplete, DoInterpolation); return control.Bind((AvaloniaProperty)Property, stateMachine, BindingPriority.Animation); } diff --git a/src/Avalonia.Animation/TransitionsEngine.cs b/src/Avalonia.Animation/TransitionsEngine.cs index 4d52b2bd48..34863e4146 100644 --- a/src/Avalonia.Animation/TransitionsEngine.cs +++ b/src/Avalonia.Animation/TransitionsEngine.cs @@ -6,12 +6,15 @@ using System; using System.Reactive.Linq; using Avalonia.Animation.Easings; using Avalonia.Animation.Utils; +using Avalonia.Reactive; namespace Avalonia.Animation { - public class TransitionsEngine : IObservable, IDisposable + /// + /// Handles the timing and lifetime of a . + /// + public class TransitionsEngine : SingleSubscriberObservableBase { - private IObserver observer; private IDisposable timerSubscription; private readonly TimeSpan startTime; private readonly TimeSpan duration; @@ -20,10 +23,6 @@ namespace Avalonia.Animation { startTime = Timing.GetTickCount(); duration = Duration; - - timerSubscription = Timing - .AnimationsTimer - .Subscribe(t => TimerTick(t)); } private void TimerTick(TimeSpan t) @@ -33,26 +32,23 @@ namespace Avalonia.Animation if (interpVal > 1d || interpVal < 0d) { - this.Dispose(); + PublishCompleted(); return; } - observer?.OnNext(interpVal); + PublishNext(interpVal); } - - public void Dispose() + + protected override void Unsubscribed() { timerSubscription?.Dispose(); - observer?.OnCompleted(); } - public IDisposable Subscribe(IObserver Observer) + protected override void Subscribed() { - if (Observer is null) - throw new InvalidProgramException("Can only set the subscription once."); - - observer = Observer; - return this; + timerSubscription = Timing + .AnimationsTimer + .Subscribe(t => TimerTick(t)); } } } \ No newline at end of file From 45ca6af21cdd15031228f4c5bbbe8c9876f606ec Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 6 Sep 2018 21:29:45 +0800 Subject: [PATCH 20/42] Remove Epsilon equality checking since the animations dont require such precision. --- src/Avalonia.Animation/AnimationsEngine`1.cs | 4 ++-- src/Avalonia.Animation/Animator`1.cs | 8 ++++---- src/Avalonia.Animation/Utils/DoubleUtils.cs | 16 ---------------- src/Avalonia.Animation/Utils/EasingUtils.cs | 2 +- 4 files changed, 7 insertions(+), 23 deletions(-) delete mode 100644 src/Avalonia.Animation/Utils/DoubleUtils.cs diff --git a/src/Avalonia.Animation/AnimationsEngine`1.cs b/src/Avalonia.Animation/AnimationsEngine`1.cs index 156056d765..e724fe52d5 100644 --- a/src/Avalonia.Animation/AnimationsEngine`1.cs +++ b/src/Avalonia.Animation/AnimationsEngine`1.cs @@ -39,10 +39,10 @@ namespace Avalonia.Animation public AnimationsEngine(Animation animation, Animatable control, Animator animator, Action OnComplete, Func Interpolator) { - if (animation.SpeedRatio <= 0 || DoubleUtils.AboutEqual(animation.SpeedRatio, 0)) + if (animation.SpeedRatio <= 0) throw new InvalidOperationException("Speed ratio cannot be negative or zero."); - if (animation.Duration.TotalSeconds <= 0 || DoubleUtils.AboutEqual(animation.Duration.TotalSeconds, 0)) + if (animation.Duration.TotalSeconds <= 0) throw new InvalidOperationException("Duration cannot be negative or zero."); _parent = animator; diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 77d761ce4c..2c95571dc8 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -59,12 +59,12 @@ namespace Avalonia.Animation int kvCount = _convertedKeyframes.Count; if (kvCount > 2) { - if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0) + if (t <= 0.0) { firstCue = _convertedKeyframes[0]; lastCue = _convertedKeyframes[1]; } - else if (DoubleUtils.AboutEqual(t, 1.0) || t > 1.0) + else if (t >= 1.0) { firstCue = _convertedKeyframes[_convertedKeyframes.Count - 2]; lastCue = _convertedKeyframes[_convertedKeyframes.Count - 1]; @@ -143,11 +143,11 @@ namespace Avalonia.Animation // Check if there's start and end keyframes. foreach (var frame in _convertedKeyframes) { - if (DoubleUtils.AboutEqual(frame.Cue.CueValue, 0.0)) + if (frame.Cue.CueValue == 0.0d) { hasStartKey = true; } - else if (DoubleUtils.AboutEqual(frame.Cue.CueValue, 1.0)) + else if (frame.Cue.CueValue == 1.0d) { hasEndKey = true; } diff --git a/src/Avalonia.Animation/Utils/DoubleUtils.cs b/src/Avalonia.Animation/Utils/DoubleUtils.cs deleted file mode 100644 index d2e74376a3..0000000000 --- a/src/Avalonia.Animation/Utils/DoubleUtils.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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; - -namespace Avalonia.Animation.Utils -{ - internal static class DoubleUtils - { - internal static bool AboutEqual(double x, double y) - { - double epsilon = Math.Max(Math.Abs(x), Math.Abs(y)) * 1E-15; - return Math.Abs(x - y) <= epsilon; - } - } -} diff --git a/src/Avalonia.Animation/Utils/EasingUtils.cs b/src/Avalonia.Animation/Utils/EasingUtils.cs index d07ec3cdf4..1a7688cace 100644 --- a/src/Avalonia.Animation/Utils/EasingUtils.cs +++ b/src/Avalonia.Animation/Utils/EasingUtils.cs @@ -13,6 +13,6 @@ namespace Avalonia.Animation.Utils /// /// Half of /// - internal static double HALFPI = Math.PI / 2d; + internal const double HALFPI = Math.PI / 2d; } } From 06a4fe7312b7b7759902b855c90616797cae683a Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 6 Sep 2018 23:15:56 +0800 Subject: [PATCH 21/42] Minor fix on TransitionsEngine --- src/Avalonia.Animation/TransitionsEngine.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Animation/TransitionsEngine.cs b/src/Avalonia.Animation/TransitionsEngine.cs index 34863e4146..81b5b28820 100644 --- a/src/Avalonia.Animation/TransitionsEngine.cs +++ b/src/Avalonia.Animation/TransitionsEngine.cs @@ -16,12 +16,11 @@ namespace Avalonia.Animation public class TransitionsEngine : SingleSubscriberObservableBase { private IDisposable timerSubscription; - private readonly TimeSpan startTime; - private readonly TimeSpan duration; + private TimeSpan startTime; + private TimeSpan duration; public TransitionsEngine(TimeSpan Duration) { - startTime = Timing.GetTickCount(); duration = Duration; } @@ -46,6 +45,7 @@ namespace Avalonia.Animation protected override void Subscribed() { + startTime = Timing.GetTickCount(); timerSubscription = Timing .AnimationsTimer .Subscribe(t => TimerTick(t)); From 544d2e26c7268325fddbc58e55a912e17b60f213 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 6 Sep 2018 20:25:49 +0200 Subject: [PATCH 22/42] Fix NRE when VirtualizingStackPanel used with ItemsControl. There is no `ScrollViewer` in the template for `ItemsControl` which means that if a `VirtualizingStackPanel` is used for `ItemsControl.ItemsPanel` then `InvalidateScroll` won't be set. Just do nothing in this case. Fixes #1829. --- src/Avalonia.Controls/Presenters/ItemVirtualizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index c5344b29d9..46da8fe3f8 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -279,6 +279,6 @@ namespace Avalonia.Controls.Presenters /// /// Invalidates the current scroll. /// - protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll(); + protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll?.Invoke(); } } From a0d2d7f70b4cccf2984c317a48b66040f003521a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 7 Sep 2018 01:48:57 +0200 Subject: [PATCH 23/42] Added failing test for #1865. And similar test for Grid. --- .../DockPanelTests.cs | 24 +++++++++++++++++ .../Avalonia.Controls.UnitTests/GridTests.cs | 26 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs b/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs index 3de67839a7..59f047abae 100644 --- a/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs @@ -58,5 +58,29 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(50, 350, 500, 50), target.Children[3].Bounds); Assert.Equal(new Rect(50, 50, 500, 300), target.Children[4].Bounds); } + + [Fact] + public void Changing_Child_Dock_Invalidates_Measure() + { + Border child; + var target = new DockPanel + { + Children = + { + (child = new Border + { + [DockPanel.DockProperty] = Dock.Left, + }), + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + Assert.True(target.IsMeasureValid); + + DockPanel.SetDock(child, Dock.Right); + + Assert.False(target.IsMeasureValid); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 4c79b7775b..5799cb91c4 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -154,5 +154,31 @@ namespace Avalonia.Controls.UnitTests GridAssert.ChildrenHeight(rowGrid, 200, 300, 300); GridAssert.ChildrenWidth(columnGrid, 200, 300, 300); } + + [Fact] + public void Changing_Child_Column_Invalidates_Measure() + { + Border child; + var target = new Grid + { + ColumnDefinitions = new ColumnDefinitions("*,*"), + Children = + { + (child = new Border + { + [Grid.ColumnProperty] = 0, + }), + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + Assert.True(target.IsMeasureValid); + + Grid.SetColumn(child, 1); + + Assert.False(target.IsMeasureValid); + } + } } From a6a80c205ce351cfa8d61905b4b69cec050a1e62 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 7 Sep 2018 01:50:30 +0200 Subject: [PATCH 24/42] Make attached panel properties invalidate parent layout. Fixes #1865. --- src/Avalonia.Controls/Canvas.cs | 26 +----------------- src/Avalonia.Controls/DockPanel.cs | 4 +-- src/Avalonia.Controls/Grid.cs | 5 ++++ src/Avalonia.Controls/Panel.cs | 42 ++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs index 5c9a97cb27..e16a0b074b 100644 --- a/src/Avalonia.Controls/Canvas.cs +++ b/src/Avalonia.Controls/Canvas.cs @@ -48,7 +48,7 @@ namespace Avalonia.Controls static Canvas() { ClipToBoundsProperty.OverrideDefaultValue(false); - AffectsCanvasArrange(LeftProperty, TopProperty, RightProperty, BottomProperty); + AffectsParentArrange(LeftProperty, TopProperty, RightProperty, BottomProperty); } /// @@ -207,29 +207,5 @@ namespace Avalonia.Controls return finalSize; } - - /// - /// Marks a property on a child as affecting the canvas' arrangement. - /// - /// The properties. - private static void AffectsCanvasArrange(params AvaloniaProperty[] properties) - { - foreach (var property in properties) - { - property.Changed.Subscribe(AffectsCanvasArrangeInvalidate); - } - } - - /// - /// Calls on the parent of the control whose - /// property changed, if that parent is a canvas. - /// - /// The event args. - private static void AffectsCanvasArrangeInvalidate(AvaloniaPropertyChangedEventArgs e) - { - var control = e.Sender as IControl; - var canvas = control?.VisualParent as Canvas; - canvas?.InvalidateArrange(); - } } } diff --git a/src/Avalonia.Controls/DockPanel.cs b/src/Avalonia.Controls/DockPanel.cs index 66e84c1110..e147fe1a52 100644 --- a/src/Avalonia.Controls/DockPanel.cs +++ b/src/Avalonia.Controls/DockPanel.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls /// static DockPanel() { - AffectsArrange(DockProperty); + AffectsParentMeasure(DockProperty); } /// @@ -173,4 +173,4 @@ namespace Avalonia.Controls return arrangeSize; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 5f194bdd71..1a07ccaf7e 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -48,6 +48,11 @@ namespace Avalonia.Controls private RowDefinitions _rowDefinitions; + static Grid() + { + AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); + } + /// /// Gets or sets the columns definitions for the grid. /// diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index a2cb013300..c0d211effb 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -72,6 +72,32 @@ namespace Avalonia.Controls base.Render(context); } + /// + /// Marks a property on a child as affecting the parent panel's arrangement. + /// + /// The properties. + protected static void AffectsParentArrange(params AvaloniaProperty[] properties) + where TPanel : class, IPanel + { + foreach (var property in properties) + { + property.Changed.Subscribe(AffectsParentArrangeInvalidate); + } + } + + /// + /// Marks a property on a child as affecting the parent panel's measurement. + /// + /// The properties. + protected static void AffectsParentMeasure(params AvaloniaProperty[] properties) + where TPanel : class, IPanel + { + foreach (var property in properties) + { + property.Changed.Subscribe(AffectsParentMeasureInvalidate); + } + } + /// /// Called when the collection changes. /// @@ -116,5 +142,21 @@ namespace Avalonia.Controls InvalidateMeasure(); } + + private static void AffectsParentArrangeInvalidate(AvaloniaPropertyChangedEventArgs e) + where TPanel : class, IPanel + { + var control = e.Sender as IControl; + var panel = control?.VisualParent as TPanel; + panel?.InvalidateArrange(); + } + + private static void AffectsParentMeasureInvalidate(AvaloniaPropertyChangedEventArgs e) + where TPanel : class, IPanel + { + var control = e.Sender as IControl; + var panel = control?.VisualParent as TPanel; + panel?.InvalidateMeasure(); + } } } From fad2e317ba413b0624de7600ebc1bdeaa02a3272 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 7 Sep 2018 02:13:57 +0200 Subject: [PATCH 25/42] Make AffectsMeasure/Arrange/Render typed. --- src/Avalonia.Controls/Border.cs | 4 ++-- src/Avalonia.Controls/Decorator.cs | 2 +- src/Avalonia.Controls/DrawingPresenter.cs | 6 ++--- src/Avalonia.Controls/Image.cs | 3 +-- .../Presenters/ContentPresenter.cs | 4 ++-- .../Presenters/ScrollContentPresenter.cs | 2 +- .../Presenters/TextPresenter.cs | 2 +- .../Primitives/AccessText.cs | 2 +- src/Avalonia.Controls/Primitives/Track.cs | 2 +- src/Avalonia.Controls/Shapes/Shape.cs | 5 ++--- src/Avalonia.Controls/StackPanel.cs | 4 ++-- src/Avalonia.Controls/TabControl.cs | 2 +- src/Avalonia.Controls/TextBlock.cs | 9 ++++---- src/Avalonia.Controls/TopLevel.cs | 2 +- src/Avalonia.Controls/WrapPanel.cs | 2 +- src/Avalonia.Layout/Layoutable.cs | 22 +++++++++++-------- src/Avalonia.Visuals/Visual.cs | 12 +++++----- 17 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs index 5f84421c64..8b2a45b090 100644 --- a/src/Avalonia.Controls/Border.cs +++ b/src/Avalonia.Controls/Border.cs @@ -43,8 +43,8 @@ namespace Avalonia.Controls /// static Border() { - AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); - AffectsMeasure(BorderThicknessProperty); + AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); + AffectsMeasure(BorderThicknessProperty); } /// diff --git a/src/Avalonia.Controls/Decorator.cs b/src/Avalonia.Controls/Decorator.cs index 389cf66d34..15651b918e 100644 --- a/src/Avalonia.Controls/Decorator.cs +++ b/src/Avalonia.Controls/Decorator.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls /// static Decorator() { - AffectsMeasure(ChildProperty, PaddingProperty); + AffectsMeasure(ChildProperty, PaddingProperty); ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); } diff --git a/src/Avalonia.Controls/DrawingPresenter.cs b/src/Avalonia.Controls/DrawingPresenter.cs index af3665fabc..34ce598218 100644 --- a/src/Avalonia.Controls/DrawingPresenter.cs +++ b/src/Avalonia.Controls/DrawingPresenter.cs @@ -8,8 +8,8 @@ namespace Avalonia.Controls { static DrawingPresenter() { - AffectsMeasure(DrawingProperty); - AffectsRender(DrawingProperty); + AffectsMeasure(DrawingProperty); + AffectsRender(DrawingProperty); } public static readonly StyledProperty DrawingProperty = @@ -56,4 +56,4 @@ namespace Avalonia.Controls } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index f146e3571c..802b700a07 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -25,8 +25,7 @@ namespace Avalonia.Controls static Image() { - AffectsRender(SourceProperty); - AffectsRender(StretchProperty); + AffectsRender(SourceProperty, StretchProperty); } /// diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 6badf91367..8d703cfc1c 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -90,8 +90,8 @@ namespace Avalonia.Controls.Presenters /// static ContentPresenter() { - AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); - AffectsMeasure(BorderThicknessProperty, PaddingProperty); + AffectsRender(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty); + AffectsMeasure(BorderThicknessProperty, PaddingProperty); ContentProperty.Changed.AddClassHandler(x => x.ContentChanged); ContentTemplateProperty.Changed.AddClassHandler(x => x.ContentChanged); TemplatedParentProperty.Changed.AddClassHandler(x => x.TemplatedParentChanged); diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 2ef7941b55..c05c1672f8 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -72,7 +72,7 @@ namespace Avalonia.Controls.Presenters { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); - AffectsArrange(OffsetProperty); + AffectsArrange(OffsetProperty); } /// diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index a30d9bfc48..f73a335de5 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -38,7 +38,7 @@ namespace Avalonia.Controls.Presenters static TextPresenter() { - AffectsRender(PasswordCharProperty); + AffectsRender(PasswordCharProperty); } public TextPresenter() diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs index 32a0efc440..5adc8d2448 100644 --- a/src/Avalonia.Controls/Primitives/AccessText.cs +++ b/src/Avalonia.Controls/Primitives/AccessText.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Primitives /// static AccessText() { - AffectsRender(ShowAccessKeyProperty); + AffectsRender(ShowAccessKeyProperty); } /// diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 648fe5f4b0..8ff3ced770 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Primitives ThumbProperty.Changed.AddClassHandler(x => x.ThumbChanged); IncreaseButtonProperty.Changed.AddClassHandler(x => x.ButtonChanged); DecreaseButtonProperty.Changed.AddClassHandler(x => x.ButtonChanged); - AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); + AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); } public double Minimum diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 604051ef28..f77c43acd0 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -30,11 +30,10 @@ namespace Avalonia.Controls.Shapes private Geometry _renderedGeometry; bool _calculateTransformOnArrange = false; - static Shape() { - AffectsMeasure(StretchProperty, StrokeThicknessProperty); - AffectsRender(FillProperty, StrokeProperty, StrokeDashArrayProperty); + AffectsMeasure(StretchProperty, StrokeThicknessProperty); + AffectsRender(FillProperty, StrokeProperty, StrokeDashArrayProperty); } public Geometry DefiningGeometry diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 645cdbd926..df0c113cc0 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -29,8 +29,8 @@ namespace Avalonia.Controls /// static StackPanel() { - AffectsMeasure(SpacingProperty); - AffectsMeasure(OrientationProperty); + AffectsMeasure(SpacingProperty); + AffectsMeasure(OrientationProperty); } /// diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index 70cf8b4e05..3aae256858 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/src/Avalonia.Controls/TabControl.cs @@ -44,7 +44,7 @@ namespace Avalonia.Controls { SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(false); - AffectsMeasure(TabStripPlacementProperty); + AffectsMeasure(TabStripPlacementProperty); } /// diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index e91d2e8fa7..541e55625a 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -99,10 +99,11 @@ namespace Avalonia.Controls static TextBlock() { ClipToBoundsProperty.OverrideDefaultValue(true); - AffectsRender(ForegroundProperty); - AffectsRender(FontWeightProperty); - AffectsRender(FontSizeProperty); - AffectsRender(FontStyleProperty); + AffectsRender( + ForegroundProperty, + FontWeightProperty, + FontSizeProperty, + FontStyleProperty); } /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 1161ded25f..5f8eac1fe3 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -59,7 +59,7 @@ namespace Avalonia.Controls /// static TopLevel() { - AffectsMeasure(ClientSizeProperty); + AffectsMeasure(ClientSizeProperty); } /// diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 8ee0636124..597734d400 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -30,7 +30,7 @@ namespace Avalonia.Controls /// static WrapPanel() { - AffectsMeasure(OrientationProperty); + AffectsMeasure(OrientationProperty); } /// diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 54bdbb5d48..b8b24e6d31 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -140,7 +140,7 @@ namespace Avalonia.Layout /// static Layoutable() { - AffectsMeasure( + AffectsMeasure( IsVisibleProperty, WidthProperty, HeightProperty, @@ -427,11 +427,12 @@ namespace Avalonia.Layout /// After a call to this method in a control's static constructor, any change to the /// property will cause to be called on the element. /// - protected static void AffectsMeasure(params AvaloniaProperty[] properties) + protected static void AffectsMeasure(params AvaloniaProperty[] properties) + where T : class, ILayoutable { foreach (var property in properties) { - property.Changed.Subscribe(AffectsMeasureInvalidate); + property.Changed.Subscribe(AffectsMeasureInvalidate); } } @@ -443,11 +444,12 @@ namespace Avalonia.Layout /// After a call to this method in a control's static constructor, any change to the /// property will cause to be called on the element. /// - protected static void AffectsArrange(params AvaloniaProperty[] properties) + protected static void AffectsArrange(params AvaloniaProperty[] properties) + where T : class, ILayoutable { foreach (var property in properties) { - property.Changed.Subscribe(AffectsArrangeInvalidate); + property.Changed.Subscribe(AffectsArrangeInvalidate); } } @@ -636,9 +638,10 @@ namespace Avalonia.Layout /// Calls on the control on which a property changed. /// /// The event args. - private static void AffectsMeasureInvalidate(AvaloniaPropertyChangedEventArgs e) + private static void AffectsMeasureInvalidate(AvaloniaPropertyChangedEventArgs e) + where T : class, ILayoutable { - ILayoutable control = e.Sender as ILayoutable; + var control = e.Sender as T; control?.InvalidateMeasure(); } @@ -646,9 +649,10 @@ namespace Avalonia.Layout /// Calls on the control on which a property changed. /// /// The event args. - private static void AffectsArrangeInvalidate(AvaloniaPropertyChangedEventArgs e) + private static void AffectsArrangeInvalidate(AvaloniaPropertyChangedEventArgs e) + where T : class, ILayoutable { - ILayoutable control = e.Sender as ILayoutable; + var control = e.Sender as T; control?.InvalidateArrange(); } diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index 81e1a93a6f..c2db20306e 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -100,7 +100,7 @@ namespace Avalonia /// static Visual() { - AffectsRender( + AffectsRender( BoundsProperty, ClipProperty, ClipToBoundsProperty, @@ -320,11 +320,12 @@ namespace Avalonia /// on the control which when changed should cause a redraw. This is similar to WPF's /// FrameworkPropertyMetadata.AffectsRender flag. /// - protected static void AffectsRender(params AvaloniaProperty[] properties) + protected static void AffectsRender(params AvaloniaProperty[] properties) + where T : class, IVisual { foreach (var property in properties) { - property.Changed.Subscribe(AffectsRenderInvalidate); + property.Changed.Subscribe(AffectsRenderInvalidate); } } @@ -416,9 +417,10 @@ namespace Avalonia /// Called when a property changes that should invalidate the visual. /// /// The event args. - private static void AffectsRenderInvalidate(AvaloniaPropertyChangedEventArgs e) + private static void AffectsRenderInvalidate(AvaloniaPropertyChangedEventArgs e) + where T : class, IVisual { - (e.Sender as Visual)?.InvalidateVisual(); + (e.Sender as T)?.InvalidateVisual(); } /// From 576cc915731892c2c6f55b928ed0588d5ecac889 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 7 Sep 2018 02:30:00 +0200 Subject: [PATCH 26/42] Make Pseudoclass method typed. --- src/Avalonia.Controls/Button.cs | 2 +- src/Avalonia.Controls/ButtonSpinner.cs | 6 +++--- src/Avalonia.Controls/ContentControl.cs | 4 ++-- src/Avalonia.Controls/Expander.cs | 10 +++++----- src/Avalonia.Controls/Primitives/ScrollBar.cs | 4 ++-- .../Primitives/ToggleButton.cs | 6 +++--- src/Avalonia.Controls/Primitives/Track.cs | 2 ++ src/Avalonia.Controls/ProgressBar.cs | 6 +++--- src/Avalonia.Controls/Slider.cs | 2 ++ src/Avalonia.Input/InputElement.cs | 6 +++--- src/Avalonia.Styling/StyledElement.cs | 20 +++++++++++-------- 11 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index fa69d72d67..24b2af7996 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -80,7 +80,7 @@ namespace Avalonia.Controls FocusableProperty.OverrideDefaultValue(typeof(Button), true); CommandProperty.Changed.Subscribe(CommandChanged); IsDefaultProperty.Changed.Subscribe(IsDefaultChanged); - PseudoClass(IsPressedProperty, ":pressed"); + PseudoClass