Created
March 8, 2024 20:17
-
-
Save softlion/fad8ab6a37809e169d4fcc6b81b0f57b to your computer and use it in GitHub Desktop.
SkottieView for Maui (replaces LottieXamarin)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
/// <skottie:SkottieView Animation="plexus.json" HeightRequest="200" AnimationScale="3" AnimationCenterOffset="-30,0" /> | |
/// </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