Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lottie playback controls #89

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 102 additions & 17 deletions src/Avalonia.Labs.Lottie/Lottie.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ public class Lottie : Control
public static readonly StyledProperty<int> RepeatCountProperty =
AvaloniaProperty.Register<Lottie, int>(nameof(RepeatCount), Infinity);

/// <summary>
/// Defines the <see cref="AutoPlay"/> property.
/// </summary>
public static readonly StyledProperty<bool> AutoPlayProperty =
AvaloniaProperty.Register<Lottie, bool>(nameof(AutoPlay), true);

/// <summary>
/// Gets or sets the Lottie animation path.
/// </summary>
Expand Down Expand Up @@ -90,6 +96,25 @@ public int RepeatCount
set => SetValue(RepeatCountProperty, value);
}

/// <summary>
/// Gets or sets whether the animation should automatically play when loaded.
/// </summary>
public bool AutoPlay
{
get => GetValue(AutoPlayProperty);
set => SetValue(AutoPlayProperty, value);
}

/// <summary>
/// Event triggered when the animation completes.
/// </summary>
public event EventHandler? AnimationCompleted;

/// <summary>
/// Event triggered when the animation completes a repetition. The event argument is the repetition number.
/// </summary>
public event EventHandler<int>? AnimationCompletedRepetition;

/// <summary>
/// Initializes a new instance of the <see cref="Lottie"/> class.
/// </summary>
Expand Down Expand Up @@ -119,7 +144,7 @@ protected override void OnLoaded(RoutedEventArgs e)
{
return;
}

_customVisual = compositor.CreateCustomVisual(new LottieCompositionCustomVisualHandler());
ElementComposition.SetElementChildVisual(this, _customVisual);

Expand All @@ -138,11 +163,14 @@ protected override void OnLoaded(RoutedEventArgs e)
_customVisual.SendHandlerMessage(
new LottiePayload(
LottieCommand.Update,
_animation,
Stretch,
_animation,
Stretch,
StretchDirection));

Start();

if (AutoPlay)
{
Start();
}
}

protected override void OnUnloaded(RoutedEventArgs e)
Expand All @@ -167,9 +195,9 @@ private void OnLayoutUpdated(object? sender, EventArgs e)
_customVisual.Size = new Vector2((float)Bounds.Size.Width, (float)Bounds.Size.Height);
_customVisual.SendHandlerMessage(
new LottiePayload(
LottieCommand.Update,
_animation,
Stretch,
LottieCommand.Update,
_animation,
Stretch,
StretchDirection));
}

Expand Down Expand Up @@ -224,7 +252,11 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
{
_repeatCount = change.GetNewValue<int>();
Stop();
Start();

if (AutoPlay)
{
Start();
}
break;
}
}
Expand Down Expand Up @@ -253,8 +285,8 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang

private SkiaSharp.Skottie.Animation? Load(string path, Uri? baseUri)
{
var uri = path.StartsWith("/")
? new Uri(path, UriKind.Relative)
var uri = path.StartsWith("/")
? new Uri(path, UriKind.Relative)
: new Uri(path, UriKind.RelativeOrAbsolute);
if (uri.IsAbsoluteUri && uri.IsFile)
{
Expand Down Expand Up @@ -297,7 +329,10 @@ private void Load(string? path)
InvalidateArrange();
InvalidateMeasure();

Start();
if (AutoPlay)
{
Start();
}
}
catch (Exception e)
{
Expand All @@ -308,22 +343,72 @@ private void Load(string? path)
}
}

private void Start()
/// <summary>
/// Starts or resumes the animation.
/// </summary>
public void Start()
{
_customVisual?.SendHandlerMessage(
new LottiePayload(
LottieCommand.Start,
_animation,
Stretch,
StretchDirection,
_repeatCount));
Stretch,
StretchDirection,
_repeatCount,
OnAnimationCompleted: OnAnimationCompleted,
OnAnimationCompletedRepetition: OnAnimationCompletedRepetition));
}

private void Stop()
/// <summary>
/// Stops the animation.
/// </summary>
public void Stop()
{
_customVisual?.SendHandlerMessage(new LottiePayload(LottieCommand.Stop));
}

/// <summary>
/// Pauses the animation.
/// </summary>
public void Pause()
{
_customVisual?.SendHandlerMessage(new LottiePayload(LottieCommand.Pause));
}

/// <summary>
/// Resumes the animation if paused.
/// </summary>
public void Resume()
{
_customVisual?.SendHandlerMessage(new LottiePayload(LottieCommand.Resume));
}

/// <summary>
/// Seeks to a given frame in the animation.
/// </summary>
public void SeekToFrame(float frame)
{
_customVisual?.SendHandlerMessage(new LottiePayload(LottieCommand.Seek, SeekFrame: frame));
}

/// <summary>
/// Seeks to a given progress value between 0 and 1.
/// </summary>
public void SeekToProgress(float progress)
{
_customVisual?.SendHandlerMessage(new LottiePayload(LottieCommand.Seek, SeekProgress: progress));
}

private void OnAnimationCompleted()
{
AnimationCompleted?.Invoke(this, EventArgs.Empty);
}

private void OnAnimationCompletedRepetition(int repetition)
{
AnimationCompletedRepetition?.Invoke(this, repetition);
}

