작업 일지

[Android][Compose] MaterialCalendarView를 Compose에서 사용하기

O_Gyong 2024. 5. 19.

기존 xml로 작업한 PWFB 프로젝트를 Compose로 변경하면서 기존에 구현했던 캘린더 UI를 어떻게 구현해야 할까 고민이 많았다. 캘린더는 MaterialCalendarView를 사용하고 있었는데, 다른 캘린더 라이브러리나 직접 구현하는 것보다는 기존 코드를 덜 수정하는 방향으로 진행하고 싶었다.

 

Compose에는 AndroidView라는 컴포저블이 있는데, Compose에서 아직 사용할 수 없는 UI 요소를 사용하는 경우에 쓰인다. AndroidView 컴포저블을 사용해서 MaterialCalendarView를 사용할 수 있었다.


AndroidView

@Composable
@UiComposable
fun <T : View> AndroidView(
    factory: (Context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
) {
    AndroidView(
        factory = factory,
        modifier = modifier,
        update = update,
        onRelease = NoOpUpdate
    )
}

AndroindView는 매개변수로 factory, modifier, update를 받는다.

◾ factory
factory 블록은 View를 반환하는 블록을 생성하여 View 요소 또는 계층 구조를 Compose UI에 포함할 수 있게 한다.

◾ update
View가 확장되었을 때 호출되는 콜백으로 View의 정보와 상태를 업데이트를 할 수 있다.

◾ modifier
상위 Composable에서 위치를 설정하는 등의 목적으로 사용한다.

AndroidView(
    factory = { MaterialCalendarView(it) },
    modifier = Modifier,
    update = { calendarView ->
        calendarView.setHeaderTextAppearance(R.style.CalendarWidgetHeader) // 연, 월 헤더 스타일
        calendarView.setWeekDayTextAppearance(R.style.CalenderViewWeekCustomText) // 1~12월, 월간 표시
        calendarView.setDateTextAppearance(R.style.CalenderViewDateCustomText) // 일~토, 주간 표시

	// '년' '월'로 표시
        calendarView.setTitleFormatter { day ->
            val inputText = day.date
            val calendarHeaderElements = inputText.toString().split("-")
            val calendarHeaderBuilder = StringBuilder()

            calendarHeaderBuilder.append(calendarHeaderElements[0]).append("년 ")
                .append(calendarHeaderElements[1]).append("월")

            calendarHeaderBuilder.toString()
        }
		...
    }
)

 

factory에 MaterialCalendarView를 전달하고, update에서 해당 view를 사용하여 캘린더의 스타일을 변경했다.

view를 통해 대부분 기존 코드를 유지할 수 있었지만 Decorator가 필요한 수정이 필요했다.


Decorator 활용

        /**
         * themes.xml
         */
        <style name="CalenderViewCustom" parent="Theme.AppCompat">
            <item name="colorAccent">@color/c_31caab3f</item>
            <item name="android:textStyle">bold</item>
        </style>


	/**
         * activity_day.xml
         */
        <com.prolificinteractive.materialcalendarview.MaterialCalendarView
			...
            android:theme="@style/CalenderViewCustom" />

 

MaterialCalendarView에서 날짜를 클릭했을 때 서클의 색 변경은 기존 코드에선 theme를 적용하여 쉽게 적용할 수 있다.

(왼쪽)theme 적용X / (오른쪽) theme 적용O

 

 

하지만 Compose로 변경하면서 xml을 사용할 수 없기 때문에 다른 방법을 적용해야만 했다. MaterialCalendarView는 

Decorator를 사용해서 특정 날짜에 대한 커스텀 스타일을 지정할 수 있다.

 

/**
 * MainActivity
 */
val drawableList: List<Drawable?> = listOf(
    ResourcesCompat.getDrawable(
        this.resources,
        R.drawable.shape_calendar_today,
        null
    ),
    ResourcesCompat.getDrawable(
        this.resources,
        R.drawable.selector_calendar_day,
        null
    )
)

NavHost(
    navController = navController,
    startDestination = if(isFirstInit) SCREEN_NAME else SCREEN_HOME
) {
    composable(route = SCREEN_DAY) { DdayScreen(navController, drawableList) }
    ...
}

MainActivity에서 Decorator로 사용할 drawable 리소스를 List로 묶어 Compose에 전달했다. Compose에서는 drawable 리소스에 접근하지 못해서 해당 과정이 필요했다.

 

/**
 * DdayScreen
 */
@Composable
fun CalendarView(
    dDayViewModel: DdayViewModel,
    datePrefState: MutableState<String>,
    drawableList: List<Drawable?>
) {
    AndroidView(
        factory = { MaterialCalendarView(it) },
        modifier = Modifier,
        update = { calendarView ->
        
            ...
        
            val dayDisableDecorator = DayDisableDecorator(disabledDates, today, Gray.toArgb())
            val todayDecorator = drawableList[0]?.let { TodayDecorator(Yellow40.toArgb(), it) }

            calendarView.addDecorators(dayDisableDecorator, todayDecorator)

            calendarView.setOnDateChangedListener { _, date, _ ->
                calendarView.addDecorators(
                    ClearDecorator(White.toArgb(), date),
                    dayDisableDecorator,
                    todayDecorator,
                    drawableList[1]?.let { SelectDecorator(Yellow40.toArgb(), it, date) }
                )
                ...
            }
        }
    )
}
/**
 * 오늘 날짜 표시 데코
 */
class TodayDecorator(
    private var color: Int,
    private var drawable: Drawable
) :DayViewDecorator {
    private var date = CalendarDay.today()
    override fun shouldDecorate(day: CalendarDay?): Boolean {
        return day?.equals(date)!!
    }
    override fun decorate(view: DayViewFacade?) {
        view?.addSpan(object: ForegroundColorSpan(color){})
        view?.setBackgroundDrawable(drawable)
    }
}

/**
 * 날짜 선택 시 데코
 */
class SelectDecorator(
    private var color: Int,
    private var drawable: Drawable,
    private var date: CalendarDay
) : DayViewDecorator {
    override fun shouldDecorate(day: CalendarDay): Boolean {
        return day == date
    }

    override fun decorate(view: DayViewFacade) {
        view.addSpan(object: ForegroundColorSpan(color){})
        view.setSelectionDrawable(drawable)
    }
}

 

현재 날짜인 경우 setBackgroundDrawable()에 shape drawable,

날짜를 선택했을 때는 setSelectionDrawable()에 select drawable을 전달하면서 해결하였다.

 

 

댓글