AndroidでScrollViewなどにListViewを入れるようなケースのお話です。この場合、ListViewのアイテム数によって動的に高さを変える必要があります。
[ Android ] コピペでListViewの高さを動的に設定 (初学者向け)
ListViewの要素数に応じてViewの高さを変える
【Android】ScrollViewにListViewを入れる
などで、「viewを取得して高さを足していけばいいよ!」という解決策が提示されています。
しかし、Databindingを使用しているときは、これらの方法を使用すると高さが合わなかったり(なぜか match_parent なのに wrap_contents で計算されたり?)、バインドする前の高さになってしまったりしました。Databindingのバインド後でTextViewが2行になっているのに、高さが1行分しか確保できていないような感じですね。
そこで、ListViewの高さをバインド後の高さに合わせるべく、アダプタに高さのリストを持たせ、データバインディングが終わったときにListViewの高さを変えるAdapterクラスを作ってみました。
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が呼ばれないんですよん なんでだろ
これのせいでアイテムが少しずつ描画されちゃう どうしよう とりあえず高さは合ってるけどうん