Skip to content

Add AnimationSpeed property to SKLottieView#331

Open
Copilot wants to merge 8 commits intomainfrom
copilot/add-animation-speed-property
Open

Add AnimationSpeed property to SKLottieView#331
Copilot wants to merge 8 commits intomainfrom
copilot/add-animation-speed-property

Conversation

Copy link
Contributor

Copilot AI commented Feb 5, 2026

Summary

Adds an AnimationSpeed property to SKLottieView to control Lottie animation playback speed, with comprehensive fixes for edge cases discovered through multi-model code review.

Features

  • AnimationSpeed property - Control playback speed (default 1.0)
    • Values > 1.0 speed up animation
    • Values 0-1.0 slow down animation
    • Value 0 pauses animation
    • Negative values reverse playback direction

Bug Fixes

Fix Description
Negative speed on load Animation now initializes at Duration when speed < 0
Source race condition Added CancellationTokenSource to cancel in-flight loads
Memory leak CancellationTokenSource now properly disposed
Reset order Added isResetting guard to prevent spurious AnimationCompleted
Reverse + negative speed Fixed flip logic to consider effective direction
Reverse → Restart switch Fixed animation getting stuck at 0 when switching modes

Code Quality

  • Renamed playForwardsisInForwardPhase for clarity
  • Added XML documentation to AnimationSpeed property
  • Added overflow protection for NaN/Infinity/extreme speed values

Demo App Enhancements

  • AnimationSpeed slider (-2x to 2x)
  • Speed preset buttons (-1x, 0.5x, 1x, 2x)
  • RepeatMode picker (Restart/Reverse)
  • RepeatCount input field

Tests

Added 18+ new tests covering:

  • Positive/negative/zero speed scenarios
  • Speed changes mid-playback
  • RepeatMode interactions
  • Edge cases (NaN, Infinity, extreme values)
  • Mode switching scenarios

Total: 140 MAUI tests passing

API Naming Research

Library Property Name
lottie-android speed
lottie-ios animationSpeed
lottie-web setSpeed()
This PR AnimationSpeed

Follows iOS convention for .NET clarity.

Copilot AI changed the title [WIP] Add property to set animation speed in SKLottieView Add AnimationSpeed property to SKLottieView Feb 5, 2026
Copilot AI requested a review from mattleibow February 5, 2026 03:17
@mattleibow mattleibow marked this pull request as ready for review February 5, 2026 03:39
@mattleibow
Copy link
Collaborator

/azp run

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AnimationSpeed bindable property with type double and default value 1.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

Copilot AI and others added 4 commits February 6, 2026 01:24
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
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

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
@mattleibow mattleibow requested a review from Copilot February 6, 2026 01:13
@mattleibow
Copy link
Collaborator

/azp run

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

IsPlaying = !IsPlaying;

private void OnSetSpeed(string speed) =>
AnimationSpeed = double.Parse(speed);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
// (Duration differs between trophy.json and lolo.json)
Assert.NotEqual(TimeSpan.Zero, lottie.Duration);
}
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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;
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
int repeatsCompleted = 0;
CancellationTokenSource? loadCancellation;
bool isResetting;

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
~SKLottieView()
{
DisposeLoadCancellation();
}
private void DisposeLoadCancellation()
{
if (loadCancellation != null)
{
try
{
loadCancellation.Cancel();
}
catch
{
// Ignore exceptions on cancel during finalization.
}
loadCancellation.Dispose();
loadCancellation = null;
}
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants