StickyListHeaders風のライブラリを自作する-2
StickyListHeaders風のライブラリを自作する-1 - 日々是好日の続き。
ItemDecoration でがんばって Sticky な動作を実現してみました。
次は、ごく簡単な機能を提供する記事を書きたい。
前回こんなこと言ってましたが、結局実装までやっちゃいましたね。←
ItemDecoration の実装について
StackOverFlow の回答を参考にしつつ、Kotlin に置き換えて要らなそうな部分を削りました。
次の ItemDecoration はライブラリ側で実装。
class PinningHeaderDecoration(private val listener: PinningHeaderListener) : RecyclerView.ItemDecoration() { //ヘッダービューのキャッシュ(雑実装) private var header = Pair<Int?, View?>(null, null) override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { super.onDrawOver(c, parent, state) //表示されている一番上のビューを取得 val topView = parent.getChildAt(0) ?: return //アダプタ上でのインデックスを取得 val adapterPosition = parent.getChildAdapterPosition(topView) //NO_POSITION なら return if (adapterPosition == RecyclerView.NO_POSITION) return //表示したいヘッダーのインデックスを取得 //getCurrentHeaderPosition はアダプタ側で実装 val currentHeaderPosition = listener.getCurrentHeaderPosition(adapterPosition) ?: return //ヘッダービューがすでに展開されていればフィールドから取得、無ければ inflate val currentHeader = when (val layoutResId = listener.getHeaderLayout() ?: return) { header.first -> { header.second!! } else -> { val inflater = LayoutInflater.from(parent.context) val view = inflater.inflate(layoutResId, parent, false) header = Pair(layoutResId, view) view } } ?: return //ヘッダーのサイズを RecyclerView にフィット fixLayoutSize(parent, currentHeader) //ヘッダーにデータをバインド listener.bindHeaderData(currentHeader, currentHeaderPosition) val contactPoint = currentHeader.bottom val nextHeaderTop = getNextHeaderPoint(parent) //表示中のヘッダーに次のヘッダーが接触したら動かす if (nextHeaderTop != null && nextHeaderTop <= contactPoint && nextHeaderTop > 0 ) { moveHeader(c, currentHeader, nextHeaderTop) } else { drawHeader(c, currentHeader) } } //次のヘッダーの位置を探す private fun getNextHeaderPoint(parent: RecyclerView): Int? { for (childPosition in 0 until parent.childCount) { val child = parent.getChildAt(childPosition) val adapterPosition = parent.getChildAdapterPosition(child) if (listener.isHeader(adapterPosition)) return child.top } return null } private fun drawHeader(c: Canvas, view: View) { c.save() c.translate(0F, 0F) view.draw(c) c.restore() } private fun moveHeader(c: Canvas, currentView: View, nextViewTop: Int) { c.save() //Canvas の座標を調整 c.translate(0F, (nextViewTop - currentView.height).toFloat()) currentView.draw(c) c.restore() } private fun fixLayoutSize(parent: ViewGroup, view: View) { ... } //アダプタに必要なインターフェース interface PinningHeaderListener { fun isHeader(adapterPosition: Int): Boolean fun getCurrentHeaderPosition(adapterPosition: Int): Int? fun getHeaderLayout() : Int? fun bindHeaderData(header: View, adapterPosition: Int) } }
Adapter 側の実装について
次にアダプタ側の PinningHeaderListener の実装。 ここは app モジュールで実装しているのでかなり雑ですが、 PinningHeaderAdapter とかにしてライブラリに取り込む予定。
class MyAdapter(private val list: List<Int>) : RecyclerView.Adapter<MyAdapter.MyItemViewHolder>(), PinningHeaderDecoration.PinningHeaderListener { //フィールドでいいのかなぁと思いつつ保持 private var headerLayout: Int? = null override fun isHeader(adapterPosition: Int): Boolean { return getItemViewType(adapterPosition) == HEADER } override fun getCurrentHeaderPosition(adapterPosition: Int): Int? { var index = adapterPosition //adapterPosition からヘッダーにあたるまでさかのぼる while (index > -1) { if (getItemViewType(index) == HEADER) return index index-- } return null } //ヘッダーのレイアウトはとりあえずフィールドに保持 override fun getHeaderLayout(): Int? { return headerLayout } //ヘッダーにデータをバインド override fun bindHeaderData(header: View, adapterPosition: Int) { val textView = header.textView textView.text = "Header ${list[adapterPosition]}" } //ヘッダーとそれ以外とを viewType で仕分け override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyItemViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { 0 -> { MyItemViewHolder(inflater.inflate(R.layout.item_layout, parent, false)) } else -> { headerLayout = R.layout.header_layout MyItemViewHolder(inflater.inflate(R.layout.header_layout, parent, false)) } } } //ヘッダーとそれ以外とを viewType で仕分け override fun getItemViewType(adapterPosition: Int): Int { return when (list[adapterPosition] % 5) { 0 -> HEADER else -> super.getItemViewType(adapterPosition) } } }
次は Adapter を抽象化してライブラリとして取り込む…つもり。