.NET for AndroidでLottieアニメーションを使えるようにしてみた(Native Library Interop)
はじめに
.NET for Android(※MAUI ではない)で書かれたアプリに Lottie アニメーションを実装することになりましたので、その実装手順になります。
素直に NuGet の Lottie ライブラリがあれば良かったのですが、nuget.org で検索してみても古くてメンテされてなさそうだったり、あっても MAUI 専用っぽかったりしてどうにも使えなさそうだったので(見落としているだけでしたらすみません)、ネイティブのライブラリを使った方が良さそうとなり、ネイティブライブラリの使い方を探していて見つかった Native Library Interop というアプローチ(元々は Slim Binding と呼ばれていたようです)が良さそうということで試してみました。
本記事の執筆にあたって作成したサンプルコードは以下になります。
Native Library Interop (Slim Binding)についてはこちらの記事に詳しく紹介されています。
その他にも以下を参考にしています。
Microsoft 公式ドキュメント:
公式ドキュメントで紹介されているプロジェクトテンプレート:
Lottie 本体(ネイティブライブラリ)の公式ドキュメント:
これらの参考記事やドキュメントでは、いわゆるクロスプラットフォーム フレームワークであるところの.NET MAUI
について書かれていますが、基本的な考え方はそれほど変わりません。
ソリューション作成
まずはソリューションと必要なディレクトリを作成していきます。
mkdir NativeLibraryInteropSample-Android
cd NativeLibraryInteropSample-Android
dotnet new sln
mkdir -p App NativeLibraryInterop/Binding NativeLibraryInterop/native
ディレクトリ構成としては以下のようになります。
.
├── App
├── NativeLibraryInterop
│ ├── Binding
│ └── native
└── NativeLibraryInteropSample-Android.sln
- NativeLibraryInterop/native
ネイティブの Lottie ライブラリをラップするWrapper ライブラリ
を作成するための Android Studio プロジェクト置き場 - NativeLibraryInterop/Binding
作成したWrapper ライブラリ
をバインドするための.NET Binding Library
プロジェクト置き場 - App
動作確認用の.NET for Android アプリ
のプロジェクト置き場
Wrapper ライブラリの作成
ネイティブの Lottie ライブラリをラップする、ネイティブの Wrapper ライブラリ
を作成していきます。
なお、参考記事や公式テンプレートでは、もう少し細かく設定ファイルが編集されていたり、テストモジュールを削除したりしていますが、ここでは最小限の手順に留めるためにそれらの作業は省略しています。(最終的にはこれらもやった方が良いと思います)
Android Studio プロジェクトの作成
Android Studio でnative
ディレクトリ内に新しいプロジェクトを作成します。
ライブラリだけのプロジェクトは作れないようなので、Empty Activity で作成。(これも後で動作確認に使います)
native
ディレクトリを指定。
Android Library モジュールの作成
プロジェクトを作った時に自動的に作成されるのはアプリモジュールなので、それとは別に Lottie ライブラリを実際にラップするためのライブラリモジュールを作成します。
New > Module を選択。
今回はモジュール名を wrapper
とします。
libs.versions.toml
に Lottie のバージョンを追記
現在の Android Studio だとデフォルトでバージョンカタログを使用していたため、それに合わせる形で設定。
[versions]
...
+ lottie = "6.5.2"
[libraries]
...
+ lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" }
Android Library モジュールの build.gradle.kts を編集
- dependencies {
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.material)
- testImplementation(libs.junit)
- androidTestImplementation(libs.androidx.junit)
- androidTestImplementation(libs.androidx.espresso.core)
- }
+ // Create configuration for copyDependencies
+ configurations {
+ create("copyDependencies")
+ }
+
+ dependencies {
+ implementation(libs.lottie)
+ "copyDependencies"(libs.lottie)
+ }
+
+ // Copy dependencies for binding library
+ project.afterEvaluate {
+ tasks.register<Copy>("copyDeps") {
+ from(configurations["copyDependencies"])
+ into("${layout.buildDirectory.get()}/outputs/deps")
+ }
+ tasks.named("preBuild") { finalizedBy("copyDeps") }
+ }
-
configurations
,project.afterEvaluate
の部分は新規追加、dependencies
の部分は元々あった依存関係を全て削除し、lottie への依存を追記しています。 -
project.afterEvaluate
の部分では、取り込んだサードパーティライブラリ(Lottie)の aar を、後に作成するアプリプロジェクトから参照出来る場所にコピーしているようです。
参考記事では${layout.buildDirectory.get()}
の部分が元々${buildDir}
だったのですが、非推奨になっていたため推奨と思われる書き方に直しています。
Lottie の Wrapper View を作成
Lottie ライブラリが提供するLottieAnimationView
を .NET 側に公開されないようにするため、これを FrameLayout で包んだカスタム View を作成します。
New > UiComponent > Custom View を選択。
ここでは名前を LottieWrapperView
とします。
class LottieWrapperView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
private val lottieView: LottieAnimationView
init {
View.inflate(getContext(), R.layout.lottie_wrapper_view, this)
lottieView = findViewById(R.id.lottie)
}
public fun setAnimation(rawRes: Int) {
lottieView.setAnimation(rawRes)
}
public fun setLoop(enabled: Boolean) {
lottieView.repeatCount = if (enabled) ValueAnimator.INFINITE else 0
}
public fun playAnimation() {
lottieView.playAnimation()
}
}
今回は単純にLottieAnimationView
の以下とほぼ等価のメソッドを定義しています。
- setAnimation
- loop(setRepeatCount)
- playAnimation
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
一度ネイティブのサンプルアプリで動作確認
プロジェクト作成時に自動的に作成された app
モジュールを使い、まずはネイティブの世界だけでライブラリの動作を確認しておきます。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<LottieWrapperView>(R.id.lottie_wrapper).apply {
setAnimation(R.raw.bullseye)
setLoop(true)
playAnimation()
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.wrapper.LottieWrapperView
android:id="@+id/lottie_wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>
- res/raw ディレクトリに Lottie JSON ファイル(bullseye.json)を入れています。ファイル自体はLottie 公式のサンプルから拝借しました。
.NET Binding Library プロジェクトの作成
上記で作成した Wrapper ライブラリ
をバインドするための .NET Binding Library
を作成していきます。
.NET プロジェクトの作成
# slnがあるディレクトリから開始
cd NativeLibraryInterop/Binding
dotnet new android-bindinglib
# slnへの追加
cd ../..
dotnet sln add NativeLibraryInterop/Binding/Binding.csproj
csproj 編集
作成したプロジェクトの csproj(Binding.csproj)に以下を追加します。
<!-- Reference to Android project -->
<ItemGroup>
<NLIGradleProjectReference Include="..\native">
<ModuleName>wrapper</ModuleName>
<!-- Metadata applicable to @(AndroidLibrary) will be used if set, otherwise the following defaults will be used:
<Bind>true</Bind>
<Pack>true</Pack>
-->
</NLIGradleProjectReference>
</ItemGroup>
<!-- Reference to NuGet for building bindings -->
<ItemGroup>
<PackageReference Include="CommunityToolkit.Maui.NativeLibraryInterop.BuildTasks" Version="0.0.1-pre1" PrivateAssets="all" />
<PackageReference Include="Xamarin.Kotlin.StdLib" Version="2.0.20" />
</ItemGroup>
-
<NLIGradleProjectReference>
のInclude
属性に先ほど作成した Android Studio プロジェクトのルートパスを指定します。 -
<NLIGradleProjectReference>
内の<ModuleName>
に Android Studio プロジェクトのライブラリモジュール名を指定します。 -
CommunityToolkit.Maui.NativeLibraryInterop.BuildTasks
を取り込んでおくことで、この Binding Library をビルドする際に<NLIGradleProjectReference>
で指定した Android Studio プロジェクトも一緒にビルドしてくれるようです。 - Wrapper ライブラリが Kotlin で書かれている場合、依存関係に
Xamarin.Kotlin.StdLib
が必要なようなので追加しています。
SupportedOSPlatformVersion(minSdkVersion)の統一
確認の時点では Android Studio プロジェクトのminSdk
のデフォルトが 24、.NET プロジェクトの<SupportedOSPlatformVersion>
のデフォルトが 21 だったため、とりあえず後者を 24 に合わせます。
- <SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
+ <SupportedOSPlatformVersion>24</SupportedOSPlatformVersion>
NuGet.config の追加
CommunityToolkit.Maui.NativeLibraryInterop.BuildTasks
をインストールするための NuGet.config も sln と同じ階層に追加します。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="maui-nativelibraryinterop" value="https://pkgs.dev.azure.com/xamarin/public/_packaging/maui-nativelibraryinterop/nuget/v3/index.json" />
</packageSources>
</configuration>
(必要に応じて)Metadata.xml の編集
今回は不要ですが、必要に応じて Metadata.xml でバインドの挙動を調整していきます。
サードパーティライブラリを直接バインドしてしまうと、大量の API をバインドしようとしてエラーが発生し、泣きながら Metadata.xml を編集することになったりするかと思うのですが、薄い Wrapper ライブラリを被せることで、これを回避することが出来ます。
動作確認用の .NET for Android サンプルアプリ作成
.NET プロジェクトの作成
# slnがあるディレクトリから開始
cd App
dotnet new android
# slnへの追加
cd ..
dotnet sln add App/App.csproj
依存関係の追加
Binding Library プロジェクトを追加
cd App
dotnet add reference ../NativeLibraryInterop/Binding/Binding.csproj
aar への参照の追加
アプリプロジェクトには、依存元となっているサードパーティ aar を含めないと実行時エラーになってしまうため、<AndroidLibrary>
で依存している aar へのパスを追加します。
今回は lottie-6.5.2.aar
(Lottie ライブラリ本体)とokio-1.17.6.jar
(Lottie が内部で依存している)を追加しています。
<ItemGroup>
<AndroidLibrary Include="..\NativeLibraryInterop\native\wrapper\bin\Release\net8.0-android\outputs\deps\lottie-6.5.2.aar">
<Bind>false</Bind>
<Visible>false</Visible>
</AndroidLibrary>
<AndroidLibrary Include="..\NativeLibraryInterop\native\wrapper\bin\Release\net8.0-android\outputs\deps\okio-1.17.6.jar">
<Bind>false</Bind>
<Visible>false</Visible>
</AndroidLibrary>
</ItemGroup>
Include
属性に書かれているパスは、Wrapper ライブラリの build.gradle.kts
に記載したパス(${layout.buildDirectory.get()}/outputs/deps
)です。
NuGet パッケージの追加
また、AppCompat にも依存していたのでパッケージを追加しておきます。(究極的にはこれも aar にした方が良いのかもしれませんが、流石にちょっと依存関係が複雑すぎるのと、これほどコアなライブラリだったら公式が Binding Library を提供し続けてくれると思いたいということで NuGet パッケージ で良いかなと...)
dotnet add package Xamarin.AndroidX.AppCompat
SupportedOSPlatformVersion(minSdkVersion)の統一
Binding Library と同様に、<SupportedOSPlatformVersion>
を 24 に合わせます。
- <SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
+ <SupportedOSPlatformVersion>24</SupportedOSPlatformVersion>
呼び出しコードの追加
using Com.Example.Wrapper;
namespace App;
[Activity(Label = "@string/app_name", MainLauncher = true)]
public class MainActivity : Activity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.activity_main);
var lottieView = FindViewById<LottieWrapperView>(Resource.Id.lottie_wrapper);
lottieView.SetAnimation(Resource.Raw.bullseye);
lottieView.SetLoop(true);
lottieView.PlayAnimation();
}
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.wrapper.LottieWrapperView
android:id="@+id/lottie_wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
- ネイティブのサンプルアプリ同様、Resources/raw ディレクトリに Lottie JSON ファイル(bullseye.json)を入れています。
この状態でビルドに成功すると、ネイティブのサンプルアプリと同じようにアニメーションが表示されるかと思います。
終わりに
「ネイティブライブラリをラップした Wrapper ライブラリを作成し、その aar を.NET 側でバインディングすること」自体は元から出来ましたが、
- ラッパー側で変更が生じた場合に、わざわざ Android Studio を立ち上げて手動で aar をビルドする
- ビルドした aar を手動で.NET プロジェクト側にコピーする
というあたりを .NET のビルド時に自動でやってくれるという感じでしょうか。
まさにこれらを自動でやってくれているであろう CommunityToolkit.Maui.NativeLibraryInterop.BuildTasks
がまだプレリリース版なのが気になるところですが、最悪これだけ外す事は簡単そうなので良しとしておきます。
あとこれは Native Library Interop に限らず Binding Library 全般に言える話ですが、<AndroidLibrary>
で実行時の aar の依存関係を自力で解消する必要がある点はちょっとハードル高いなというのが正直な感想ですね。(私が慣れてないのもあるとは思いますが、今回だと okio の aar を追加する必要があるのが分からなくてハマった...)
Discussion