Android: Lottie Animation を任意の位置とサイズにオーバーレイ(ViewOverlay)表示させる


2021/12/06:編集メモ:Lottie 3.4.0 では以下の方法でうまく動作していますが、 Lottie 3.6.0 ではうまく動作していないかもしれません。Lottie 3.6.0 以降ではこの記事に記載した方針と ViewGroupOverlay + View (foreground = LottieDrawable) の組み合わせでうまく動かせました。今後あらためて検証する時間が取れたら情報を整理し、 ViewGroupOverlay での対応を記載予定です。

TL;DR

  • Lottie Animation を ViewOverlay に表示することで、View Tree に影響を与えずにアニメーション表示させる
  • ViewOverlay は Android 4.3 以上で使えます

やりたいことと問題点

Button タップを起点として Lottie でリッチなアニメーションを表示させるとき、レイアウト上の Button のサイズよりも広い範囲にアニメーションを表示させたいことがあります。

たとえば、 Twitter Android アプリのお気に入り Button UI はタップするとハートマークよりも広い範囲にアニメーションが表示されています。

これを XML レイアウト上で表現すると以下のようなレイアウトとなりますが、この方法では Button の周囲の View と LottieAnimationView が被ることがあったり、見た目上の margin と XML での margin 指定がズレており、レイアウトの配置に苦労します。レイアウトの重なり順にも問題があるため、LottieAnimationView を最前面に移動させる工夫も必要になります。

このレイアウトで A も C もアニメーションを付けることになったら XML が大変なことになりそうです...

アニメーションを XML で表現したときの構成:

<LinearLayout...>
    <Button.../>
    <FrameLayout...
        android:layout_marginTop="-14dp"
        android:layout_marginStart="-10dp"
        android:layout_marginEnd="-10dp">
        <Button.../>
        <com.airbnb.lottie.LottieAnimationView.../>
    </FrameLayout>
    <Button..../>
</LinearLayout>

解決策: Lottie Animation を ViewOverlay へ表示する

Android 4.3 以上であれば、ViewOverlay を利用できます。

ViewOverlay は View の最前面に存在しているレイヤーで、任意の View か Drawable を ViewOverlay に表示することができます。
ViewOverlay は View ごとに存在します。ViewOverlay は描画のみに使われるため、タップイベントなどには反応しません。

Activity や Fragment 画面遷移時の Shared Element Transition も ViewOverlay により実現されています。

ViewOverlay の使い方

  • 任意の View に対して view.overlay でアクセスできる
  • オーバーレイ表示するタイミングで view.overlay.add(Drawable) または、 view.overlay.add(View) により、描画したい要素を追加する
    • Drawable の描画位置は drawable.bounds で指定する
  • オーバーレイ表示が不要になったら、view.overlay.remove() で要素を削除する

ViewOverlay + LottieDrawable 実装

後述の PositionedLottieDrawable を用いて、以下のように実装します。

// アニメーションの起点となる Button
private val button: ToggleButton = ...
// オーバーレイ表示したい View。 Button の Parent である場合が多い
private val targetView: ViewGroup = (button.parent as! ViewGroup)
private val drawable = PositionedLottieDrawable()
private val compositionTask: LottieTask<LottieComposition>
init {
    drawable.addAnimatorListener(object: Animator.AnimatorListener {
        override fun onAnimationStart(animation: Animator?) = Unit
        override fun onAnimationEnd(animation: Animator?) {
            // アニメーション終了で Drawable を Overlay から削除
            targetView.overlay.remove(drawable)
            // アニメーション終了後の状態 (Toggle ON/OFF) で表示
            button.alpha = 1f
        }
        override fun onAnimationCancel(animation: Animator?) = Unit
        override fun onAnimationRepeat(animation: Animator?) = Unit
    })
    compositionTask = LottieCompositionFactory
        .fromRawRes(context, R.raw.lottie_animation)
        .addListener {
            // 非同期で Lottie JSON を読み込み、drawable へ設定する
            drawable.composition = it
        }
}
fun clickButton(checked: Boolean) {
    if (checked) {
        // checked = true へ移行するアニメーション処理

        // より安全な実装とするには、ここで drawable.composition が読み込み済みであることを確認してください
        // 読み込みが完了していなければ compositionTask の終了を待ってからアニメーションを実行する必要があります

        // アニメーション中は Button を非表示
        // 表示したままでよければ alpha を変更する必要はない
        // アニメーション中にも Button タップ判定を拾いたいため、visibility ではなく alpha 変更
        button.alpha = 0f
        // アニメーション Drawable の座標を計算する
        // このサンプルでは Button とアニメーションが中央合わせとなるように計算している
        drawable.x = (button.x - (drawable.composition.bounds.width() - button.width) / 2)
        drawable.y = (button.y - (drawable.composition.bounds.height() - button.height) / 2)
        targetView.overlay.add(drawable)
        drawable.playAnimation()
    } else {
        // checked = false へ移行する実装は省略
        // button.alpha やアニメーションの処理を実装する
        // こちらにもアニメーションが必要なら、checked = true のアニメーションを停止してから
        // あたらしくアニメーションを開始したりする
    }
}

PositionedLottieDrawable ワークアラウンド

Lottie 3.6.1 時点で、LottieDrawable は bounds で指定した座標に描画してくれない問題があります。

以下の PositionedLottieDrawable により、描画座標を修正します。

class PositionedLottieDrawable : LottieDrawable() {
    var x: Float = 0f
    var y: Float = 0f
    override fun draw(canvas: Canvas) {
        canvas.save()
        canvas.translate(x, y)
        super.draw(canvas)
        canvas.restore()
    }
}

ViewOverlay + LottieAnimationView ではうまく動かない

Lottie 3.6.1 時点で、LottieAnimationView は ViewOverlay に追加しても正しくアニメーションしません。