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

Fast, reliable Lottie file loader and player #556

Open
r2d2Proton opened this issue Apr 13, 2024 · 2 comments
Open

Fast, reliable Lottie file loader and player #556

r2d2Proton opened this issue Apr 13, 2024 · 2 comments

Comments

@r2d2Proton
Copy link

r2d2Proton commented Apr 13, 2024

Would like to share a fast and (appears) reliable Lottie file loader and player. One that uses SkiaSharp.Skottie.Animation:

public class LottieData : MyData
{
    public string Filename;
    public SKPoint Pos = new SKPoint { };
    public float Scale = 1.0f;
    public int Width = 0;
    public int Height = 0;

    public SkiaSharp.Skottie.Animation? Animation;
    public bool PlayForwards = true;
    public int RepeatsCompleted = 0;

    protected TimeSpan duration = new TimeSpan();
    public TimeSpan Duration
    {
        get => duration;
        set => duration = value;
    }

    protected TimeSpan progress = new TimeSpan();
    public TimeSpan Progress
    {
        get => progress;
        set => progress = value;
    }

    protected bool isComplete = false;
    public bool IsComplete
    {
        get => isComplete;
        set => isComplete = value;
    }

    int repeatCount = 0;
    public int RepeatCount
    {
        get => repeatCount;
        set => repeatCount = value;
    }

    protected SKLottieRepeatMode repeatMode = SKLottieRepeatMode.Restart;
    public SKLottieRepeatMode RepeatMode
    {
        get => repeatMode;
        set => repeatMode = value;
    }

    protected MyEventArgs myArgs;
    public MyEventArgs MyArgs
    {
        get => myArgs;
        set => myArgs = value;
    }

    public event EventHandler? AnimationFailed;

    public event EventHandler? AnimationLoaded;

    public event EventHandler<MyEventArgs>? AnimationCompleted;
    public void OnAnimationCompleted(LottiePlayer lottiePlayer)
    {
        AnimationCompleted?.Invoke(lottiePlayer, myArgs);
    }
}

public class LottiePlayer
{
    public new LottieData MyData { get => (LottieData)base.MyData; set => base.MyData = value; }

    public LottiePlayer() : base(ShapeType.Lottie)
    {
        MyData = new LottieData();
        InitWidget();
    }

    public LottiePlayer(LottieData lottieData) : base(ShapeType.Lottie, lottieData)
    {
        InitWidget();
    }

    protected void InitWidget()
    {
        CreateLottie();
        //CreateLottieView();
    }
    
    protected void CreateLottie()
    {
        LottieData lottieData = MyData;
        if (lottieData != null)
        {
            if (!string.IsNullOrEmpty(lottieData.Filename))
            {
                string lottieStr = Utils.LoadStringResource(lottieData.Filename);
                if (!string.IsNullOrEmpty(lottieStr))
                {
                    //Stream stream = new MemoryStream(bytes);
                    var bytes = System.Text.Encoding.UTF8.GetBytes(lottieStr);
                    var data = SKData.CreateCopy(bytes);
                    if (SkiaSharp.Skottie.Animation.TryCreate(data.AsStream(), out lottieData.Animation))
                    {
                    }
                }
            }

            ResetAnimation();
        }
    }

    void ResetAnimation()
    {
        LottieData lottieData = MyData;
        if (lottieData != null)
        {
            lottieData.PlayForwards = true;
            lottieData.RepeatsCompleted = 0;

            lottieData.Progress = TimeSpan.Zero;
            lottieData.Duration = TimeSpan.Zero;

            if (lottieData.Animation != null)
                lottieData.Duration = lottieData.Animation.Duration;
        }
    }
    
    public virtual void Update(TimeSpan deltaTime)
    {
        LottieData lottieData = MyData;
        if (lottieData == null || !lottieData.IsEnabled) return;

        // TODO: handle case where a repeat or revers cases the progress
        //       to either wrap or start the next round
        if (!lottieData.PlayForwards)
            deltaTime = -deltaTime;

        var newProgress = lottieData.Progress + deltaTime;

        if (newProgress > lottieData.Duration)
            newProgress = lottieData.Duration;

        if (newProgress < TimeSpan.Zero)
            newProgress = TimeSpan.Zero;

        lottieData.Progress = newProgress;

        UpdateProgress(lottieData.Progress);
    }

