Skip to content

Fast, reliable Lottie file loader and player #556

@r2d2Proton

Description

@r2d2Proton

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();
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions