日々是好日

プログラミングについてのあれこれ、ムダ知識など

HeaderView に Animator を DataBinding で実装

参考

medium.com

前回に引き続き、HeaderView と DataBinding でいろいろやった話。

やりたいこと

NavigationDrawer の項目をタップすると、HeaderView の背景色(ConstraintLayout.background要素)が徐々に変わるアニメーションの実装です。
イメージは次の感じ。

f:id:kcpoipoi:20190102170743g:plain
HeaderView

gif にするにはちょっと duration 短かったかも( ˘ω˘)

手順

MainViewModel の作成

View にバインドするデータ(headerColorTo)を ViewModel に定義します。

class MainViewModel(application: Application): AndroidViewModel(application){
    val headerColorTo = MutableLiveData<Int>()

    fun setRecipeType(enum: RecipeTypeEnum) {
        headerColorTo.value = 
            ContextCompat.getColor(getApplication(), enum.symbolColorId)
    }
}

この MainViewModel を HeaderView のレイアウトファイルにバインドしますが、その前に@BindingAdapterでカスタムセッターを定義します。

拡張関数でカスタムセッター定義

ConstraintLayout の親クラスである ViewGroup に、拡張関数と BindingAdapter アノテーションを使ってカスタムセッターを定義します。

メソッド名はsetBackgroundColorToとしていますが、コード上から呼ぶことはないので ViewGroup の既定のメソッドと被らなければなんでもいいです。

現在の背景色はthis.backgroundで取得します(ColorDrawable? 型)。このとき の返り値は null の可能性があるので、null チェックを行っています。

そしたら Animator オブジェクトを生成し、変化時間(duration)、UpdateListener 等を設定してstartを叩けば背景色が徐々に変化するアニメーションが出来上がります。

@BindingAdapter("bind:animate_background_color")
fun ViewGroup.setBackgroundColorTo(colorTo: Int){
    val backgroundDrawable = background

    //backgroundがnullの場合あり。
    //既定の色を colorFrom 変数に格納
    val colorFrom = if (backgroundDrawable != null){
        (background as ColorDrawable).color
    } else {
        //nullだった場合既定の色を設定
        ContextCompat.getColor(context, R.color.all_dish_color)
    }

    //色変化のAnimatorの生成
    val colorAnimator
         = ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo).also { 
        it.setTarget(this)
        it.duration = 500
        it.addUpdateListener { animator ->
            val animatedValue = animator.animatedValue as Int
            setBackgroundColor(
                Color.argb(Color.alpha(animatedValue),
                Color.red(animatedValue),
                Color.green(animatedValue),
                Color.blue(animatedValue)))
        }
    }
    colorAnimator.start()
}

さて、ここで作成したbind:animate_background_colorMainViewModel.headerColorToをバインドします。

HeaderView レイアウトの定義

真ん中のアイコン用の ImageView 一個だけです。

<layout xmlns:bind="http://schemas.android.com/apk/res-auto">
    <data>
        <variable name="viewmodel" type="...MainViewModel" />
    </data>

    <ConstraintLayout
        android:id="@+id/navigation_header_frame"
        bind:animate_background_color="@{viewmodel.headerColorTo}"
    ...>

        <ImageView
            android:id="@+id/header_image" .../>

    </ConstraintLayout>
</layout>

ViewGroup を拡張してbind:animate_background_colorを定義したので、ViewGroup を継承した ConstraintLayout にもカスタムセッターが出てきます。

これで、NavigationDrawer の項目をタップしたイベントでMainViewModel#setRecipeTypeをコールするようにすれば、

MutableLiveData#setValueViewModel#setBackgroundColorTo

が呼ばれるので、タップした項目に応じて色変化のアニメーションが実行されます。

//念のため再掲
class MainViewModel(application: Application): AndroidViewModel(application){
    val headerColorTo = MutableLiveData<Int>()

    //コレ↓
    fun setRecipeType(enum: RecipeTypeEnum) {
        headerColorTo.value = 
            ContextCompat.getColor(getApplication(), enum.symbolColorId)
    }
}

NavigationDrawer のイベント実装

NavigationDrawer の項目をタップしたときに MainViewModel#setRecipeTypeをコールするように、イベントを実装します。 NavigationDrawer はレイアウトファイル上 Activity に配置されているのでActivity#onCreateで実装してもいいですが、UIアクション実装の原則に従って Fragment 側で実装するようにしましょう( ˘ω˘)

mainBinding.navView.setNavigationItemSelectedListener{ menuItem ->
    when (menuItem.itemId){
            R.id.all_navigation_menu_item ->{ binding.viewmodel?.setRecipeType(RecipeTypeEnum.ALL_DISH) }
            //中略
            else ->{ throw IllegalArgumentException() }
        }
    ...
    return@setNavigationItemSelectedListener true
}

それでは最後に HeaderView の Inflate の実装です。一癖あってハマったところ。

HeaderView の Inflate

前回の記事をご参照ください><

やったことまとめ

  1. ViewModel クラスを定義、フィールドに MutableLiveData 型の変数を宣言
  2. 拡張関数 + BindingAdapter でアニメーション用のカスタムセッターを定義
  3. HeaderView の DataBinding Layout File(xml)を定義、カスタムセッターにバインド
  4. NavigationDrawer のイベント実装

所感

Java でカスタムセッターを定義する場合は static クラスを定義して行う模様ですが、Kotlin では static クラスを定義できません。

ググるとシングルトン(object)クラスで実装する方法が出てきましたが、なんとなくソレジャナイ感があったのでさてどうしたものかと思っていました。

そんな中ヒットしたのがこの記事です↓

jumperson.hatenablog.com

やはり Kotlin は最強ですね。ちょっと凝ったことしようとすると拡張関数どんどこ増えるけど気にしない(マテ