Lottie-Android - Steigerung der User Experience mit kleinem Aufwand

20. März 2018

Wie man mit dem Framework Lottie geniale Animationen mit nur wenigen Zeilen Code in iOS einfügt, kann man bereits in unserem Artikel von letzter Woche nachlesen. Da wir bei Jamit Labs nicht nur Applikationen für iOS, sondern ebenso für Android programmieren, erklären wir in diesem Artikel, wie unser Android Team mit seiner Hilfe die verrücktesten Ideen unserer Designer mit geringem Aufwand umsetzt.

Hinzufügen des Frameworks zum Projekt

Um Lottie einem Android Projekt hinzuzufügen, muss in der build.gradle folgende Zeile in die Dependencies aufgenommen werden:

...
dependencies {
    ...
    implementation 'com.airbnb.android:lottie:$latest'
    ...
}
...

Anschließend muss eine Synchronisation des Projekts durchgeführt werden, sodass die IDE das Framework herunterlädt und dem Projekt verfügbar macht.

Implementierung

Nachdem der Designer die Animation in Adobe AfterEffects erstellt und exportiert hat, kann diese dem Projekt hinzugefügt werden. Im Android Projekt müssen diese dem assets Ordner hinzugefügt werden, sodass einfach darauf zugegriffen werden kann.

Nun kann eine LottieAnimationView überall im Projekt eingebaut werden, um die in den assets gespeicherten Animationen abzuspielen. Dafür wird ins entsprechende Layout eine spezielle View eingebunden:

...
<com.airbnb.lottie.LottieAnimationView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:lottie_fileName="animation.json"
    app:lottie_autoPlay="true" />
...

Es muss lediglich das JSON-File angegeben und das autoPlay Flag gesetzt werden, dann startet die Animation, sobald sie vom Lottie Framework geparst und geladen wurde.

Abfolge von verschiedenen Animationen

So weit ist die Verwendung von Lottie also nur mit wenig Aufwand verbunden, will man allerdings eine Abfolge von einzelnen JSON-Files wie in folgendem Beispiel abspielen, muss man doch etwas mehr Aufwand investieren.

Wir bei Jamit Labs arbeiten in unseren Android Projekten mit Databinding und der MVVM-Architektur. In unseren benutzerdefinierten BindingAdapter haben wir das folgende Binding hinzugefügt:

...
@BindingAdapter(value = ["animation_file", "start_animation"])
fun setAnimationFileAndStart(lottieAnimationView: LottieAnimationView,
                             animationFile: String?,
                             startAnimation: Boolean?) {

    if ((lottieAnimationView.getTag(R.id.lottie_animation_file) as? String) != animationFile) {
        lottieAnimationView.setAnimation(animationFile)
        lottieAnimationView.setTag(R.id.lottie_animation_file, animationFile)
    }

    if (startAnimation) {
        lottieAnimationView.playAnimation()
    }
}
...

Wir verwenden Lotties eigene Attribute lottie_file und lottie_autoPlay nicht, sondern nutzen das obige Binding, um die Daten flexibel aus dem ViewModel laden zu können. Da beispielsweise bei jedem Orientation Change die Activity neu erstellt und somit auch jedes Binding erneut ausgeführt wird, wird die Überprüfung mit dem Tag der lottieAnimationView benötigt. Andernfalls würde die Animation jedes Mal neu geladen und entsprechend von vorne gestartet werden. Ist der Tag jedoch unterschiedlich, das heißt wir wollen das nächste JSON-File laden, so wird dies mit lottieAnimationView.setAnimation(animation_file) getan. Anschließend muss der Tag noch mittels lottieAnimationView.setTag(R.id.lottie_animation_file, animationFile) neu gesetzt werden, sodass obiges Verhalten gewährleistet werden kann.

Um die JSON-Files austauschen zu können, müssen wir nun noch auf die verschiedenen Events der LottieAnimationView reagieren. Dies wird mit folgendem Binding ermöglicht:

...

@BindingAdapter("animation_model")
fun bindAnimationModel(lottieAnimationView: LottieAnimationView, model: AnimationModel?) {
    if (model != null) {
        lottieAnimationView.addAnimatorListener(
                object : Animator.AnimatorListener {
                    override fun onAnimationRepeat(p0: Animator?) {
                    }

                    override fun onAnimationEnd(p0: Animator?) {
                        model.hasEnded.onNext(true)
                    }

                    override fun onAnimationCancel(p0: Animator?) {
                    }

                    override fun onAnimationStart(p0: Animator?) {
                    }
                })
    }
}
...

Um über Events wie das Beenden einer Animation benachrichtigt zu werden, nutzen wir ein AnimationModel, welches die oben beschriebenen Funktionen beinhaltet. Nun müssen wir der LottieAnimationView einen AnimationListener hinzufügen, welcher unser model über die einzelnen Events informiert.

Das AnimationModel sieht dabei wie folgt aus:

class AnimationModel {
    val hasEnded: BehaviorSubject<Boolean> = BehaviorSubject.create()
}

In unserem ViewModel müssen wir entsprechend reagieren können, sodass die JSON-Files ausgetauscht werden können.

...
var animationListener: Disposable? = null
...

fun onInit() {
    animationListener = animationModel.hasEnded.subscribe({
        // Replace current JSON-File
        animationFile.set("other_animation_file.json")
    })
}
...
override fun onFinish() {
    super.onFinish()
    animationListener?.dispose()
}
...

Hierzu benötigen wir einen animationListener welcher sich bei Initialisierung von dem ViewModel auf das hasEnded Event des AnimationModels registriert. Wird nun hasEnded über das Binding aufgerufen, kann das JSON-File ausgetauscht werden. Der animationListener muss schließlich in onFinish wieder disposed werden, sodass keine Leaks und Seiteneffekte auftreten können.

Zudem verändert sich die Verwendung der LottieAnimationView ein wenig, so dass aus obigem Layoutelement folgendes wird:

...
<com.airbnb.lottie.LottieAnimationView
    android:id="@+id/animation_enter"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:animation_file="@{viewModel.animationFile}"
    app:animation_model="@{viewModel.animationModel}"
    app:start_animation="@{true}" />
...

Es werden nun nicht mehr die Attribute von Lottie verwendet, sondern die die im BindingAdapter definiert wurden. Wir sehen auch, dass wir das JSON-File nun nicht mehr als String Ressource hier im Layout angeben, sondern über das ViewModel gehen, da sich dies zur Laufzeit verändern kann.

Anmerkungen

Um diese Bindings so verwenden zu können, wie sie oben beschrieben sind, muss Lottie in Version ab 2.5.0-RC2 dem Projekt hinzugefügt werden.

Weiterhin ist zu beachten, dass bei größeren JSON-Files die Ladezeit der LottieAnimationView nicht zu vernachlässigen ist. Gerade der Wechsel zwischen den JSON-Files kann so die User Experience stark einschränken. Um das zu umgehen, können Animationen vorgeladen werden. Wir verwenden dazu das folgende Binding, das über Reflection Lotties internen Cache füllt:

...

/**
 * This binding can be used to preload additional lottie files when they should be played in sequence
 * Do not include the animation that is played initially in the animationFiles list of this method!
 * This would cause the animation to be loaded twice
 */
@Suppress("UNCHECKED_CAST")
@BindingAdapter("preload_animations")
fun preloadLottieAnimationFiles(lottieAnimationView: LottieAnimationView,
                                animationFiles: Collection<String>) {

    animationFiles.forEach { animationName ->
        // currently the Lottie API does not provide a public method to preload files. This is a copy of what's
        // happening within setAnimation in LottieAnimationView, just without setting the actual composition
        // Since the cache field is private, make it accessible first using reflection
        LottieComposition.Factory.fromAssetFileName(lottieAnimationView.context, animationName) { composition ->
            val refCacheField = LottieAnimationView::class.java.getDeclaredField("ASSET_WEAK_REF_CACHE")
            refCacheField.isAccessible = true
            val refCacheMap = (refCacheField.get(null) as MutableMap<String, WeakReference<LottieComposition?>>)
            refCacheMap[animationName] = WeakReference(composition)
        }
    }
}
...

Um dieses Binding zu verwenden, ändert sich die Verwendung der LottieAnimationView erneut:

...
<com.airbnb.lottie.LottieAnimationView
    android:id="@+id/animation_enter"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:animation_file="@{viewModel.animationFile}"
    app:animation_model="@{viewModel.animationModel}"
    app:preload_animations="@{viewModel.preloadAnimationFiles}"
    app:start_animation="@{true}" />
...

Im ViewModel wird nun folgende Konstante hinzugefügt:

val preloadAnimationFiles = listOf("Name of further animation files")

Hier kann man alle aufeinanderfolgenden JSON-Files auflisten. Zu beachten ist, dass das erste JSON-File in dieser Liste nicht enthalten sein sollte, da ansonsten diese Animation zweimal deserialisiert würde.

Ausblick

Wie Lottie auf ihrer Github Seite schreibt:

"They say a picture is worth 1,000 words so here are 13,000",

können Animationen die User-Experience deutlich steigern. Mit den hier gezeigten Erkenntnissen sind wir nun gewappnet, um jegliche Animationen, die sich unsere Designer ausdenken, in kürzester Zeit umzusetzen und der App das gewisse Etwas zu verleihen.