##はじめに
RecyclerViewとItemDecorationクラスを用いて、Sticky Headerを実装します。
Epoxyなどのライブラリを利用することで、Sticky Headerの実装は可能ですが、今回は使い慣れてるRecyclerViewを用いて実装します。
ライブラリを使うより、描画のロジックが分かりやすいため、できればこちらの方法で実装することをお勧めします。
理解して頂くためにも、ソースコードにはなるべくコメント残していますので、最後まで見て頂ける嬉しいです。
ソースコードはGitHubに上げております。
##そもそもSticky Headerとは?
Sticky Headerとはこのような動きをするリストです。
## 全体の流れ
RecyclerViewとRecyclerViewのItemDecorationクラスを用いて実装します。
- 各リストを管理するItemクラスを作成
- StickyHeader描画用のインターフェースの定義
- RecyclerViewのAdapterクラスに上記インターフェースを継承させる
- Adapterクラスのインスタンスから、RecyclerViewのItemDecorationを作成し、ItemDecorationのonDrawOverメソッド内で、Sticky Headerの描画ロジックを作成
- RecyclerViewに上記のItemDecorationをセット
以上が全体の流れになります。
# 実装
まずは各リストを管理するItemクラスを作成します。
このItemクラスでは日付を管理するString型のdateと、Headerかどうかを判断するBoolean型のisHeaderの二つの変数を持たせます。
data class Item(var date: String, var isHeader: Boolean)
通常のRecyclerViewの実装から始める
一度に書いてしまうと混乱するため、まずは通常のRecyclerViewの実装します。
MainActivity
MainActivityではAdapterのセットと、日付を格納したリストを作成します。
class MainActivity : AppCompatActivity() {
companion object {
const val STICKY_HEADER_SAMPLE = "STICKY_HEADER_SAMPLE"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
// adapterのセット
val adapter = MyAdapter(makeItems())
recyclerView.adapter = adapter
}
// 2021年5月1日から今日までの日付を格納したリストを作成
// 10の倍数をStickyHeaderとする
private fun makeItems(): MutableList<Item>{
val list = mutableListOf<Item>()
// 現在の日付を元にカレンダーのインスタンスを取得
val calender = Calendar.getInstance()
// 2021年5月1日
val pastCalender = Calendar.getInstance().apply {
set(2021,5,1)
}
while (calender.after(pastCalender)){
val day = calender.get(Calendar.DATE)
val month = calender.get(Calendar.MONTH).plus(1)
val date = "${month}月${day}日"
// 10の倍数であればSticky Headerとする
val isHeader = (day % 10) == 0
val item = Item(date = date, isHeader = isHeader)
list.add(item)
calender.add(Calendar.DATE,-1)
}
return list
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Adapter
Headerタイプと通常タイプの2種類のレイアウトを使用しています。
class MyAdapter(private val mItemList: List<Item>) : RecyclerView.Adapter<MyAdapter.ParentViewHolder>(){
enum class ListStyle(val type: Int){
HeaderType(0),
NormalType(1)
}
// ViewHolder機構(親クラス)
open class ParentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
// ViewHolder機構 Header Type(子クラス)
class HeaderViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
val headerDate: TextView = view.findViewById(R.id.headerDate)
}
// ViewHolder機構 Normal type(子クラス)
class NormalViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
val date: TextView = view.findViewById(R.id.date)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder {
return when(viewType){
// HeaderType
ListStyle.HeaderType.type -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.header_layout, parent,false)
HeaderViewHolder(view)
}
// NormalType
else -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.normal_layout, parent,false)
NormalViewHolder(view)
}
}
}
override fun onBindViewHolder(holder: ParentViewHolder, position: Int) {
val item = mItemList[position]
when(holder){
// HeaderType
is HeaderViewHolder -> {
holder.headerDate.text = item.date
}
// NormalType
is NormalViewHolder -> {
holder.date.text = item.date
}
}
}
override fun getItemCount(): Int = mItemList.size
override fun getItemViewType(position: Int): Int {
val item = mItemList[position]
return if(item.isHeader){
ListStyle.HeaderType.type
}else{
ListStyle.NormalType.type
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".MainActivity"
android:background="#eee"
android:paddingTop="25dp"
android:paddingBottom="25dp">
<TextView
android:id="@+id/headerDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/normal_layout_bg"
tools:context=".MainActivity"
android:paddingTop="25dp"
android:paddingBottom="25dp">
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
以上が通常のRecyclerViewの実装です。
10の倍数の日付のセルに着色させた、至って普通のリストですね。
では次にこのヘッダー用のセルをSticky Headerへと変えて行きます。
まずRecycleraViewのItemDecorationクラスの作成とSticky Headerの作成に必要なメソッドを定義したンターフェースを作成します。
StickyHeaderHandlerインターフェースの作成
interface StickyHeaderHandler {
companion object {
const val HEADER_POSITION_NOT_FOUND = -1
}
/** StickyHeaderのポジションを返す */
fun getHeaderPosition(itemPosition: Int): Int
/** StickyHeaderのレイアウトIDを返す */
fun getHeaderLayout(headerPosition: Int): Int
/** StickyHeaderにデーターを渡す */
fun bindHeaderData(header: View?, headerPosition: Int)
/** リストがヘッダーかどうかを判定する */
fun isHeader(itemPosition: Int): Boolean
}
実装先である、Adapterクラスにて具体的な処理内容を記述します。
class MyAdapter(private val mItemList: List<Item>) : RecyclerView.Adapter<MyAdapter.ParentViewHolder>(),
StickyHeaderHandler // 追加{
enum class ListStyle(val type: Int){
HeaderType(0),
NormalType(1)
}
// ViewHolder機構(親クラス)
open class ParentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
// ViewHolder機構 Header Type(子クラス)
class HeaderViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
val headerDate: TextView = view.findViewById(R.id.headerDate)
}
// ViewHolder機構 Normal type(子クラス)
class NormalViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
val date: TextView = view.findViewById(R.id.date)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder {
return when(viewType){
// HeaderType
ListStyle.HeaderType.type -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.header_layout, parent,false)
HeaderViewHolder(view)
}
// NormalType
else -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.normal_layout, parent,false)
NormalViewHolder(view)
}
}
}
override fun onBindViewHolder(holder: ParentViewHolder, position: Int) {
val item = mItemList[position]
when(holder){
// HeaderType
is HeaderViewHolder -> {
holder.headerDate.text = item.date
}
// NormalType
is NormalViewHolder -> {
holder.date.text = item.date
}
}
}
override fun getItemCount(): Int = mItemList.size
override fun getItemViewType(position: Int): Int {
val item = mItemList[position]
return if(item.isHeader){
ListStyle.HeaderType.type
}else{
ListStyle.NormalType.type
}
}
// ここから下を追加 ---------
// 表示中の一番上のセルからリストの先頭(一番上)まで遡り、ヘッダーがあればそのインデックスを、なければ -1を返す
override fun getHeaderPosition(itemPosition: Int): Int {
var headerPosition = StickyHeaderHandler.HEADER_POSITION_NOT_FOUND
var targetItemPosition = itemPosition
do {
if (isHeader(targetItemPosition)) {
headerPosition = targetItemPosition
break
}
targetItemPosition -= 1
} while (targetItemPosition >= 0)
return headerPosition
}
// Header用のレイアウトを返す
override fun getHeaderLayout(headerPosition: Int): Int {
return R.layout.header_layout
}
// Headerセルにてデータをbindする
override fun bindHeaderData(header: View?, headerPosition: Int) {
header?:return
val headerItem = mItemList[headerPosition]
if (headerItem.isHeader) {
val headerDate = header.findViewById(R.id.headerDate) as TextView
headerDate.text = headerItem.date
}
}
override fun isHeader(itemPosition: Int): Boolean {
val item = mItemList[itemPosition]
return item.isHeader
}
}
ItemDecorationクラスの実装
このItemDecorationクラス内で先ほど作成したインターフェースのメソッドを使い、Sticky Headerの描画を行います。
何をしてるかざっくり解説
- ・画面上にSticky Headerが既に描画されている場合
- そのSticky Headerの次のセル(下のセル)がSticky Headerタイプのセルかどうかを判定し、Sticky Headerタイプであれば既存のSticky Headeを押し上げ、新たなSticky Headerを描画。Sticky Headerタイプでなければ、既存のSticky Headerをそのまま同じ位置で最描画。
- ・画面上にSticky Headerが描画されていない場合
- 表示中の一番上のセルがSticky Headerタイプであれば、そのセルを新たにSticky Headerとして描画。通常のセルであれば何もしない。
class MyItemDecoration(private val mStickyHeaderListener: StickyHeaderHandler) : RecyclerView.ItemDecoration() {
// Header View
private var mCurrentHeaderView: View? = null
// RecyclerViewのセルが表示される度に呼ばれる
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
// 表示中のリスト内のトップViewを取得
val topChildView: View = parent.getChildAt(0)?:return
// topChildViewのインデックス
val topChildViewPosition = parent.getChildAdapterPosition(topChildView)
// topChildViewPosition取得失敗時はこれ以降の処理を行わない
if (topChildViewPosition == RecyclerView.NO_POSITION) return
// 直近のHeaderPositionのインデックスを取得
val prevHeaderPosition = mStickyHeaderListener.getHeaderPosition(topChildViewPosition)
// 直近にHeaderが存在しない場合はこれ以降の処理を行わない
if(prevHeaderPosition == StickyHeaderHandler.HEADER_POSITION_NOT_FOUND) return
// 直近にHeaderが存在する → topChildViewPositionにHeaderが存在する
mCurrentHeaderView = getHeaderView(topChildViewPosition, parent)
// 現在のHeaderレイアウトのセット
fixLayoutSize(parent, mCurrentHeaderView)
// 現在のHeaderのBottom Positionを取得 (親Viewからの相対距離)
val contactPoint = mCurrentHeaderView!!.bottom
// Headerの次のセルを取得
val nextCell = getNextCellToHeader(parent, contactPoint) ?: return // 次のセルがない
// nextCellのインデックスを取得
val nextCellPosition = parent.getChildAdapterPosition(nextCell)
if(nextCellPosition == StickyHeaderHandler.HEADER_POSITION_NOT_FOUND) return
// nextCellがHeaderかどうかの判定
if (mStickyHeaderListener.isHeader(nextCellPosition)) {
// 既存のStickyヘッダーを押し上げる
moveHeader(c, mCurrentHeaderView, nextCell)
return
}
// Stickyヘッダーの描画
drawHeader(c, mCurrentHeaderView)
}
// HeaderのViewを取得
private fun getHeaderView(itemPosition: Int, parent: RecyclerView): View? {
val headerPosition = mStickyHeaderListener.getHeaderPosition(itemPosition)
val layoutResId = mStickyHeaderListener.getHeaderLayout(headerPosition)
// Headerレイアウトをinflate
val headerView = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
// データバインディング
mStickyHeaderListener.bindHeaderData(headerView, headerPosition)
return headerView
}
// Headerレイアウトのセット
private fun fixLayoutSize(parent: ViewGroup, headerView: View?) {
headerView?:return
// RecyclerViewのSpecを取得
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
// Header ViewのSpecを取得
val headerWidthSpec = ViewGroup.getChildMeasureSpec(
widthSpec,
parent.paddingLeft + parent.paddingRight,
headerView.layoutParams.width
)
val headerHeightSpec = ViewGroup.getChildMeasureSpec(
heightSpec,
parent.paddingTop + parent.paddingBottom,
headerView.layoutParams.height
)
headerView.measure(headerWidthSpec, headerHeightSpec)
headerView.layout(0, 0, headerView.measuredWidth, headerView.measuredHeight)
}
// Headerの次のセルを取得
private fun getNextCellToHeader(parent: RecyclerView, contactPoint: Int): View? {
var nextView: View? = null
for (index in 0 until parent.childCount) {
val child = parent.getChildAt(index)
if (child.bottom > contactPoint) {
if (child.top <= contactPoint) {
nextView = child
break
}
}
}
return nextView
}
// Stickyヘッダーを動かす
private fun moveHeader(c: Canvas, currentHeader: View?, nextCell: View) {
currentHeader?:return
c.save()
c.translate(0F, (nextCell.top - currentHeader.height).toFloat())
currentHeader.draw(c)
c.restore()
}
// Stickyヘッダーを描画する
private fun drawHeader(c: Canvas, header: View?) {
c.save()
c.translate(0F, 0F)
header!!.draw(c)
c.restore()
}
}
あとは上記のMyItemDecorationクラスをRecyclerViewにセットしてやれば終わりです。
全てのソースコード
data class Item(var date: String, var isHeader: Boolean)
class MainActivity : AppCompatActivity() {
companion object {
const val STICKY_HEADER_SAMPLE = "STICKY_HEADER_SAMPLE"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
// adapterのセット
val adapter = MyAdapter(makeItems())
recyclerView.adapter = adapter
// itemDecorationのセット
val itemDecoration = MyItemDecoration(adapter)
recyclerView.addItemDecoration(itemDecoration)
}
// 2021年5月1日から今日までの日付を格納したリストを作成
// 10の倍数をStickyHeaderとする
private fun makeItems(): MutableList<Item>{
val list = mutableListOf<Item>()
// 現在の日付を元にカレンダーのインスタンスを取得
val calender = Calendar.getInstance()
// 2021年5月1日
val pastCalender = Calendar.getInstance().apply {
set(2021,5,1)
}
while (calender.after(pastCalender)){
val day = calender.get(Calendar.DATE)
val month = calender.get(Calendar.MONTH).plus(1)
val date = "${month}月${day}日"
// 10の倍数であればSticky Headerとする
val isHeader = (day % 10) == 0
val item = Item(date = date, isHeader = isHeader)
list.add(item)
calender.add(Calendar.DATE,-1)
}
return list
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.constraintlayout.widget.ConstraintLayout>
class MyAdapter(private val mItemList: List<Item>) : RecyclerView.Adapter<MyAdapter.ParentViewHolder>(),StickyHeaderHandler {
enum class ListStyle(val type: Int){
HeaderType(0),
NormalType(1)
}
// ViewHolder機構(親クラス)
open class ParentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
// ViewHolder機構 Header Type(子クラス)
class HeaderViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
val headerDate: TextView = view.findViewById(R.id.headerDate)
}
// ViewHolder機構 Normal type(子クラス)
class NormalViewHolder(view: View) : MyAdapter.ParentViewHolder(view) {
val date: TextView = view.findViewById(R.id.date)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParentViewHolder {
return when(viewType){
// HeaderType
ListStyle.HeaderType.type -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.header_layout, parent,false)
HeaderViewHolder(view)
}
// NormalType
else -> {
val view = LayoutInflater.from(parent.context).inflate(R.layout.normal_layout, parent,false)
NormalViewHolder(view)
}
}
}
override fun onBindViewHolder(holder: ParentViewHolder, position: Int) {
val item = mItemList[position]
when(holder){
// HeaderType
is HeaderViewHolder -> {
holder.headerDate.text = item.date
}
// NormalType
is NormalViewHolder -> {
holder.date.text = item.date
}
}
}
override fun getItemCount(): Int = mItemList.size
override fun getItemViewType(position: Int): Int {
val item = mItemList[position]
return if(item.isHeader){
ListStyle.HeaderType.type
}else{
ListStyle.NormalType.type
}
}
// 表示中の一番上のセルからリストの先頭(一番上)まで遡り、ヘッダーがあればそのインデックスを、なければ -1を返す
override fun getHeaderPosition(itemPosition: Int): Int {
var headerPosition = StickyHeaderHandler.HEADER_POSITION_NOT_FOUND
var targetItemPosition = itemPosition
do {
if (isHeader(targetItemPosition)) {
headerPosition = targetItemPosition
break
}
targetItemPosition -= 1
} while (targetItemPosition >= 0)
return headerPosition
}
// Header用のレイアウトを返す
override fun getHeaderLayout(headerPosition: Int): Int {
return R.layout.header_layout
}
// Headerセルにてデータをbindする
override fun bindHeaderData(header: View?, headerPosition: Int) {
header?:return
val headerItem = mItemList[headerPosition]
if (headerItem.isHeader) {
val headerDate = header.findViewById(R.id.headerDate) as TextView
headerDate.text = headerItem.date
}
}
// Headerかどうかの判定
override fun isHeader(itemPosition: Int): Boolean {
val item = mItemList[itemPosition]
return item.isHeader
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".MainActivity"
android:background="#eee"
android:paddingTop="25dp"
android:paddingBottom="25dp">
<TextView
android:id="@+id/headerDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/normal_layout_bg"
tools:context=".MainActivity"
android:paddingTop="25dp"
android:paddingBottom="25dp">
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
class MyItemDecoration(private val mStickyHeaderListener: StickyHeaderHandler) : RecyclerView.ItemDecoration() {
// Header View
private var mCurrentHeaderView: View? = null
// RecyclerViewのセルが表示される度に呼ばれる
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
// 表示中のリスト内のトップViewを取得
val topChildView: View = parent.getChildAt(0)?:return
// topChildViewのインデックス
val topChildViewPosition = parent.getChildAdapterPosition(topChildView)
// topChildViewPosition取得失敗時はこれ以降の処理を行わない
if (topChildViewPosition == RecyclerView.NO_POSITION) return
// 直近のHeaderPositionのインデックスを取得
val prevHeaderPosition = mStickyHeaderListener.getHeaderPosition(topChildViewPosition)
// 直近にHeaderが存在しない場合はこれ以降の処理を行わない
if(prevHeaderPosition == StickyHeaderHandler.HEADER_POSITION_NOT_FOUND) return
// 直近にHeaderが存在する → topChildViewPositionにHeaderが存在する
mCurrentHeaderView = getHeaderView(topChildViewPosition, parent)
// 現在のHeaderレイアウトのセット
fixLayoutSize(parent, mCurrentHeaderView)
// 現在のHeaderのBottom Positionを取得 (親Viewからの相対距離)
val contactPoint = mCurrentHeaderView!!.bottom
// Headerの次のセルを取得
val nextCell = getNextCellToHeader(parent, contactPoint) ?: return // 次のセルがない
// nextCellのインデックスを取得
val nextCellPosition = parent.getChildAdapterPosition(nextCell)
if(nextCellPosition == StickyHeaderHandler.HEADER_POSITION_NOT_FOUND) return
// nextCellがHeaderかどうかの判定
if (mStickyHeaderListener.isHeader(nextCellPosition)) {
// 既存のStickyヘッダーを押し上げる
moveHeader(c, mCurrentHeaderView, nextCell)
return
}
// Stickyヘッダーの描画
drawHeader(c, mCurrentHeaderView)
}
// HeaderのViewを取得
private fun getHeaderView(itemPosition: Int, parent: RecyclerView): View? {
val headerPosition = mStickyHeaderListener.getHeaderPosition(itemPosition)
val layoutResId = mStickyHeaderListener.getHeaderLayout(headerPosition)
// Headerレイアウトをinflate
val headerView = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
// データバインディング
mStickyHeaderListener.bindHeaderData(headerView, headerPosition)
return headerView
}
// Headerレイアウトのセット
private fun fixLayoutSize(parent: ViewGroup, headerView: View?) {
headerView?:return
// RecyclerViewのSpecを取得
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
// Header ViewのSpecを取得
val headerWidthSpec = ViewGroup.getChildMeasureSpec(
widthSpec,
parent.paddingLeft + parent.paddingRight,
headerView.layoutParams.width
)
val headerHeightSpec = ViewGroup.getChildMeasureSpec(
heightSpec,
parent.paddingTop + parent.paddingBottom,
headerView.layoutParams.height
)
headerView.measure(headerWidthSpec, headerHeightSpec)
headerView.layout(0, 0, headerView.measuredWidth, headerView.measuredHeight)
}
// Headerの次のセルを取得
private fun getNextCellToHeader(parent: RecyclerView, contactPoint: Int): View? {
var nextView: View? = null
for (index in 0 until parent.childCount) {
val child = parent.getChildAt(index)
if (child.bottom > contactPoint) {
if (child.top <= contactPoint) {
nextView = child
break
}
}
}
return nextView
}
// Stickyヘッダーを動かす
private fun moveHeader(c: Canvas, currentHeader: View?, nextCell: View) {
currentHeader?:return
c.save()
c.translate(0F, (nextCell.top - currentHeader.height).toFloat())
currentHeader.draw(c)
c.restore()
}
// Stickyヘッダーを描画する
private fun drawHeader(c: Canvas, header: View?) {
c.save()
c.translate(0F, 0F)
header!!.draw(c)
c.restore()
}
}
interface StickyHeaderHandler {
companion object {
const val HEADER_POSITION_NOT_FOUND = -1
}
/** StickyHeaderのポジションを返す */
fun getHeaderPosition(itemPosition: Int): Int
/** StickyHeaderのレイアウトIDを返す */
fun getHeaderLayout(headerPosition: Int): Int
/** StickyHeaderにデーターを渡す */
fun bindHeaderData(header: View?, headerPosition: Int)
/** リストがヘッダーかどうかを判定する */
fun isHeader(itemPosition: Int): Boolean
}
終わりに
ライブラリを使うより、描画のロジックが分かりやすいはずです。
では!^^