日々是好日

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

StickyListHeaders風のライブラリを自作する-7

StickyListHeaders風のライブラリを自作する-6 - 日々是好日の続き。

ヘッダーオブジェクトを自動で挿入するようにしてみた。

リファクタしてみた

旧PinningListViewライブラリは、ヘッダーとなるオブジェクトをユーザ側で挿入しなければならなかった。 たとえばこんな形。

val list: List<Any> = listOf(
  Header(...),
  Item(...),
  Item(...),
  Header(...),
  Item(...),
  Item(...),
  ...
)

Headerはヘッダー用の、Itemはコンテンツ用のオブジェクト。 型を混在させる必要があるため、List<Any>としてリストを作っていた。

こんなん使い物にならないのは明らかだったので、List<Item>として渡せるように改良してみた。

どのように使いたいか

コンテンツのリストList<Item>を渡すだけで、次のようにヘッダーを自動で表示する。 この例では日付の部分が自動で挿入されたヘッダーオブジェクト。

f:id:kcpoipoi:20191110234927g:plain

ヘッダー抽出クラス

新たにPinningListHeaderExtractorインターフェースを定義し、ヘッダーに使用するプロパティを指定できるようにした。

// E : アイテムクラス
// T : 参照するプロパティの型
// H : ヘッダークラス
interface PinningHeaderExtractor<E, T, out H> {
  val referenceHeaderProperty: KProperty1<E, T>
  fun createElement(sectionTopElement: E): H
}

// たとえばアイテムクラスはこんな感じ
data class Item(
  val id: Int,
  val index: String,
  val content: String
)

// ヘッダークラスはこんな感じ
data class Header(
  val header: String
)

referenceHeaderPropertyでは、ヘッダーを挿入したい箇所を検知するためのプロパティを設定する。 プロパティはリフレクション(Item::index)により指定する。

ライブラリ側ではこのItem::indexを基にリストを走査し、Item::indexが切り替わるところでcreateElementをコールする。

createElementではHで指定したオブジェクトが返されるので、このオブジェクトをリストに挿入しヘッダーとして表示する。

実装

// Activity や Fragment など
class Hoge {
  fun initializeView() {
    val list = listOf<Item>(...)
    val adapter = MyPinningListAdapter(list, R.layout.header)
    .also {

      // ヘッダーを抽出(Extract)するための Extractor をセット
      it.setExtractor(object : PinningListHeaderExtractor<Item, String, Header> {
        
        // ヘッダーの切り替わり検知用プロパティを指定
        override val referenceHeaderProperty: KProperty1<Item, String>
          get() = Item::index

        // ヘッダーオブジェクトを生成する
        override fun createElement(sectionTopElement: Item): Header {
          return Header(sectionTopElement.index)
        }
      })
      
      // リストを走査してヘッダーオブジェクトをセットする
      it.extractHeader()
    }
    ...
  }
}

// PinningListAdapter を継承したアダプタを作り、リストとヘッダー用レイアウトファイルを渡す
class MyPinningListAdapter(list: List<Item>, @LayoutRes override val headerLayout: Int?)
  : PinningListAdapter<Item, String, Header, RecyclerView.ViewHolder>(list) {
    ...
  }

// 指定したプロパティを読み取り、自動でヘッダーオブジェクトを挿入する
// 今回は index プロパティを読み取る
data class Item(
  val id: Int,
  val index: String,
  val content: String
)

// ヘッダーには index を表示する
data class Header(
  val header: String
)

4行目のリストはこんな感じで作る。 "A", "B", "C"それぞれの境界のところにHeader("A")等が挿入される。

val list = listOf(
  Item(id = 0, index = "A", content = "content1"),
  Item(1, "A", "content2"),
  Item(2, "B", "content3"),
  Item(3, "B", "content4"),
  Item(4, "C", "content5")
  )

ライブラリ公開先

bintray.com

Gradleでは次の記述で使えます。 JCenter申請中なので、とおればリポジトリの参照は不要となります。

// プロジェクトレベル build.gradle
repositories {
  maven { url 'https://dl.bintray.com/hide-kc/maven'}
}

// モジュールレベル build.gradle
dependencies {
  implementation 'work.kcs_labo:pinninglistview:1.0.1'
}