This post is part of #MAUIUIJuly. It's a great initiative to share helpful content with the community.
What is Lottie?
Lottie is a library for Android, iOS, Web, and Windows that parses Adobe After Effects animations exported as json with Bodymovin and renders them natively on mobile and on the web!
For the first time, designers can create and ship beautiful animations without an engineer painstakingly recreating it by hand.
Why bind?
When I wrote my app which needed Lottie animations, I found LottieXamarin library, but that only works with Xamarin.Forms not with MAUI. I tried to update it, however I've run into multiple issues and at the same time I've realised only older Lottie versions can be binded, because from March of 2019, Lottie is a Swift library, which means it can't be binded easily. With the binding solution I'll show below you can bind the latest version.
The other option is Skottie which was released after I'd already done my Lottie binding. I think Skottie is a viable alternative, based on your project needs can you select the best option.
Getting Started
I have two great resources for binding a library:
To bind Lottie I chose slim binding approach, please check the video for the details.
This post would like to give a high level overview how view binding can be done, for the details please check the resources above and LottieMaui repo.
Let's start with Android
LottieProxy
LottieAnimationViewWrapper does what the name suggest. It wraps LottieAnimationView which means, it initializes, exposes the View and makes the caller able to set the animation. The class itself is in an android library.
In the video above there is a script which downloads all the dependencies. Here you can find it, so you can easily paste it in your gradle.
public class LottieAnimationViewWrapper { | |
private final LottieAnimationView lottieAnimationView; | |
private final LottieSdkCallback lottieSdkCallback; | |
private final Context _context; | |
public LottieAnimationViewWrapper(Context context, LottieSdkCallback sdkCallback){ | |
_context = context; | |
lottieAnimationView = new LottieAnimationView(context); | |
lottieAnimationView.setRepeatMode(LottieDrawable.RESTART); | |
lottieAnimationView.setRepeatCount(LottieDrawable.INFINITE); | |
lottieSdkCallback = sdkCallback; | |
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); | |
lottieAnimationView.setLayoutParams(lp); | |
lottieAnimationView.setFailureListener(result -> { | |
lottieSdkCallback.onFailure(result.getLocalizedMessage()); | |
}); | |
lottieAnimationView.addAnimatorListener(new Animator.AnimatorListener() { | |
@Override | |
public void onAnimationStart(Animator animation) { | |
System.out.println("Animation: "+"start"); | |
} | |
@Override | |
public void onAnimationEnd(Animator animation) { | |
System.out.println("Animation: "+"end"); | |
} | |
@Override | |
public void onAnimationCancel(Animator animation) { | |
System.out.println("Animation: "+"cancel"); | |
} | |
@Override | |
public void onAnimationRepeat(Animator animation) { | |
System.out.println("Animation: " + "repeat"); | |
} | |
}); | |
} | |
public View getView() { | |
return lottieAnimationView; | |
} | |
public void setAnimation(String animation){ | |
LottieCompositionFactory.fromAsset(_context, animation).addListener(result -> { | |
System.out.println("Loaded!!!!!!!!!!!!!! "+result.getBounds() + " " + result.toString()); | |
lottieAnimationView.setComposition(result); | |
lottieAnimationView.playAnimation(); | |
}).addFailureListener(result -> result.printStackTrace()); | |
} | |
} |
Lottie.Proxy.Binding
Nothing to see here, this is to connect the .net world with the android library. This project simplifies our job with not forcing us to bind 100% of any library, but only what we need (and already wrote in LottieProxy).
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<TargetFramework>net6.0-android</TargetFramework> | |
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion> | |
<Nullable>enable</Nullable> | |
<ImplicitUsings>enable</ImplicitUsings> | |
</PropertyGroup> | |
<ItemGroup> | |
<AndroidAarLibrary Include="Jar\mylibrary-release.aar" /> | |
</ItemGroup> | |
</Project> |
Then make it work on iOS
LottieProxyiOS
On iOS side to keep it clean, I've used Carthage to get Lottie. The wrapping is pretty much the same, the goal is to make sure we can set the animation and get the view.
import Foundation | |
import UIKit | |
import Lottie | |
@objc(LottieAnimationViewWrapper) | |
public class LottieAnimationViewWrapper: NSObject { | |
private let animationView = AnimationView() | |
@objc | |
public func initAnimation() { | |
animationView.loopMode = .loop | |
animationView.contentMode = .scaleAspectFit | |
} | |
@objc | |
public func setAnimation(animation: String) { | |
animationView.animation = Animation.named(animation) | |
animationView.play { (finished) in | |
/// Animation finished | |
} | |
} | |
@objc | |
public func getView() -> UIView { | |
return animationView; | |
} | |
} |
I've borrowed the build script from the Swift binding walkthrough.
LottieProxyiOS.Binding
After you run sharpie you'll get an interface close to this:
using Foundation; | |
using UIKit; | |
using ObjCRuntime; | |
namespace LottieProxyiOS.Binding | |
{ | |
// @interface LottieAnimationViewWrapper : NSObject | |
[BaseType(typeof(NSObject))] | |
interface LottieAnimationViewWrapper | |
{ | |
// -(void)initAnimation __attribute__((objc_method_family("none"))); | |
[Export("initAnimation")] | |
void InitAnimation(); | |
// -(void)setAnimationWithAnimation:(NSString * _Nonnull)animation; | |
[Export("setAnimationWithAnimation:")] | |
void SetAnimationWithAnimation(string animation); | |
// -(UIView * _Nonnull)getView __attribute__((warn_unused_result(""))); | |
[Export("getView")] | |
UIView View { get; } | |
} | |
} |
The good thing is you have 100% control over what will be binded, so if you run any issue, it's much easier to fix as you would do the binding against the whole Lottie framework.
Don't forget to include all the frameworks and you see absolute paths here because relative paths don't always work for me.
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<TargetFramework>net6.0-ios</TargetFramework> | |
<Nullable>enable</Nullable> | |
<ImplicitUsings>true</ImplicitUsings> | |
<IsBindingProject>true</IsBindingProject> | |
</PropertyGroup> | |
<ItemGroup> | |
<ObjcBindingApiDefinition Include="ApiDefinition.cs" /> | |
<ObjcBindingCoreSource Include="StructsAndEnums.cs" /> | |
</ItemGroup> | |
<ItemGroup> | |
<NativeReference Include="/Users/csabahuszar/Projects/LottieMaui/LottieProxyiOS/VendorFrameworks/swift-framework-proxy/LottieProxyiOS.framework"> | |
<Kind>Framework</Kind> | |
<Frameworks>Foundation UIKit</Frameworks> | |
</NativeReference> | |
</ItemGroup> | |
<ItemGroup> | |
<NativeReference Include="/Users/csabahuszar/Projects/LottieMaui/LottieProxyiOS/Carthage/Build/Lottie.xcframework"> | |
<Kind>Framework</Kind> | |
<Frameworks>Foundation UIKit</Frameworks> | |
</NativeReference> | |
</ItemGroup> | |
</Project> |
Common
Lottie.Maui
Okay, we have the Lottie binding, now what? Let's use one of the greatest addition of MAUI, handlers. You'll find in this post the steps for LottieMaui, and please find here the detailed description.
First step is to define our interface:
namespace Lottie.Maui; | |
public interface ILottieView: IView | |
{ | |
string Animation { get; set; } | |
bool Loops { get; } | |
bool IsPlaying { get; set; } | |
void PlaybackCompleted(); | |
} |
Then implement the interface:
namespace Lottie.Maui; | |
public class LottieView : View, ILottieView | |
{ | |
private string _animation; | |
public string Animation | |
{ | |
get => _animation; | |
set { | |
_animation = value; | |
} | |
} | |
public bool Loops => true; | |
private bool _isPlaying; | |
public bool IsPlaying { get => _isPlaying; set => _isPlaying = value; } | |
public void PlaybackCompleted() | |
{ | |
throw new NotImplementedException(); | |
} | |
} |
Then create the LottieHandler with mappers. This is a partial class, the most interesting things will be happening in the Platform folder. Basically, there will be connected the binded C# libraries with our cross-platform app. As you see below I've only implemented what I needed. I wanted to show an animation loop, so I only implemented those parts. Later, anything else can be easily added.
Don't forget to add the handler:
handlers.AddHandler(typeof(ILottieView), typeof(LottieHandler));
#if __IOS__ || MACCATALYST | |
using PlatformView = UIKit.UIView; | |
#elif ANDROID | |
using PlatformView = AndroidX.AppCompat.Widget.AppCompatImageView; | |
#endif | |
namespace Lottie.Maui; | |
public partial class LottieHandler | |
{ | |
public static readonly PropertyMapper<ILottieView, LottieHandler> Mapper = new(ViewMapper) | |
{ | |
[nameof(ILottieView.Animation)] = MapAnimation, | |
[nameof(ILottieView.Loops)] = MapLoops, | |
[nameof(ILottieView.IsPlaying)] = MapIsPlaying, | |
}; | |
public static readonly CommandMapper<ILottieView, LottieHandler> CommmandMapper = new() | |
{ | |
["Reset"] = MapResetAnimation, | |
}; | |
public LottieHandler() : base(Mapper, CommmandMapper) | |
{ | |
} | |
} |
Lottie.Maui iOS handler
namespace Lottie.Maui; | |
public partial class LottieHandler : ViewHandler<ILottieView, UIView> | |
{ | |
LottieAnimationViewWrapper platformView; | |
private static void MapResetAnimation(LottieHandler handler, ILottieView view, object args) | |
{ | |
//handler.PlatformView.Progress = 0; | |
} | |
private static void MapIsPlaying(LottieHandler handler, ILottieView view) | |
{ | |
/* | |
if (view.IsPlaying) | |
handler.PlatformView.PlayAnimation(); | |
else | |
handler.PlatformView.PauseAnimation(); | |
*/ | |
} | |
private static void MapLoops(LottieHandler handler, ILottieView view) | |
{ | |
// handler.PlatformView.Loop(view.Loops); | |
} | |
public static void MapAnimation(LottieHandler handler, ILottieView view) | |
{ | |
var name = view?.Animation.Split(".")[0]; | |
handler.UpdateAnimation(name); | |
} | |
void UpdateAnimation(string name) | |
{ | |
platformView.SetAnimationWithAnimation(name); | |
} | |
protected override UIView CreatePlatformView() | |
{ | |
platformView = new LottieAnimationViewWrapper(); | |
platformView.InitAnimation(); | |
return platformView.View; | |
} | |
} |
Lottie.Maui Android handler
namespace Lottie.Maui; | |
public partial class LottieHandler : ViewHandler<ILottieView, AndroidX.AppCompat.Widget.AppCompatImageView> | |
{ | |
LottieAnimationView platformView; | |
private static void MapResetAnimation(LottieHandler handler, ILottieView view, object args) | |
{ | |
//handler.PlatformView.Progress = 0; | |
} | |
private static void MapIsPlaying(LottieHandler handler, ILottieView view) | |
{ | |
/* | |
if (view.IsPlaying) | |
handler.PlatformView.PlayAnimation(); | |
else | |
handler.PlatformView.PauseAnimation(); | |
*/ | |
} | |
private static void MapLoops(LottieHandler handler, ILottieView view) | |
{ | |
// handler.PlatformView.Loop(view.Loops); | |
} | |
public static void MapAnimation(LottieHandler handler, ILottieView view) | |
{ | |
var name = view?.Animation; | |
handler.UpdateAnimation(name); | |
} | |
void UpdateAnimation(string name) | |
{ | |
platformView.SetAnimation(name); | |
} | |
protected override AndroidX.AppCompat.Widget.AppCompatImageView CreatePlatformView() | |
{ | |
var callback = new LottieSdkCallbackImpl(); | |
platformView = new LottieAnimationView(Context, callback); | |
return (AndroidX.AppCompat.Widget.AppCompatImageView)platformView.View; | |
} | |
} |
And last but not least, you can add LottieView to your app, and you'll have the looping animation:
new LottieView { | |
Animation="logo-loading.json", | |
HeightRequest = 150, | |
Margin = new Thickness(0,48,0,0) | |
} |
If you made it here, you're awesome! I hope this post is going to be helpful.
I mentioned above, I implemented all this for my app, which is a Microsoft To Do client for Apple Watch and Wear OS devices. You can download from the AppStore and from Play Store
You can find all the code here: https://github.com/Csaba8472/LottieMaui
Top comments (0)