Android ライブラリを使わず自力でカレンダーを表示

## はじめに
とある要件で独自でカレンダー表示が必要になったため、
3rdパーティ製のライブラリは使わず自作しました。

カレンダーとしての機能は最低限にしているので、
コードはシンプルな構成となっております。

<img src="https://qiita-image-store.s3.amazonaws.com/0/112186/f5ffbd60-d406-0e9d-77e0-347ef2849e1a.png" width=320>

## 環境
* Android / Kotlin
* RecyclerView (Support Libraryなので勘弁してくださいｗ)

## 実装

### カレンダーのアイテムクラス

ヘッダと日付部分のデータクラスです。
曜日(ヘッダ)、日付(セル)、本日か否か、の情報のみです。
要件に合わせてプロパティは増やしてください。

```kotlin
sealed class CalendarItem {
    data class Header(val text: String) : CalendarItem()
    data class Day(val day: String, val isToDay: Boolean = false) : CalendarItem()
}
```

### Adapterクラス

アイテムクラスの型によって、ヘッダと日付でViewHolderを分けます。
動的に日付のセルを変化させる対応は、内部のViewHolderクラスでやっております。

```kotlin
class CalendarAdapter : RecyclerView.Adapter<CalendarAdapter.BaseViewHolder>() {

    companion object {
        private const val VIEW_TYPE_HEADER = R.layout.list_item_calendar_header
        private const val VIEW_TYPE_DAY = R.layout.list_item_calendar
    }

    var dataSource: Array<CalendarItem> = emptyArray()
        set(value) {
            field = value
            notifyDataSetChanged()
        }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
        return when (viewType) {
            VIEW_TYPE_HEADER -> CalendarHeaderViewHolder(LayoutInflater.from(parent.context).inflate(viewType, parent, false))
            VIEW_TYPE_DAY -> CalendarDayViewHolder(LayoutInflater.from(parent.context).inflate(viewType, parent, false))
            else -> throw IllegalStateException("Bad view type!!")
        }
    }

    override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
        val item = dataSource[position]
        when (item) {
            is CalendarItem.Header -> {
                holder.setViewData(item)
            }
            is CalendarItem.Day -> {
                holder.setViewData(item)
            }
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (dataSource[position]) {
            is CalendarItem.Header -> VIEW_TYPE_HEADER
            is CalendarItem.Day -> VIEW_TYPE_DAY
        }
    }

    override fun getItemCount(): Int {
        return dataSource.size
    }

    abstract class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        abstract fun setViewData(item: CalendarItem)
    }

    private class CalendarHeaderViewHolder(view: View) : BaseViewHolder(view) {

        private val headerLabel: TextView = view.headerLabel

        override fun setViewData(item: CalendarItem) {
            item as CalendarItem.Header
            headerLabel.text = item.text
        }
    }

    private class CalendarDayViewHolder(view: View) : BaseViewHolder(view) {

        private val dayLabel: TextView = view.dayLabel

        override fun setViewData(item: CalendarItem) {
            item as CalendarItem.Day
            dayLabel.text = item.day
            dayLabel.visibility = if (item.day.isEmpty()) {
                View.GONE
            } else {
                View.VISIBLE
            }
            if (item.isToDay) {
                itemView.setBackgroundColor(Color.YELLOW)
            } else {
                itemView.setBackgroundColor(Color.WHITE)
            }
        }
    }
}
```
### 日付データ作成クラス

カレンダーの日付データの作成部分。
とにかく泥臭いコードをゴリゴリと書きます。

```kotlin
class CalendarItemFactory {

    companion object {

        private const val MAX_ROW = 6
        private const val NUM_WEEK = 7

        fun create(offsetMonth: Int): Array<CalendarItem> {
            val itemList: MutableList<CalendarItem> = arrayListOf()
            val calendar = Calendar.getInstance()
            val currentDay = calendar.get(Calendar.DATE)
            calendar.add(Calendar.MONTH, offsetMonth)
            val dayOfMonth = calendar.getActualMaximum(Calendar.DATE) // 当月は何日か？
            calendar.set(Calendar.DATE, 1)
            val dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK) // SUNDAY(1)、MONDAY(2) ...

            // ヘッダー部分
            arrayOf("日", "月", "火", "水", "木", "金", "土").forEach {
                itemList.add(CalendarItem.Header(it))
            }
            val headerSize = itemList.size

            // 開始日までを埋める処理
            for (i in 1 until dayOfWeek) {
                itemList.add(CalendarItem.Day(""))
            }
            // 有効日を埋める処理
            for (i in 1..dayOfMonth) {
                if (offsetMonth == 0 && i == currentDay) {
                    itemList.add(CalendarItem.Day("$i", true))
                } else {
                    itemList.add(CalendarItem.Day("$i"))
                }
            }
            // 余りセルを埋める処理
            val fillSize = (MAX_ROW * NUM_WEEK + headerSize) - itemList.size
            for (i in 0 until fillSize) {
                itemList.add(CalendarItem.Day(""))
            }
            return itemList.toTypedArray()
        }
    }
}
```
### Activity

最後にRecyclerViewとCalendarAdapterの設定を行います。
今回は日付の切り替えとラベル表示はActivity側でやっちゃいます。

```kotlin
class MainActivity : AppCompatActivity() {

    private var offsetMonth = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val context = applicationContext ?: return

        val adapter = CalendarAdapter()
        recyclerView.adapter = adapter
        recyclerView.layoutManager = GridLayoutManager(context, 7)

        adapter.dataSource = CalendarItemFactory.create(offsetMonth)
        updateDateLabel()

        prevButton.setOnClickListener {
            adapter.dataSource = CalendarItemFactory.create(--offsetMonth)
            updateDateLabel()
        }
        nextButton.setOnClickListener {
            adapter.dataSource = CalendarItemFactory.create(++offsetMonth)
            updateDateLabel()
        }
    }

    private fun updateDateLabel() {
        dateLabel.text = Date().apply { offset(month = offsetMonth) }.toYearMonthText()
    }

    private fun Date.offset(year: Int = 0, month: Int = 0, day: Int = 0) {
        time = Calendar.getInstance().apply {
            add(Calendar.YEAR, year)
            add(Calendar.MONTH, month)
            add(Calendar.DATE, day)
        }.timeInMillis
    }

    private fun Date.toYearMonthText(): String {
        return SimpleDateFormat("yyyy/MM").format(time)
    }
}
```

### まとめ
レイアウトは割愛しますが、これだけでカレンダーの表示が行えます。
カスタマイズは自由にできるので、要件に合わせて柔軟に対応できるかと思います。