7
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-02

はじめに

とある要件で独自でカレンダー表示が必要になったため、
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)
    }
}

まとめ

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

7
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?