private void DisposeImpl()
{
_customVisual?.SendHandlerMessage(new LottiePayload(LottieCommand.Dispose));
Expand Down
5 changes: 4 additions & 1 deletion src/Avalonia.Labs.Lottie/LottieCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ internal enum LottieCommand
Start,
Stop,
Update,
Dispose
Dispose,
Pause,
Resume,
Seek
}
93 changes: 88 additions & 5 deletions src/Avalonia.Labs.Lottie/LottieCompositionCustomVisualHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ internal class LottieCompositionCustomVisualHandler : CompositionCustomVisualHan
private TimeSpan _primaryTimeElapsed, _animationElapsed;
private TimeSpan? _lastServerTime;
private bool _running;
private bool _paused;
private SkiaSharp.Skottie.Animation? _animation;
private Stretch? _stretch;
private StretchDirection? _stretchDirection;
private SkiaSharp.SceneGraph.InvalidationController? _ic;
private readonly object _sync = new();
private int _repeatCount;
private int _count;
private Action? _onAnimationCompleted;
private Action<int>? _onAnimationCompletedRepetition;

public override void OnMessage(object message)
{
Expand All @@ -39,17 +42,52 @@ public override void OnMessage(object message)
}:
{
_running = true;
_paused = false;
_lastServerTime = null;
_stretch = st;
_stretchDirection = sd;
_animation = an;
_repeatCount = rp;
_count = 0;
_animationElapsed = TimeSpan.Zero;
_onAnimationCompleted = msg.OnAnimationCompleted;
_onAnimationCompletedRepetition = msg.OnAnimationCompletedRepetition;
RegisterForNextAnimationFrameUpdate();
break;
}
case
{
LottieCommand: LottieCommand.Pause
}:
{
_paused = true;
break;
}
case
{
LottieCommand: LottieCommand.Resume
}:
{
_paused = false;
RegisterForNextAnimationFrameUpdate();
break;
}
case
{
LottieCommand: LottieCommand.Seek
}:
{
if (msg.SeekFrame.HasValue)
{
SeekToFrame(msg.SeekFrame.Value);
}
else if (msg.SeekProgress.HasValue)
{
SeekToProgress(msg.SeekProgress.Value);
}
break;
}
case
{
LottieCommand: LottieCommand.Update,
Stretch: { } st,
Expand Down Expand Up @@ -84,16 +122,48 @@ public override void OnMessage(object message)

public override void OnAnimationFrameUpdate()
{
if (!_running || _paused)
return;

if (_lastServerTime.HasValue)
{
var delta = CompositionNow - _lastServerTime.Value;
_primaryTimeElapsed += delta;
_animationElapsed += delta;
}

_lastServerTime = CompositionNow;

GetFrameTime(); // This will handle cycle completion and repetitions

if (!_running)
return; // Animation has completed all repetitions

Invalidate();
RegisterForNextAnimationFrameUpdate();
}

private void SeekToFrame(float frame)
{
if (_animation == null)
{
return;
}

_animationElapsed = TimeSpan.FromSeconds(frame / _animation.Fps);
Invalidate();
RegisterForNextAnimationFrameUpdate();
}

if (_repeatCount == 0 || (_repeatCount > 0 && _count >= _repeatCount))
private void SeekToProgress(float progress)
{
if (_animation == null)
{
_running = false;
_animationElapsed = TimeSpan.Zero;
return;
}

progress = Math.Min(Math.Max(progress, 0), 1);
_animationElapsed = TimeSpan.FromSeconds(_animation.Duration.TotalSeconds * progress);
Invalidate();
RegisterForNextAnimationFrameUpdate();
}
Expand Down Expand Up @@ -125,6 +195,19 @@ private double GetFrameTime()
_ic?.End();
_ic?.Begin();
_count++;

if (_repeatCount != Lottie.Infinity && _count >= _repeatCount)
{
// Animation has finished all repetitions
_running = false;
_onAnimationCompleted?.Invoke();
return _animation.Duration.TotalSeconds; // Return the last frame
}

// Animation cycle completed, but not finished all repetitions
_onAnimationCompletedRepetition?.Invoke(_count);

frameTime = 0; // Reset frame time for the next cycle
}

return frameTime;
Expand Down Expand Up @@ -183,8 +266,8 @@ public override void OnRender(ImmediateDrawingContext context)
_lastServerTime = CompositionNow;
}

if (_animation is not { } an
|| _stretch is not { } st
if (_animation is not { } an
|| _stretch is not { } st
|| _stretchDirection is not { } sd)
{
return;
Expand Down
7 changes: 6 additions & 1 deletion src/Avalonia.Labs.Lottie/LottiePayload.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Avalonia.Media;

namespace Avalonia.Labs.Lottie;
Expand All @@ -7,4 +8,8 @@ internal record struct LottiePayload(
SkiaSharp.Skottie.Animation? Animation = null,
Stretch? Stretch = null,
StretchDirection? StretchDirection = null,
int? RepeatCount = null);
int? RepeatCount = null,
float? SeekFrame = null,
float? SeekProgress = null,
Action? OnAnimationCompleted = null,
Action<int>? OnAnimationCompletedRepetition = null);