Conversation
|
/azp run |
There was a problem hiding this comment.
Pull request overview
This PR adds an AnimationSpeed property to SKLottieView to control Lottie animation playback speed, addressing issue #281. The implementation allows users to speed up, slow down, or pause animations by multiplying the frame delta time by a speed factor. The default speed is 1.0 (normal playback).
Changes:
- Added
AnimationSpeedbindable property with typedoubleand default value1.0 - Modified
Update()method to apply the speed multiplier to deltaTime before processing - Added comprehensive unit tests covering default value, speed variations (2x, 0.5x, 0), and dynamic speed changes
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs | Adds AnimationSpeed bindable property and applies speed multiplier in Update() method |
| tests/SkiaSharp.Extended.UI.Maui.Tests/Controls/Lottie/SKLottieViewTest.cs | Adds 5 test methods covering default value, speed variations, and dynamic changes |
source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
Outdated
Show resolved
Hide resolved
source/SkiaSharp.Extended.UI.Maui/Controls/Lottie/SKLottieView.shared.cs
Outdated
Show resolved
Hide resolved
tests/SkiaSharp.Extended.UI.Maui.Tests/Controls/Lottie/SKLottieViewTest.cs
Show resolved
Hide resolved
tests/SkiaSharp.Extended.UI.Maui.Tests/Controls/Lottie/SKLottieViewTest.cs
Show resolved
Hide resolved
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
Co-authored-by: mattleibow <1096616+mattleibow@users.noreply.github.com>
- Add XML documentation explaining AnimationSpeed behavior including negative values - Rename playForwards to isInForwardPhase for clarity - Add overflow protection for extreme speed values - Add tests for negative AnimationSpeed (reverse playback) - Add test for AnimationSpeed with RepeatMode.Reverse - Add test for dynamically changing AnimationSpeed
4128657 to
2d0e369
Compare
Fixes identified by 5-model code review: - Fix overflow protection for NaN, Infinity, and edge cases - Fix negative AnimationSpeed not triggering completion or repeats - Fix restart position for negative speed (restarts at Duration) Changes: - Use effective direction (movingForward) based on AnimationSpeed sign - Update boundary detection to work with negative speeds - Update restart logic to reset to correct position based on speed - Add 6 new tests for negative speed scenarios
Tests added based on 5-model code review: - AnimationCompletedEventFiresOnlyOnce - ManualProgressSetDoesNotIncrementRepeatCount - SwitchingRepeatModeFromReverseToRestartMidAnimation - ProgressOutOfBoundsIsAccepted - ChangingAnimationSpeedToNegativeMidPlayback - ZeroAnimationSpeedPausesAnimation All 257 tests passing (120 base + 137 MAUI)
Fix 1: Negative AnimationSpeed now correctly initializes Progress to Duration - Animation loads at correct starting position for reverse playback - No longer requires manual Progress = Duration workaround Fix 2: Source race condition fixed with CancellationTokenSource - Changing Source cancels any in-flight load - Prevents wrong animation from displaying on rapid source changes Tests added: - NegativeSpeedAnimationStartsAtDurationOnLoad - SourceChangeCancellsPreviousLoad - Updated NegativeAnimationSpeedFromStartClampsToZero -> NegativeAnimationSpeedStartsAtDuration All 259 tests passing (139 MAUI + 120 base)
Fixes from 5-model code review: - Fix Reset() order causing spurious AnimationCompleted on load (isResetting guard) - Fix CancellationTokenSource memory leak (dispose before recreating) - Fix Reverse mode with negative speed (proper flip logic) Demo app enhancements: - Add AnimationSpeed slider (-2x to 2x) with preset buttons - Add RepeatMode picker (Restart/Reverse) - Add RepeatCount input field
|
/azp run |
| IsPlaying = !IsPlaying; | ||
|
|
||
| private void OnSetSpeed(string speed) => | ||
| AnimationSpeed = double.Parse(speed); |
There was a problem hiding this comment.
Using double.Parse without culture or error handling could throw FormatException if the command parameter is malformed. Consider using double.TryParse with InvariantCulture, or add try-catch error handling to prevent the app from crashing if an invalid value is passed.
| // (Duration differs between trophy.json and lolo.json) | ||
| Assert.NotEqual(TimeSpan.Zero, lottie.Duration); | ||
| } | ||
| } |
There was a problem hiding this comment.
The PR description mentions adding tests for NaN and Infinity edge cases, but there are no tests covering AnimationSpeed set to double.NaN, double.PositiveInfinity, or double.NegativeInfinity. While the overflow protection code handles these cases (lines 140-141), tests should verify this behavior to ensure the protection works correctly and prevent regressions.
| const long SafeMax = long.MaxValue - 1; // Avoid overflow when casting from double | ||
| const long SafeMin = long.MinValue + 2; // Avoid overflow when negating TimeSpan | ||
| if (!double.IsFinite(scaledTicks)) | ||
| scaledTicks = double.IsNaN(scaledTicks) || scaledTicks < 0 ? SafeMin : SafeMax; |
There was a problem hiding this comment.
The logic condition "scaledTicks < 0" on line 141 is redundant when double.IsFinite already returned false. By the time this condition is evaluated, we already know !double.IsFinite(scaledTicks) is true, meaning the value is either NaN, PositiveInfinity, or NegativeInfinity. For NaN, the comparison "scaledTicks < 0" always returns false in C#. For infinities, NegativeInfinity < 0 is true and PositiveInfinity < 0 is false. The comment says "Handle NaN and Infinity explicitly" but the logic mixes them. Consider simplifying to: if (double.IsNaN(scaledTicks)) scaledTicks = 0; else if (double.IsNegativeInfinity(scaledTicks)) scaledTicks = SafeMin; else if (double.IsPositiveInfinity(scaledTicks)) scaledTicks = SafeMax;
| scaledTicks = double.IsNaN(scaledTicks) || scaledTicks < 0 ? SafeMin : SafeMax; | |
| { | |
| if (double.IsNaN(scaledTicks)) | |
| scaledTicks = 0; | |
| else if (double.IsNegativeInfinity(scaledTicks)) | |
| scaledTicks = SafeMin; | |
| else if (double.IsPositiveInfinity(scaledTicks)) | |
| scaledTicks = SafeMax; | |
| } |
| int repeatsCompleted = 0; | ||
| CancellationTokenSource? loadCancellation; | ||
| bool isResetting; | ||
|
|
There was a problem hiding this comment.
The CancellationTokenSource field 'loadCancellation' is never disposed when the control itself is disposed or finalized. This could lead to a memory leak if the control is created and destroyed repeatedly (e.g., in navigation scenarios). Consider implementing IDisposable on SKLottieView or overriding an appropriate cleanup method to dispose of loadCancellation when the control is no longer needed.
| ~SKLottieView() | |
| { | |
| DisposeLoadCancellation(); | |
| } | |
| private void DisposeLoadCancellation() | |
| { | |
| if (loadCancellation != null) | |
| { | |
| try | |
| { | |
| loadCancellation.Cancel(); | |
| } | |
| catch | |
| { | |
| // Ignore exceptions on cancel during finalization. | |
| } | |
| loadCancellation.Dispose(); | |
| loadCancellation = null; | |
| } | |
| } |
Summary
Adds an
AnimationSpeedproperty toSKLottieViewto control Lottie animation playback speed, with comprehensive fixes for edge cases discovered through multi-model code review.Features
Bug Fixes
isResettingguard to prevent spurious AnimationCompletedCode Quality
playForwards→isInForwardPhasefor clarityDemo App Enhancements
Tests
Added 18+ new tests covering:
Total: 140 MAUI tests passing
API Naming Research
speedanimationSpeedsetSpeed()AnimationSpeedFollows iOS convention for .NET clarity.