Skip to content

Instantly share code, notes, and snippets.

@softlion
Created March 8, 2024 20:17
Show Gist options
  • Save softlion/fad8ab6a37809e169d4fcc6b81b0f57b to your computer and use it in GitHub Desktop.
Save softlion/fad8ab6a37809e169d4fcc6b81b0f57b to your computer and use it in GitHub Desktop.
SkottieView for Maui (replaces LottieXamarin)
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Reflection;
using SkiaSharp;
using SkiaSharp.Views.Maui;
using Animation = SkiaSharp.Skottie.Animation;
namespace Skottie;
/// <summary>
/// Display a lottie file
/// </summary>
/// <example>
/// &lt;skottie:SkottieView Animation="plexus.json" HeightRequest="200" AnimationScale="3" AnimationCenterOffset="-30,0" /&gt;
/// </example>
public class SkottieView : SkiaSharp.Views.Maui.Controls.SKCanvasView
{
Animation? animation;
Stopwatch sw = new ();
CancellationTokenSource cancelLoop = new();
private bool hasHandler;
public string Animation { get; set; } = null!;
public double AnimationScale { get; set; }
/// <summary>
/// After scale is applied
/// </summary>
public Point AnimationCenterOffset { get; set; }
public bool IsDebugging { get; set; }
private void OnAnimationChanged()
{
Stop(true);
if(string.IsNullOrWhiteSpace(Animation))
return;
using var animationStream = ResourceLoader.GetEmbeddedResourceStream(typeof(SkottieView).Assembly, Animation);
if (animationStream == null)
Console.WriteLine($"Can't find lottie animation resource '{Animation}'");
else if(SkiaSharp.Skottie.Animation.TryCreate(animationStream, out animation))
Start();
else
Console.WriteLine($"Can't load lottie animation referenced by resource '{Animation}'");
}
private void Stop(bool disposeAnimation = false)
{
cancelLoop.Cancel();
sw.Stop();
if(disposeAnimation)
{
animation?.Dispose();
animation = null;
}
}
private void Start()
{
if (animation != null && hasHandler && Parent != null && IsVisible)
{
animation.Seek(0, null);
sw.Restart();
var delayBetweenFrames = TimeSpan.FromSeconds(1.0 / animation.Fps);
cancelLoop.Cancel();
var c = cancelLoop = new ();
Task.Run(async () =>
{
while(!c.IsCancellationRequested)
{
await MainThread.InvokeOnMainThreadAsync(() =>
{
if (c.IsCancellationRequested)
return;
try
{
//mainthread required on iOS
if (IsVisible && Parent != null && hasHandler && animation != null) //re-test
InvalidateSurface();
}
catch (Exception e)
{
Console.WriteLine($"SkottieView crashed in update loop: {e.Message}");
}
});
try
{
await Task.Delay(delayBetweenFrames, c.Token).ConfigureAwait(false);
}
catch
{
//Ignore task cancelled
}
}
});
}
}
protected override void OnParentSet()
{
base.OnParentSet();
if (Parent == null)
Stop(true);
else
Start();
}
protected override void OnPropertyChanged(string? propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(IsVisible))
{
if(!IsVisible)
Stop();
else
Start();
}
}
protected override void OnHandlerChanging(HandlerChangingEventArgs args)
{
base.OnHandlerChanging(args);
hasHandler = args.NewHandler != null;
var hadHandler = args.OldHandler != null;
if (hasHandler && !hadHandler)
Start();
else if (!hasHandler && hadHandler)
Stop();
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
var anim = animation;
if(anim == null)
return;
if(sw.Elapsed > anim.Duration)
sw.Restart();
anim.SeekFrameTime(sw.Elapsed);
//If IsDebugging="true", display a red solid background
e.Surface.Canvas.Clear(IsDebugging ? SKColor.Parse("FF0000") : SKColors.Transparent);
var rect = e.Info.Rect;
if (AnimationScale > 1 || !AnimationCenterOffset.IsEmpty)
{
var center = new SKPointI((rect.Right+rect.Left)/2, (rect.Top+rect.Bottom)/2);
rect = new (
(int)Math.Round(center.X - rect.Width * AnimationScale / 2 + AnimationCenterOffset.X* AnimationScale),
(int)Math.Round(center.Y - rect.Height*AnimationScale/2 + AnimationCenterOffset.Y* AnimationScale),
(int)Math.Round(center.X + rect.Width*AnimationScale/2 + AnimationCenterOffset.X* AnimationScale),
(int)Math.Round(center.Y + rect.Height * AnimationScale/2 + AnimationCenterOffset.Y* AnimationScale)
);
//e.Surface.Canvas.Scale();
}
anim.Render(e.Surface.Canvas, rect);
}
#region resource loader
static class ResourceLoader
{
static readonly ConcurrentDictionary<string, string[]> CacheAssemblyResourceNames = new ();
/// <summary>
/// Attempts to find and return the given resource from within the specified assembly.
/// </summary>
/// <returns>The embedded resource stream.</returns>
/// <param name="assembly">Assembly.</param>
/// <param name="resourceFileName">Resource file name.</param>
public static Stream? GetEmbeddedResourceStream(Assembly assembly, string resourceFileName)
{
if (assembly?.FullName == null || string.IsNullOrWhiteSpace(resourceFileName))
return null;
if (!CacheAssemblyResourceNames.TryGetValue(assembly.FullName, out var resourceNames))
{
resourceNames = assembly.GetManifestResourceNames();
CacheAssemblyResourceNames[assembly.FullName] = resourceNames;
}
var resourcePaths = (from x in resourceNames
where x.EndsWith(resourceFileName, StringComparison.CurrentCultureIgnoreCase)
orderby Math.Abs(resourceFileName.Length - x.Length)
select x
).ToList();
if (resourcePaths.Count != 1)
return null;
return assembly.GetManifestResourceStream(resourcePaths.First());
}
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment