관리 메뉴

나만을 위한 블로그

[Android] XML 프로젝트에서 커스텀 달력 라이브러리 구현하는 법 본문

Android

[Android] XML 프로젝트에서 커스텀 달력 라이브러리 구현하는 법

참깨빵위에참깨빵_ 2025. 6. 22. 00:28
728x90
반응형

3년 전에 달력 커스텀하는 글을 쓴 적이 있다. 그러나 현재 이 글에서 사용하는 라이브러리는 최신 안드로이드 프로젝트에선 사용할 수 없다.

 

https://onlyfor-me-blog.tistory.com/437

 

[Android] Material CalendarView 커스텀 사용법 정리

앱에서 달력을 보여주는 방법으로는 안드로이드에서 기본제공하는 캘린더뷰를 쓰는 방법이 있다. 그러나 이 캘린더뷰는 단일 날짜를 지정하는 건 가능하지만 예를 들어 11일~24일까지의 연속된

onlyfor-me-blog.tistory.com

 

그래서 다른 라이브러리를 찾아보고 사용해 보니 나쁘지 않아서 간단한 커스텀을 거친 후에 어떻게 사용하는지를 정리한다.

XML, 컴포즈 둘 다 지원하는 라이브러리라 XML 먼저 다룬 후에 컴포즈에서 어떻게 쓰는지도 확인한다.

라이브러리 의존성은 toml 파일에 아래처럼 설정한다. 그 밑은 앱 gradle에 정의하는 방법이다.

 

[versions]
calendar-view = "2.6.2"

[libraries]
calendar = { module = "com.kizitonwose.calendar:view", version.ref = "calendar-view" }



implementation(libs.calendar)

 

달력을 구현하기 전에 적당한 왼쪽, 오른쪽 아이콘 드로어블을 구해서 이름은 ic_left, ic_right로 설정해 둔다. 이 아이콘들은 달력을 전, 후로 넘기는 처리를 구현하기 위해 필요하다.

이제 달력을 구현해 본다. 이 라이브러리를 써서 달력을 구현하려면 아래가 필요하다.

 

  • 최상단에 표시되는 왼쪽, 오른쪽 아이콘
  • 현재 선택한 연월을 표시하는 텍스트뷰
  • 달력 안에 표시되는 숫자 형태의 날짜들을 표시할 텍스트뷰
  • 날짜들 위에 일~토까지 표시되는 요일들의 텍스트뷰
  • 이 텍스트뷰들을 모두 표현하는 레이아웃

 

아이콘은 이미 구했다고 가정하고 달력 안에 표시되는 숫자 날짜들을 표시할 텍스트뷰부터 만든다. 이름은 calendar_day_layout.xml로 지었다.

 

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/calendarDayText"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:textSize="16sp"
    tools:text="22">

</TextView>

 

그리고 일~토까지 표시되는 요일들을 표시할 텍스트뷰도 만든다. calendar_day_title_text.xml로 지었다.

 

<?xml version="1.0" encoding="utf-8"?>
<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">

</TextView>

 

이제 일~토까지 총 7개의 텍스트뷰가 날짜 위에 표시되어야 하기 때문에 calendar_day_titles_container.xml을 만든다.

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:weightSum="7"
    android:layout_marginBottom="10dp"
    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_custom_calendar.xml을 만들어서 아래처럼 구현한다.

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="8dp">

        <ImageView
            android:id="@+id/btnPrevMonth"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:paddingVertical="12dp"
            android:paddingStart="12dp"
            android:src="@drawable/ic_left" />

        <TextView
            android:id="@+id/tvYearMonth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:textColor="@android:color/black"
            tools:text="2024년 5월" />

        <ImageView
            android:id="@+id/btnNextMonth"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:paddingVertical="12dp"
            android:paddingEnd="12dp"
            android:src="@drawable/ic_right" />
    </LinearLayout>

    <include
        android:id="@+id/titlesContainer"
        layout="@layout/calendar_day_titles_container" />

    <com.kizitonwose.calendar.view.CalendarView
        android:id="@+id/mcCustom"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:cv_dayViewResource="@layout/calendar_day_layout"
        app:cv_monthHeaderResource="@layout/calendar_day_titles_container" />

</LinearLayout>

 

그리고 이걸 사용하는 MyCalendar 파일을 만든다. import에서 에러가 발생할텐데 본인 프로젝트에 맞게 적절히 수정해주면 될 것이다.

 

import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.view.children
import com.example.regacyviewexample.databinding.CalendarDayLayoutBinding
import com.example.regacyviewexample.databinding.ViewCustomCalendarBinding
import com.kizitonwose.calendar.core.CalendarDay
import com.kizitonwose.calendar.core.CalendarMonth
import com.kizitonwose.calendar.core.DayPosition
import com.kizitonwose.calendar.core.daysOfWeek
import com.kizitonwose.calendar.view.MonthDayBinder
import com.kizitonwose.calendar.view.MonthHeaderFooterBinder
import com.kizitonwose.calendar.view.ViewContainer
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.format.TextStyle
import java.util.Locale

class MyCalendar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    private val TAG = this::class.simpleName
    private val binding: ViewCustomCalendarBinding =
        ViewCustomCalendarBinding.inflate(LayoutInflater.from(context), this, true)

    private var selectedDate: LocalDate? = null
    private var currentMonth = YearMonth.now()

    private var onDateSelectedListener: ((String) -> Unit)? = null

    fun setOnDateSelectedListener(listener: (String) -> Unit) {
        onDateSelectedListener = listener
    }

    // 선택된 날짜를 "yyyy년 MM월 dd일" 형태로 반환
    // 달력 클릭 후 받는 연월일 형태를 수정하려면 이 함수를 수정
    fun getSelectedDateFormatted(): String {
        return selectedDate?.let {
            "${it.year}.${it.monthValue}.${it.dayOfMonth}"
        } ?: "${currentMonth.year}.${currentMonth.monthValue}"
    }

    init {
        setupCalendar()
    }

    private fun setupCalendar() {
        with(binding) {
            // 현재 연월 표시
            updateYearMonthText()

            // 이전 달 버튼(<) 클릭 리스너
            btnPrevMonth.setOnClickListener {
                currentMonth = currentMonth.minusMonths(1)
                mcCustom.smoothScrollToMonth(currentMonth)
                updateYearMonthText()
            }

            // 다음 달 버튼(>) 클릭 리스너
            btnNextMonth.setOnClickListener {
                currentMonth = currentMonth.plusMonths(1)
                mcCustom.smoothScrollToMonth(currentMonth)
                updateYearMonthText()
            }

            // 오늘 날짜 이전, 이후 연월은 100개월 전까지 표시
            val startMonth = currentMonth.minusMonths(100)
            val endMonth = currentMonth.plusMonths(100)

            // 지정된 첫 번째 요일이 시작 위치에 오는 주간 요일 값
            // 실행 시 일요일이 먼저 표시됨
            val daysOfWeek: List<DayOfWeek> = daysOfWeek()

            mcCustom.setup(startMonth, endMonth, daysOfWeek.first())
            mcCustom.scrollToMonth(currentMonth)
            // 달력 스크롤 시
            mcCustom.monthScrollListener = { month ->
                Log.d(TAG, "## [스크롤 리스너] mouthScrollListener: $month")
                currentMonth = month.yearMonth
                updateYearMonthText()
            }

            // 일~토 텍스트가 표시되는 상단 뷰
            mcCustom.monthHeaderBinder = object : MonthHeaderFooterBinder<MonthViewContainer> {
                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
                                
                                // 일요일은 빨간색, 토요일은 파란색으로 한글 글자색 설정
                                // 현재 코드에서 이렇게 설정해도 이번 달 외의 날짜들은 회색으로 표시된다
                                when (dayOfWeek) {
                                    DayOfWeek.SUNDAY -> textView.setTextColor(Color.RED)
                                    DayOfWeek.SATURDAY -> textView.setTextColor(Color.BLUE)
                                    else -> textView.setTextColor(Color.BLACK)
                                }
                            }
                    }
                }
            }

            // 날짜가 표시되는 뷰
            mcCustom.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()

                    // 오늘 날짜 가져오기
                    val today = LocalDate.now()
                    val isFutureDate = data.date.isAfter(today)

                    // 텍스트 색상 설정
                    when {
                        isFutureDate -> {
                            // 미래 날짜는 항상 회색으로 표시
                            container.textView.setTextColor(Color.GRAY)
                        }
                        data.position == DayPosition.MonthDate -> {
                            // 현재 월에 속한 과거 또는 오늘 날짜는 요일에 따라 색상 설정
                            when (data.date.dayOfWeek) {
                                DayOfWeek.SUNDAY -> container.textView.setTextColor(Color.RED)
                                DayOfWeek.SATURDAY -> container.textView.setTextColor(Color.BLUE)
                                else -> container.textView.setTextColor(Color.BLACK)
                            }
                        }
                        else -> {
                            // 이전/다음 달의 날짜는 회색
                            container.textView.setTextColor(Color.GRAY)
                        }
                    }

                    container.textView.background = null

                    // 선택된 날짜 스타일 적용 (미래 날짜가 아닌 경우만)
                    if (selectedDate == data.date && !isFutureDate) {
                        // 원형 배경 설정
                        container.textView.background = GradientDrawable().apply {
                            shape = GradientDrawable.OVAL
                            setColor(Color.GREEN)
                        }

                        // 선택된 날짜는 흰색 텍스트
                        container.textView.setTextColor(Color.WHITE)
                        container.textView.gravity = Gravity.CENTER
                    }

                    // 날짜 클릭 리스너 - 미래 날짜는 선택 불가
                    container.textView.setOnClickListener {
                        // 현재 월에 속한 과거 또는 오늘 날짜만 선택 가능
                        if (data.position == DayPosition.MonthDate && !isFutureDate) {
                            if (selectedDate != data.date) {
                                val oldDate = selectedDate
                                selectedDate = data.date

                                // 이전 선택된 날짜 갱신
                                oldDate?.let { date ->
                                    mcCustom.notifyDateChanged(date)
                                }

                                // 새로 선택된 날짜 갱신 후 콜백에 전달
                                mcCustom.notifyDateChanged(data.date)
                                onDateSelectedListener?.invoke(getSelectedDateFormatted())
                            }
                        }
                    }
                }
            }
        }
    }

    private fun updateYearMonthText() {
        binding.tvYearMonth.text = "${currentMonth.year}년 ${currentMonth.monthValue}월"
    }
}

class DayViewContainer(view: View) : ViewContainer(view) {
    val textView = CalendarDayLayoutBinding.bind(view).calendarDayText
}

class MonthViewContainer(view: View) : ViewContainer(view) {
    val titlesContainer = view as ViewGroup
}

 

이제 메인 액티비티에서 가져와 쓰면 된다.

 

<?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"
    android:id="@+id/main"
    android:layout_marginHorizontal="14dp"
    android:background="@color/white"
    tools:context=".presentation.MainActivity">

    <com.example.regacyviewexample.presentation.component.MyCalendar
        android:id="@+id/myCalendar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/tvSelectedDate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:textSize="16sp"
        android:textStyle="bold"
        app:layout_constraintTop_toBottomOf="@id/myCalendar"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        tools:text="선택된 날짜: 2024.06.21" />

</androidx.constraintlayout.widget.ConstraintLayout>
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.regacyviewexample.R
import com.example.regacyviewexample.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val TAG = this::class.simpleName
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }

        setupCalendar()

    }

    private fun setupCalendar() {
        // 캘린더에서 날짜가 선택될 때 호출되는 리스너 설정
        with(binding) {
            myCalendar.setOnDateSelectedListener { selectedDate ->
                // 선택된 날짜 표시
                tvSelectedDate.text = "선택된 날짜: $selectedDate"

                // 선택된 날짜에 따른 추가 작업
            }
        }
    }

}

 

이렇게 작성하고 실행하면 일, 토 글자와 그 밑의 날짜들이 각각 빨간색, 파란색으로 보이고 그 외 오늘까지의 날짜들은 검은색, 내일부터의 날짜들은 회색으로 표시되는 달력이 보일 것이다.

 

 

에뮬레이터에서 확인해서 요일이 영어로 표시되지만 실기기에서 확인하면 한글로 보인다.

상단에 "2025년 6월"이라고 표시되고 양 옆으로 프로젝트에 추가한 화살표 아이콘들이 보일 텐데 이걸 클릭하면 이전 달, 다음 달로 넘어가면서 달력이 바뀌는 걸 볼 수 있다.

 

대충 작동은 하니 이제 핵심 파일인 MyCalendar를 확인한다.

 

import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.view.children
import com.example.regacyviewexample.databinding.CalendarDayLayoutBinding
import com.example.regacyviewexample.databinding.ViewCustomCalendarBinding
import com.kizitonwose.calendar.core.CalendarDay
import com.kizitonwose.calendar.core.CalendarMonth
import com.kizitonwose.calendar.core.DayPosition
import com.kizitonwose.calendar.core.daysOfWeek
import com.kizitonwose.calendar.view.MonthDayBinder
import com.kizitonwose.calendar.view.MonthHeaderFooterBinder
import com.kizitonwose.calendar.view.ViewContainer
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.format.TextStyle
import java.util.Locale

class MyCalendar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    private val TAG = this::class.simpleName
    private val binding: ViewCustomCalendarBinding =
        ViewCustomCalendarBinding.inflate(LayoutInflater.from(context), this, true)

    private var selectedDate: LocalDate? = null
    private var currentMonth = YearMonth.now()

    private var onDateSelectedListener: ((String) -> Unit)? = null

    fun setOnDateSelectedListener(listener: (String) -> Unit) {
        onDateSelectedListener = listener
    }

    // 선택된 날짜를 "yyyy년 MM월 dd일" 형태로 반환
    // 달력 클릭 후 받는 연월일 형태를 수정하려면 이 함수를 수정
    fun getSelectedDateFormatted(): String {
        return selectedDate?.let {
            "${it.year}.${it.monthValue}.${it.dayOfMonth}"
        } ?: "${currentMonth.year}.${currentMonth.monthValue}"
    }

    init {
        setupCalendar()
    }

    private fun setupCalendar() {
        with(binding) {
            // 현재 연월 표시
            updateYearMonthText()

            // 이전 달 버튼(<) 클릭 리스너
            btnPrevMonth.setOnClickListener {
                currentMonth = currentMonth.minusMonths(1)
                mcCustom.smoothScrollToMonth(currentMonth)
                updateYearMonthText()
            }

            // 다음 달 버튼(>) 클릭 리스너
            btnNextMonth.setOnClickListener {
                currentMonth = currentMonth.plusMonths(1)
                mcCustom.smoothScrollToMonth(currentMonth)
                updateYearMonthText()
            }

            // 오늘 날짜 이전, 이후 연월은 100개월 전까지 표시
            val startMonth = currentMonth.minusMonths(100)
            val endMonth = currentMonth.plusMonths(100)

            // 지정된 첫 번째 요일이 시작 위치에 오는 주간 요일 값
            // 실행 시 일요일이 먼저 표시됨
            val daysOfWeek: List<DayOfWeek> = daysOfWeek()

            mcCustom.setup(startMonth, endMonth, daysOfWeek.first())
            mcCustom.scrollToMonth(currentMonth)
            // 달력 스크롤 시
            mcCustom.monthScrollListener = { month ->
                Log.d(TAG, "## [스크롤 리스너] mouthScrollListener: $month")
                currentMonth = month.yearMonth
                updateYearMonthText()
            }

            // 일~토 텍스트가 표시되는 상단 뷰
            mcCustom.monthHeaderBinder = object : MonthHeaderFooterBinder<MonthViewContainer> {
                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
                                
                                // 일요일은 빨간색, 토요일은 파란색으로 한글 글자색 설정
                                // 현재 코드에서 이렇게 설정해도 이번 달 외의 날짜들은 회색으로 표시된다
                                when (dayOfWeek) {
                                    DayOfWeek.SUNDAY -> textView.setTextColor(Color.RED)
                                    DayOfWeek.SATURDAY -> textView.setTextColor(Color.BLUE)
                                    else -> textView.setTextColor(Color.BLACK)
                                }
                            }
                    }
                }
            }

            // 날짜가 표시되는 뷰
            mcCustom.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()

                    // 오늘 날짜 가져오기
                    val today = LocalDate.now()
                    val isFutureDate = data.date.isAfter(today)

                    // 텍스트 색상 설정
                    when {
                        isFutureDate -> {
                            // 미래 날짜는 항상 회색으로 표시
                            container.textView.setTextColor(Color.GRAY)
                        }
                        data.position == DayPosition.MonthDate -> {
                            // 현재 월에 속한 과거 또는 오늘 날짜는 요일에 따라 색상 설정
                            when (data.date.dayOfWeek) {
                                DayOfWeek.SUNDAY -> container.textView.setTextColor(Color.RED)
                                DayOfWeek.SATURDAY -> container.textView.setTextColor(Color.BLUE)
                                else -> container.textView.setTextColor(Color.BLACK)
                            }
                        }
                        else -> {
                            // 이전/다음 달의 날짜는 회색
                            container.textView.setTextColor(Color.GRAY)
                        }
                    }

                    container.textView.background = null

                    // 선택된 날짜 스타일 적용 (미래 날짜가 아닌 경우만)
                    if (selectedDate == data.date && !isFutureDate) {
                        // 원형 배경 설정
                        container.textView.background = GradientDrawable().apply {
                            shape = GradientDrawable.OVAL
                            setColor(Color.GREEN)
                        }

                        // 선택된 날짜는 흰색 텍스트
                        container.textView.setTextColor(Color.WHITE)
                    }

                    // 날짜 클릭 리스너 - 미래 날짜는 선택 불가
                    container.textView.setOnClickListener {
                        // 현재 월에 속한 과거 또는 오늘 날짜만 선택 가능
                        if (data.position == DayPosition.MonthDate && !isFutureDate) {
                            if (selectedDate != data.date) {
                                val oldDate = selectedDate
                                selectedDate = data.date

                                // 이전 선택된 날짜 갱신
                                oldDate?.let { date ->
                                    mcCustom.notifyDateChanged(date)
                                }

                                // 새로 선택된 날짜 갱신 후 콜백에 전달
                                mcCustom.notifyDateChanged(data.date)
                                onDateSelectedListener?.invoke(getSelectedDateFormatted())
                            }
                        }
                    }
                }
            }
        }
    }

    private fun updateYearMonthText() {
        binding.tvYearMonth.text = "${currentMonth.year}년 ${currentMonth.monthValue}월"
    }
}

