Blog

【Android】アニメーション付きBindingAdapterを作る

Jetpack Composeのβ版が公開され盛り上がりを見せていますが、まだまだAndroidでViewを作成する際はDataBindingが主流でしょう。

DataBindingはxml内でコードを参照することでアプリのデータとUIを同期することができ、MVVMのアーキテクチャでより威力を発揮します。

BindingAdapterを使うことで、独自のプロパティを作成することも可能です。

これらは非常に便利なツールですが、アニメーションを扱う上ことは若干苦手とします。

今回はアニメーション付きのBindingAdapterを作るときの注意点と、その解決方法について紹介をします。

BindingAdapterの作り方

アニメーションの話に入る前に、BindingAdapterの作り方についておさらいしておきましょう。

例えば、DataBindingVISIBLEINVISIBLEを切り替えるため、以下のようなBindingAdapterを用意することが多いと思います。

object ViewBindingAdapters {
    @JvmStatic
    @BindingAdapter("visibleInvisible")
    fun setVisibleInvisible(view: View, isVisible: Boolean?) {
        view.visibility = if (isVisible != false) {
            View.VISIBLE
        } else {
            View.INVISIBLE
        }
    }
}

ちなみに、BindingAdapterは拡張関数でも書くことが出来ます。

@BindingAdapter("visibleInvisible")
fun View.setVisibleInvisible(isVisible: Boolean?) {
    visibility = if (isVisible != false) {
        View.VISIBLE
    } else {
        View.INVISIBLE
    }
}

BindingAdapterは、以下のようにxmlから参照することが出来ます。

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:visibleInvisible="@{viewModel.isVisible}"
    ... />

例えば、以下のようにボタンを押すことでTextViewvisibilityが変更されるUIを実現可能です。

BindingAdapterでアニメーションさせる

では、表示/非表示にフェードイン、フェードアウトをつけましょう。

例えば、以下のようなBindingAdapterを作成し、xmlから参照します。

object ViewBindingAdapters {
    @JvmStatic
    @BindingAdapter("visibleFade")
    fun setVisibleWithFade(view: View, isVisible: Boolean?) {
        val duration = 500L
        view.animate().cancel()
        if (isVisible != false) {
            view.animate().alpha(1F)
                .setDuration(duration)
                .start()
        } else {
            view.animate().alpha(0F)
                .setDuration(duration)
                .start()
        }
    }
}
<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:visibleFade="@{viewModel.isVisible}"
    ... />

これを動かすと、以下のように0.5秒でフェードイン、フェードアウトし、期待するように動いてるように見えます。

しかし、画面回転をさせると1点だけ問題があります。

非表示のまま画面回転をさせると、画面回転時に一瞬表示され、フェードアウトしてしまいます。(android:configChanges等を使って、画面回転時にViewを再生成していない場合、このような問題は発生しません)

画面回転に対応させる

これに対応させるため、初回のレンダリング時はアニメーションさせないようにさせます。

Viewに状態を持たせるため、ここではtagを使います。

重複しないIDを作成するため、ids.xmlを作成し、以下のようにitemを追加します。

<!-- res/values/ids.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="databinding_initialized" type="id" />
</resources>

そして、このIDがtrueになっていなかった場合、アニメーションさせないことで、初回レンダリング時のアニメーションを避けます。

object ViewBindingAdapters {
    @JvmStatic
    @BindingAdapter("visibleFade")
    fun setVisibleWithFade(view: View, isVisible: Boolean?) {
        val isInitialized = view.getTag(R.id.databinding_initialized) as? Boolean

        // 初回レンダリングの場合、アニメーションさせない
        if (isInitialized != true) {
            view.alpha = if (isVisible != false) {
                1F
            } else {
                0F
            }
            view.setTag(R.id.databinding_initialized, true)
            return
        }

        val duration = 500L
        view.animate().cancel()
        if (isVisible != false) {
            view.animate().alpha(1F)
                .setDuration(duration)
                .start()
        } else {
            view.animate().alpha(0F)
                .setDuration(duration)
                .start()
        }
    }
}

これで画面回転をした際に、一瞬表示される問題も解決しました。

まとめ

今回はBindingAdapterを用いたアニメーションの付与方法について紹介しました。

今回紹介した方法を応用することで、より複雑なアニメーションも実現可能です。

BindingAdapterはメソッドのため、基本状態をもたせることは出来ませんが、Viewtagを使うことで、今回のように状態をもたせることが可能です。

一方で、tagは型安全でなく、またどこからでも参照/変更が可能なため、乱用するのはあまり良いアイディアではないでしょう。

気をつけつつ、参考にしてもらえれば幸いです。

人気の記事

kotlin coroutinesのFlow, SharedFlow, StateFlowを整理する

Jetpack ComposeとKotlin Coroutinesを連携させる

Layout Composableを使って複雑なレイアウトを組む【Jetpack Compose】

テスト用Dispatcherの使い分け【Kotlin Coroutines】

Flow.combineの内部実装がすごい話

Jetpack ComposeのRippleエフェクトを深堀り、カスタマイズも