    private void UpdateProgress(TimeSpan progress)
    {
        LottieData lottieData = MyData;
        if (lottieData == null || lottieData.Animation == null)
        {
            if (lottieData != null)
                lottieData.IsComplete = true;

            return;
        }

        lottieData.Animation.SeekFrameTime(progress.TotalSeconds);

        var repeatMode = lottieData.RepeatMode;
        var duration = lottieData.Duration;

        // have we reached the end of this run
        var atStart = !lottieData.PlayForwards && progress <= TimeSpan.Zero;
        var atEnd = lottieData.PlayForwards && progress >= duration;
        var isFinishedRun = repeatMode == SKLottieRepeatMode.Restart ? atEnd : atStart;

        // maybe the direction changed
        var needsFlip = (atEnd && repeatMode == SKLottieRepeatMode.Reverse) || (atStart && repeatMode == SKLottieRepeatMode.Restart);
        if (needsFlip)
        {
            // we need to reverse to finish the run
            lottieData.PlayForwards = !lottieData.PlayForwards;
            lottieData.IsComplete = false;
        }
        else
        {
            // make sure repeats are positive to make things easier
            var totalRepeatCount = lottieData.RepeatCount;
            if (totalRepeatCount < 0)
                totalRepeatCount = int.MaxValue;

            // infinite
            var infinite = totalRepeatCount == int.MaxValue;
            if (infinite)
                lottieData.RepeatsCompleted = 0;

            // if we are at the end and we are repeating, then repeat
            if (isFinishedRun && lottieData.RepeatsCompleted < totalRepeatCount)
            {
                if (!infinite)
                    lottieData.RepeatsCompleted++;

                isFinishedRun = false;

                if (repeatMode == SKLottieRepeatMode.Restart)
                    lottieData.Progress = TimeSpan.Zero;
                else if (repeatMode == SKLottieRepeatMode.Reverse)
                    lottieData.PlayForwards = !lottieData.PlayForwards;
            }

            lottieData.IsComplete = isFinishedRun && lottieData.RepeatsCompleted >= totalRepeatCount;

            if (lottieData.IsComplete)
                lottieData.OnAnimationCompleted(this);
        }
    }

    public override void Draw(SKCanvas canvas, RectF dirtyRect)
    {
        canvas.Save();

        base.Draw(canvas, dirtyRect);

        LottieData lottieData = MyData;
        if (lottieData != null && lottieData.IsVisible)
        {
            int width = lottieData.Width;
            int height = lottieData.Height;

            // set transforms
            canvas.Translate(lottieData.Pos);
            canvas.Scale(lottieData.Scale);

            SKRect rect = new SKRect(0, 0, width, height);

            if (lottieData.Animation != null)
            {
                lottieData.Animation.Render(canvas, rect);
            }
        }
        
        canvas.Restore();
    }
}
@r2d2Proton
Copy link
Author

r2d2Proton commented Apr 13, 2024

I have yet to see Lottie's fail on Windows with the above code.

When I try with SKLottieView some lotties do not render correctly or not at all. . . but that may be due to intermingled calls such as:

private void UpdateProgress(TimeSpan progress)
{
    :
    lottieData.Animation.SeekFrameTime(progress.TotalSeconds);
}

And would expect a pixel perfect overdraw of the entire animation wtih:

public override void Draw(SKCanvas canvas, RectF dirtyRect)
{
    :
            lottieData.Animation.Render(canvas, rect);
    :
}

@r2d2Proton
Copy link
Author

In an effort to test with SKLottieView, it was added to my LottieData class and created instead of the version above:

public class LottieData : MyData
{
    :
    public SKLottieView skLottieView = null;
    :
}

public class LottiePlayer
{
    :
    protected void InitWidget()
    {
        //CreateLottie();
        CreateLottieView();
    }

    public async Task CreateLottieView()
    {
        LottieData lottieData = MyData;
        if (lottieData != null)
        {
            if (!string.IsNullOrEmpty(lottieData.Filename))
            {
                //SKLottieImageSource skLottieImageSource = (SKLottieImageSource)SKLottieImageSource.FromFile(lottieData.Filename);
                string lottieStr = Utils.LoadStringResource(lottieData.Filename);
                if (!string.IsNullOrEmpty(lottieStr))
                {
                    //Stream stream = new MemoryStream(bytes);
                    var bytes = System.Text.Encoding.UTF8.GetBytes(lottieStr);
                    var data = SKData.CreateCopy(bytes);
                    SKLottieImageSource skLottieImageSource = (SKLottieImageSource)SKLottieImageSource.FromStream(data.AsStream());
                    await TaskDelay(100);

                    lottieData.skLottieView = new SKLottieView
                    {
                        Source = skLottieImageSource,
                        IsAnimationEnabled = true,
                        RepeatCount = -1,
                        RepeatMode = SKLottieRepeatMode.Reverse,
                        HeightRequest = lottieData.Height,
                        WidthRequest = lottieData.Width
                    };

                    lottieData.Animation = await lottieData.skLottieView.Source.LoadAnimationAsync();
                    await TaskDelay(100);
                }
            }

            ResetAnimation();
        }
    }

    protected async Task TaskDelay(int delay)
    {
        await Task.Delay(delay);
    }

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

No branches or pull requests

1 participant