class DayViewContainer(view: View) : ViewContainer(view) {
    val textView = CalendarDayLayoutBinding.bind(view).calendarDayText
}

class MonthViewContainer(view: View) : ViewContainer(view) {
    val titlesContainer = view as ViewGroup
}

 

날짜를 다루기 위해 LocalDate, YearMonth를 사용한다. 그리고 날짜 클릭 시 어떤 날짜를 선택했는지 알기 위해 onDateSelectedListener 콜백을 정의한 걸 볼 수 있다.

그 밑으로 "yyyy.MM.dd" 형태로 선택한 날짜를 리턴하는 getSelectedDateFormatted()가 보인다. api에 넘겨야 하는 값의 형태가 다르거나 UI에서 사용해야 하는 날짜의 형태가 다르다면 이 함수의 리턴값을 수정하면 된다. 굳이 액티비티 / 프래그먼트에서 수정할 필요가 절대 없다. 날짜 가공을 2번 하게 된다.

 

클래스 하단의 DayViewContainer, MonthViewContainer는 각각 일~토, 날짜를 표시하기 위한 클래스다.

문서에선 각 날짜 셀의 뷰홀더로 작동하는 View 컨테이너를 만들며 생성자로 전달되는 view는 앞서 만든 calendar_day_layout.xml과 calendar_day_titles_container를 각각 사용한다.

