【Android】 Databinding + ListView で可変リストの高さいっぱいに合わせる


AndroidでScrollViewなどにListViewを入れるようなケースのお話です。この場合、ListViewのアイテム数によって動的に高さを変える必要があります。

[[ Android ] コピペでListViewの高さを動的に設定 (初学者向け)](https://qiita.com/Tsutou/items/e0e70d6cf81db5b9d13a)
[ListViewの要素数に応じてViewの高さを変える](https://rooandqoo.hatenablog.com/entry/20130415/1365995731)
[【Android】ScrollViewにListViewを入れる](https://rooandqoo.hatenablog.com/entry/20130415/1365995731)
などで、「viewを取得して高さを足していけばいいよ！」という解決策が提示されています。

しかし、Databindingを使用しているときは、これらの方法を使用すると高さが合わなかったり(なぜか match_parent なのに wrap_contents で計算されたり?)、バインドする前の高さになってしまったりしました。Databindingのバインド後でTextViewが2行になっているのに、高さが1行分しか確保できていないような感じですね。

そこで、ListViewの高さをバインド後の高さに合わせるべく、アダプタに高さのリストを持たせ、データバインディングが終わったときにListViewの高さを変えるAdapterクラスを作ってみました。

```kotlin
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes
import androidx.databinding.DataBindingUtil
import androidx.databinding.OnRebindCallback
import androidx.databinding.ViewDataBinding
import java.lang.IllegalStateException

abstract class AutoSizeAdapter<T: ViewDataBinding>
    (@LayoutRes val res: Int, open val context: Context) : BaseAdapter() {

    private val measuredHeightList: MutableList<Int> = mutableListOf()
    private var parent: ViewGroup? = null
    lateinit var binding: T

    override fun notifyDataSetChanged() {
        //データセットが変更されたとき、計測済みの高さをクリアする
        measuredHeightList.clear()
        measuredHeightList.addAll( (0 until count).toMutableList())
        measuredHeightList.fill(-1)

        //データセットが空のときは高さを0にする
        if(count == 0){
            parent?.layoutParams?.height = 0
            parent?.requestLayout()
        }

        super.notifyDataSetChanged()
    }

    //getViewの変わりに実装する処理
    abstract fun onInflateView(position: Int, binding: T)

    override fun getView(position: Int, convertView: View?, _parent: ViewGroup?): View {

        parent = _parent

        binding = if(convertView == null){
            DataBindingUtil.inflate(LayoutInflater.from(context), res, parent, false)
        }else{
            DataBindingUtil.getBinding(convertView) ?: throw IllegalStateException()
        }

        if(measuredHeightList.size < position){
            measuredHeightList.add(position, 0)
        }
        binding.root.tag = position

        binding.addOnRebindCallback( object: OnRebindCallback<T>() {
            override fun onPreBind(binding: T): Boolean {
                binding.root.also { view ->
                    measuredHeightList[view.tag as Int] = view.measuredHeight
                    (parent as ListView).setHeightBasedOnChildren(measuredHeightList)
                }
                return super.onPreBind(binding)
            }
        })

        onInflateView(position, binding)

        return binding.root
    }

}

private fun ListView.setHeightBasedOnChildren(list: List<Int>){

    var sum = list.sum()

    //既にBinding済みのものがあって、全部が終わっていないとき、
    //残りのgetViewを呼ぶために残りのサイズを推定する
    if(list.count { it == -1 } > 0 && list.count {it != -1} > 0){
        sum += list.count { it == -1 } * list.filter {it != -1 }.max()
    }

    val params = layoutParams
    params.height = paddingTop + paddingBottom + sum + dividerHeight * (adapter.count) - 1
    layoutParams = params
    requestLayout()
}
```

## 課題
いずれか 0 のときは呼ばなくてよくない？ !measuredHeightList.any { it != 0 } みたいなとき。
そうすると他の行のgetViewが呼ばれないんですよん　なんでだろ
これのせいでアイテムが少しずつ描画されちゃう どうしよう とりあえず高さは合ってるけどうん