はじめに
とある要件で独自でカレンダー表示が必要になったため、
3rdパーティ製のライブラリは使わず自作しました。
カレンダーとしての機能は最低限にしているので、
コードはシンプルな構成となっております。
環境
- Android / Kotlin
- RecyclerView (Support Libraryなので勘弁してくださいw)
実装
カレンダーのアイテムクラス
ヘッダと日付部分のデータクラスです。
曜日(ヘッダ)、日付(セル)、本日か否か、の情報のみです。
要件に合わせてプロパティは増やしてください。
sealed class CalendarItem {
data class Header(val text: String) : CalendarItem()
data class Day(val day: String, val isToDay: Boolean = false) : CalendarItem()
}
Adapterクラス
アイテムクラスの型によって、ヘッダと日付でViewHolderを分けます。
動的に日付のセルを変化させる対応は、内部のViewHolderクラスでやっております。
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)
}
}
}
}
日付データ作成クラス
カレンダーの日付データの作成部分。
とにかく泥臭いコードをゴリゴリと書きます。
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側でやっちゃいます。
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)
}
}
まとめ
レイアウトは割愛しますが、これだけでカレンダーの表示が行えます。
カスタマイズは自由にできるので、要件に合わせて柔軟に対応できるかと思います。