MonthViewContainer는 클래스 안에서 어떤 xml도 쓰지 않는데 어떻게 calendar_day_titles_container를 사용하는지 궁금할 수 있다. view_custom_calendar.xml의 mcCustom을 보면 확인할 수 있다.

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="8dp">

        <ImageView
            android:id="@+id/btnPrevMonth"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:paddingVertical="12dp"
            android:paddingStart="12dp"
            android:src="@drawable/ic_left" />

        <TextView
            android:id="@+id/tvYearMonth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:textColor="@android:color/black"
            tools:text="2024년 5월" />

        <ImageView
            android:id="@+id/btnNextMonth"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:paddingVertical="12dp"
            android:paddingEnd="12dp"
            android:src="@drawable/ic_right" />
    </LinearLayout>

    <include
        android:id="@+id/titlesContainer"
        layout="@layout/calendar_day_titles_container" />

    <com.kizitonwose.calendar.view.CalendarView
        android:id="@+id/mcCustom"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:cv_dayViewResource="@layout/calendar_day_layout"
        app:cv_monthHeaderResource="@layout/calendar_day_titles_container" />

</LinearLayout>

 

calendar_day_titles_container.xml을 xml에서 이미 inflate해서 create()의 view 매개변수로 전달하기 때문에 MyCalendar에서만 쓰지 않을 뿐이다.

 

날짜 클릭 시 밝은 녹색 원이 생기는데 이렇게 만드는 코드는 MonthDayBinder의 함수 구현 중 bind()에서 확인할 수 있다.

 

if (selectedDate == data.date && !isFutureDate) {
    // 원형 배경 설정
    container.textView.background = GradientDrawable().apply {
        shape = GradientDrawable.OVAL
        setColor(Color.GREEN)
    }

    // 선택된 날짜는 흰색 텍스트
    container.textView.setTextColor(Color.WHITE)
}

 

추가로 이 코드 앞에서 container.textView.background 프로퍼티를 null로 설정하는데, 이 코드가 없으면 연속해서 다른 날짜를 선택할 때 앞서 선택한 날짜의 배경에 표시되는 녹색 원이 사라지지 않고 그대로 남아있다. 가장 마지막에 선택한 날짜에만 배경을 바꿔서 표시하고 싶다면 null 처리 코드를 없애지 말고 남겨놔야 한다.

그 외에는 본인에게 필요한 UI 스펙에 따라 수정하면 된다. 추가로 이 라이브러리는 내부적으로 리사이클러뷰를 쓰기 때문에 notifyDateChanged()라는 익숙한 이름의 함수를 제공하고 있다.

 

    /**
     * Notify the calendar to reload the cell for this [CalendarDay]
     * This causes [MonthDayBinder.bind] to be called with the [ViewContainer]
     * at this position. Use this to reload a date cell on the Calendar.
     */
    public fun notifyDayChanged(day: CalendarDay) {
        calendarAdapter.reloadDay(day)
    }

    /**
     * Shortcut for [notifyDayChanged] with a [LocalDate] instance.
     */
    @JvmOverloads
    public fun notifyDateChanged(date: LocalDate, position: DayPosition = DayPosition.MonthDate) {
        notifyDayChanged(CalendarDay(date, position))
    }

 

reloadDay()는 라이브러리에 내장된 MonthCalendarAdapter에 정의돼 있다. 역시 익숙한 구현이다.

 

fun reloadDay(vararg day: CalendarDay) {
    day.forEach { day ->
        val position = getAdapterPosition(day)
        if (position != NO_INDEX) {
            notifyItemChanged(position, day)
        }
    }
}

 

그 외에는 특별한 코드는 없다. 추가 커스텀이 필요하다면 문서를 보면서 본인에게 맞춰 커스텀한다.

 

https://github.com/kizitonwose/Calendar/blob/main/docs/View.md

 

Calendar/docs/View.md at main · kizitonwose/Calendar

A highly customizable calendar view and compose library for Android and Kotlin Multiplatform. - kizitonwose/Calendar

github.com

 

반응형
Comments