Jetpack Composeのβ版が公開され盛り上がりを見せていますが、まだまだAndroidでViewを作成する際はDataBindingが主流でしょう。
DataBinding
はxml内でコードを参照することでアプリのデータとUIを同期することができ、MVVMのアーキテクチャでより威力を発揮します。
BindingAdapterを使うことで、独自のプロパティを作成することも可能です。
これらは非常に便利なツールですが、アニメーションを扱う上ことは若干苦手とします。
今回はアニメーション付きのBindingAdapter
を作るときの注意点と、その解決方法について紹介をします。
アニメーションの話に入る前に、BindingAdapterの作り方についておさらいしておきましょう。
例えば、DataBinding
でVISIBLE
とINVISIBLE
を切り替えるため、以下のような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}"
... />
例えば、以下のようにボタンを押すことでTextView
のvisibility
が変更されるUIを実現可能です。
では、表示/非表示にフェードイン、フェードアウトをつけましょう。
例えば、以下のような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
はメソッドのため、基本状態をもたせることは出来ませんが、View
のtag
を使うことで、今回のように状態をもたせることが可能です。
一方で、tag
は型安全でなく、またどこからでも参照/変更が可能なため、乱用するのはあまり良いアイディアではないでしょう。
気をつけつつ、参考にしてもらえれば幸いです。