GitHub - kizitonwose/Calendar: A highly customizable calendar view and compose library for Android and Kotlin Multiplatform.
A highly customizable calendar view and compose library for Android and Kotlin Multiplatform. - kizitonwose/Calendar
github.com
캘린더에 날짜 이외에 다른 데이터를 함께 표시할 일이 생겨서 적합한 라이브러리를 찾던 중 kizitonwose의 Calendar 라이브러리를 발견했다.
해당 라이브러리는 캘린더를 개발자가 원하는 모습으로 꾸미는 것이 가능하다. 그리고 라이브러리를 사용하기 위한 샘플을 제공하고, 함수에 대한 정리 문서도 잘 되어 있어서 적용을 하는데 굉장히 편하다는 느낌이 들었다.
◾ 의존성 등록
libs.version.toml
[versions]
kizitonwoseCalendar = "2.6.0"
[libraries]
kizitonwose-calendar = { module = "com.kizitonwose.calendar:view", version.ref = "kizitonwoseCalendar" }
라이브러리를 사용하기 위해서 버전 카탈로그에 위와 같이 등록해 준다.
◾ 캘린더 날짜 표시하기
activity_main.xml
<com.kizitonwose.calendar.view.CalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:cv_dayViewResource="@layout/calendar_day_layout" />
라이브러리에서 '주' 단위 달력인 WeekCalednarView, '연도' 단위 달력인 YearCaledarView 그리고 '월' 기반의 CalendarView가 있다. 월별 표시를 위해서 CalendarView를 사용했다.
CalendarView를 보면 cv_dayViewResource가 있는데, 이 속성에 날짜 View 리소스를 넣어야 한다. 날짜 리소스는 직접 만들어서 사용하기 때문에 개발자가 표현하고자 하는 모습으로 꾸미는 것이 가능하다.
calendar_day_layout.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants"
android:paddingVertical="5dp">
<TextView
android:id="@+id/calendarDayText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="16sp"
tools:text="22" />
</androidx.constraintlayout.widget.ConstraintLayout>
우선 캘린더는 간단하게 일자만 표시하도록 했다.
DayViewContainer.kt
class DayViewContainer(view: View) : ViewContainer(view) {
val textView = view.findViewById<TextView>(R.id.calendarDayText)
}
MainActivity.kt
binding.calendarView.dayBinder = object : MonthDayBinder<DayViewContainer> {
override fun create(view: View) = DayViewContainer(view)
override fun bind(container: DayViewContainer, data: CalendarDay) {
container.textView.text = data.date.dayOfMonth.toString()
}
}
CalendarView는 RecyclerView로 구현이 되어 있는데, 방금 만든 날짜 리소스를 뷰 홀더에 전달할 필요가 있다. ViewContainer가 뷰 홀더 역할을 하게 된다.
CalendarView를 사용하기 위해 필요한 DayViewContainer 유형을 MonthDayBinder를 통해 제공한다. 즉, CalendarView는 monthDayBinder를 통해 DayViewContainer를 생성하고, 각 날짜 항목에 CalendarDay 데이터를 바인딩하여 화면에 표시한다.
MainActivity.kt
private fun setCalendar() {
val currentMonth = YearMonth.now()
val startMonth = currentMonth.minusMonths(100) // 과거 100달
val endMonth = currentMonth.plusMonths(100) // 미래 100달
val firstDayOfWeek = firstDayOfWeekFromLocale()
binding.calendarView.setup(startMonth, endMonth, firstDayOfWeek)
binding.calendarView.scrollToMonth(currentMonth)
// 생략
}
마지막으로 CalendarView에 표시할 날짜를 설정하면 캘린더가 그려진다.
◾ 캘린더 요일 표시하기
위의 과정에서 CalendarView에 날짜를 표시했는데, 이번에는 요일 표시를 하려고 한다. 요일을 표시하는 방법으로 첫 번째는 달력 위에 고정된 요일 View를 넣는 것이고, 두 번째는 CalendarView의 cv_monthHeaderResource 속성을 사용하여 정적이지 않고 스크롤이 되도록 표시할 수도 있다.
val daysOfWeek = daysOfWeek()
val daysOfWeek = daysOfWeek(firstDayOfWeek = DayOfWeek.THURSDAY)
요일을 표시할 때 daysOfWeek()를 사용하는데, daysOfWeek()는 현재 요일을 생성하는 함수로 기본적으로 '일, 월, 화, 수, 목, 금, 토' 순서로 설정이 되어 있는 거 같다. 만약 요일의 순서를 변경하고 싶은 경우 firstDayOfWeek에 값을 설정하여 변경할 수 있다.
calendar_day_title_text.xml
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center" />
caledar_day_titles_container.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:weightSum="7"
android:orientation="horizontal">
<include layout="@layout/calendar_day_title_text" />
<include layout="@layout/calendar_day_title_text" />
<include layout="@layout/calendar_day_title_text" />
<include layout="@layout/calendar_day_title_text" />
<include layout="@layout/calendar_day_title_text" />
<include layout="@layout/calendar_day_title_text" />
<include layout="@layout/calendar_day_title_text" />
</LinearLayout>
요일을 표시하기 위한 View를 그린다. TextView 타입을 정해놓고 container에서 재사용할 수 있게 했다.
1. 정적 View로 그리기
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<include
android:id="@+id/titlesContainer"
layout="@layout/calendar_day_titles_container" />
<com.kizitonwose.calendar.view.CalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintTop_toBottomOf="@id/titlesContainer"
app:cv_dayViewResource="@layout/calendar_day_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>
요일 View를 CalendarView 위에 표시한다.
MainActivity.kt
val titlesContainer = binding.titlesContainer as ViewGroup
titlesContainer.children
.map { it as TextView }
.forEachIndexed { index, textView ->
val dayOfWeek = daysOfWeek[index]
val title = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault())
textView.text = title
}
daysOfWeek의 요일 값을 하나씩 매칭시켜주면 요일 설정이 끝난다.
※ 위의 방법을 쓰지 않고 요일 '일~월' 요일 View를 그려서 표시해도 된다.
2. 동적 View로 그리기
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.kizitonwose.calendar.view.CalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:cv_dayViewResource="@layout/calendar_day_layout"
app:cv_monthHeaderResource="@layout/calendar_day_titles_container" />
</androidx.constraintlayout.widget.ConstraintLayout>
CalendarView의 cv_monthHeaderResource 속성에 요일 리소스를 넣어준다.
MonthViewContainer.kt
class MonthViewContainer(view: View) : ViewContainer(view) {
val titlesContainer = view as ViewGroup
}
MainActivity.kt
binding.calendarView.monthHeaderBinder = object : MonthHeaderFooterBinder<MonthViewContainer> {
val daysOfWeek = daysOfWeek()
override fun create(view: View) = MonthViewContainer(view)
override fun bind(container: MonthViewContainer, data: CalendarMonth) {
if (container.titlesContainer.tag == null) {
container.titlesContainer.tag = data.yearMonth
container.titlesContainer.children.map { it as TextView }
.forEachIndexed { index, textView ->
val dayOfWeek = daysOfWeek[index]
val title = dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.getDefault())
textView.text = title
}
}
}
}
날짜를 표시했을 때와 마찬가지로 뷰홀더에 요일 View 리소스를 전달해야 하고, MonthHeaderFooterBinder를 통해 MonthViewContainer를 생성한다.
MonthViewContainer의 bind 블록에서 daysOfWeek의 요일 값을 하나씩 매칭시켜 주면 요일 설정이 끝난다.
제일 상단에 표시되는 요일은 첫 번째 정적으로 그린 것이고, 두 번째로 표시되는 요일은 동적으로 그린 것이다. 선호하는 동작에 맞춰 선택하면 될 것 같다.
◾ 날짜 선택(클릭)
문서를 보면 클릭 이벤트를 처리할 때 Activity나 Fragment에서 클릭 리스너를 구현하라고 되어있다. 그래서 ViewContainer가 Activity나 Fragment 내에서 선언이 된 것 같은데, 나는 ViewContainer를 분리해서 사용하고 싶어서 bind 블록 내에서 모두 처리했다.
DayViewContainer.kt
class DayViewContainer(view: View): ViewContainer(view) {
val containerView = view.findViewById<ConstraintLayout>(R.id.cl_container)
val textView = view.findViewById<TextView>(R.id.calendarDayText)
lateinit var day: CalendarDay
}
DayViewContainer에 클릭을 위한 containerView와 day 값을 추가한다.
MainActivity.kt
private var selectedDate: LocalDate? = null
binding.calendarView.dayBinder = object : MonthDayBinder<DayViewContainer> {
override fun create(view: View) = DayViewContainer(view)
override fun bind(container: DayViewContainer, data: CalendarDay) {
val textView = container.textView
val containerView = container.containerView
textView.text = data.date.dayOfMonth.toString()
if (data.position == DayPosition.MonthDate) {
containerView.visibility = View.VISIBLE
if (data.date == selectedDate) {
textView.setTextColor(Color.WHITE)
containerView.setBackgroundResource(R.drawable.selection_background)
} else {
textView.setTextColor(Color.BLACK)
containerView.background = null
}
containerView.setOnClickListener {
val currentSelection = selectedDate
if (currentSelection == data.date) {
selectedDate = null
binding.calendarView.notifyDateChanged(currentSelection)
} else {
selectedDate = data.date
binding.calendarView.notifyDateChanged(data.date)
if (currentSelection != null) {
binding.calendarView.notifyDateChanged(currentSelection)
}
}
}
} else {
containerView.visibility = View.INVISIBLE
}
}
}
DayPosition은 MonthDate, InDate, OutDate가 있다. data.position으로 해당 값을 확인할 수 있다.
- MonthDate: 현재 표시되는 날짜 중 현재 달의 날짜
- InDate: 현재 표시되는 날짜 중 이전 달의 날짜
- OutDate: 현재 표시되는 날짜 중 다음 달의 날짜
CalendarView에 표시되는 데이터 중 MonthDate인 값만 표시 및 클릭 이벤트 처리를 해준다. 그리고 이 라이브러리에는 날짜 선택을 했다는 것에 대한 상태값이 없기 때문에 selectedDate 필드 변수를 추가하여 클릭한 날짜에 대해서만 강조 표시를 하고, 다시 클릭했을 때 해제할 수 있도록 한다.
※ 프로젝트 설정
minSdk: 26
compileSdk, targetSdk: 35
gradle: 8.14.2
java: JavaVersion.VERSION_1_8 (ver 8)
Android_Study/Calendar by Kizitonwose at calendar-by-kizitonwose-step1 · OhGyong/Android_Study
안드로이드 개발 공부. Contribute to OhGyong/Android_Study development by creating an account on GitHub.
github.com
'Android > Calendar' 카테고리의 다른 글
[Android][Compose] kizitonwose/Calendar, 캘린더 라이브러리 사용기 #3 (3) | 2025.07.14 |
---|---|
[Android] kizitonwose/Calendar, 캘린더 라이브러리 사용기 #2 (6) | 2025.07.11 |