From df831974d67a36430d12a8613e70bf9ebe1a3a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=A7=84=EC=9A=B0?= <85734140+jinuemong@users.noreply.github.com> Date: Fri, 2 Feb 2024 20:42:21 +0900 Subject: [PATCH] =?UTF-8?q?[Setting]=20=20=EC=BA=98=EB=A6=B0=EB=8D=94=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20(#3?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feat]: 캘린더 컴포넌트 구현 * [Fix]: 잘못된 기능 수정 * [Feat]: 달 변경 시 날짜 데이터 초기화 기능 적용 --- .../view/calendar/CalendarColorProperties.kt | 10 + .../common/view/calendar/CalendarComponent.kt | 240 ++++++++++++++++++ .../common/view/calendar/CalendarConfig.kt | 85 +++++++ .../common/view/calendar/CalendarDay.kt | 6 + .../common/view/calendar/CalendarDayType.kt | 8 + 5 files changed, 349 insertions(+) create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarColorProperties.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarComponent.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarConfig.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarDay.kt create mode 100644 presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarDayType.kt diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarColorProperties.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarColorProperties.kt new file mode 100644 index 00000000..1c447dc2 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarColorProperties.kt @@ -0,0 +1,10 @@ +package ac.dnd.bookkeeping.android.presentation.common.view.calendar + +import androidx.compose.ui.graphics.Color + +data class CalendarColorProperties( + val otherMonthColor: Color, + val todayBeforeColor: Color, + val todayColor: Color, + val todayAfterColor: Color +) diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarComponent.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarComponent.kt new file mode 100644 index 00000000..4756a0c8 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarComponent.kt @@ -0,0 +1,240 @@ +package ac.dnd.bookkeeping.android.presentation.common.view.calendar + +import ac.dnd.bookkeeping.android.presentation.R +import ac.dnd.bookkeeping.android.presentation.common.theme.Body1 +import ac.dnd.bookkeeping.android.presentation.common.theme.Body2 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray000 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray300 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray600 +import ac.dnd.bookkeeping.android.presentation.common.theme.Gray700 +import ac.dnd.bookkeeping.android.presentation.common.theme.Primary4 +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun CalendarComponent( + modifier: Modifier = Modifier, + selectedYear: Int = 0, + selectedMonth: Int = 0, + selectedDay: Int = -1, + verticalSpace: Dp = 0.dp, + backgroundImageSize: Dp = 30.dp, + calendarConfig: CalendarConfig, + calendarColorProperties: CalendarColorProperties = CalendarColorProperties( + otherMonthColor = Color.Transparent, + todayBeforeColor = Gray700, + todayColor = Gray600, + todayAfterColor = Gray700 + ), + unClickableDays: Set = setOf(CalendarDayType.AFTER_TODAY), + onDaySelect: (Int) -> Unit, + itemContent: @Composable (dayItem: CalendarDay) -> Unit = { } +) { + val dayItems = remember { mutableStateListOf() } + LaunchedEffect(selectedYear, selectedMonth) { + dayItems.clear() + dayItems.addAll(calendarConfig.getCurrentCalendarDate(selectedYear, selectedMonth)) + onDaySelect(calendarConfig.getCalendarDay()) + } + + Column( + modifier = modifier.fillMaxWidth() + ) { + LazyVerticalGrid( + columns = GridCells.Fixed(7), + horizontalArrangement = Arrangement.SpaceBetween, + verticalArrangement = Arrangement.spacedBy(verticalSpace) + ) { + items(calendarConfig.getDayOfWeek()) { + Box( + modifier = Modifier.aspectRatio(1f), + contentAlignment = Alignment.Center + ) { + Text( + text = it, + style = Body2.merge( + color = Gray600, + fontWeight = FontWeight.SemiBold + ) + ) + } + } + items(dayItems) { dayItem -> + val dayItemColor = animateColorAsState( + targetValue = if (selectedDay == dayItem.day && dayItem.dayType != CalendarDayType.OTHER_MONTH) Gray000 + else when (dayItem.dayType) { + CalendarDayType.BEFORE_TODAY -> calendarColorProperties.todayBeforeColor + CalendarDayType.TODAY -> calendarColorProperties.todayAfterColor + CalendarDayType.AFTER_TODAY -> calendarColorProperties.todayAfterColor + CalendarDayType.OTHER_MONTH -> Color.Transparent + }, + label = "text color" + ) + val dayItemBackGroundColor = animateColorAsState( + targetValue = if (dayItem.day == selectedDay && dayItem.dayType != CalendarDayType.OTHER_MONTH) Primary4 + else if (dayItem.dayType == CalendarDayType.TODAY) Gray300 + else Color.Transparent, + label = "text bacgound color" + ) + + Column( + modifier = Modifier + .clickable { + if (!unClickableDays.contains(dayItem.dayType) && dayItem.dayType != CalendarDayType.OTHER_MONTH) + onDaySelect(dayItem.day) + }, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier.aspectRatio(1f), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(backgroundImageSize) + .background( + color = dayItemBackGroundColor.value, + shape = CircleShape + ) + ) + Text( + text = dayItem.day.toString(), + style = Body1.merge( + color = dayItemColor.value, + fontWeight = FontWeight.SemiBold + ) + ) + } + if (dayItem.dayType != CalendarDayType.OTHER_MONTH) { + itemContent(dayItem) + } + } + } + } + } +} + +@Preview +@Composable +fun CalendarWithIconPreview() { + CalendarComponent( + modifier = Modifier + .background(Color.White) + .padding(horizontal = 20.dp), + calendarConfig = CalendarConfig(), + selectedYear = 2023, + selectedMonth = 12, + onDaySelect = {} + ) { _ -> + Image( + painter = painterResource(R.drawable.ic_check_circle), + contentDescription = null, + modifier = Modifier.size(5.dp) + ) + } +} + +@Preview +@Composable +fun CalendarSelectedPreview() { + val selectedDay by remember { mutableIntStateOf(10) } + CalendarComponent( + modifier = Modifier + .background(Color.White) + .padding(horizontal = 20.dp), + selectedYear = 2024, + selectedMonth = 2, + calendarConfig = CalendarConfig(), + selectedDay = selectedDay, + onDaySelect = {} + ) +} + +@Preview +@Composable +fun CalendarBottomSheetPreview() { + val calendarConfig = CalendarConfig() + var selectedYear by remember { mutableIntStateOf(calendarConfig.getCalendarYear()) } + var selectedMonth by remember { mutableIntStateOf(calendarConfig.getCalendarMonth()) } + var selectedDay by remember { mutableIntStateOf(calendarConfig.getCalendarDay()) } + Column( + modifier = Modifier.background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(R.drawable.ic_chevron_left), + contentDescription = null, + modifier = Modifier.clickable { + if (selectedMonth > 1) { + selectedMonth -= 1 + } else { + selectedMonth = 12 + selectedYear -= 1 + } + } + ) + Text( + text = "$selectedYear-$selectedMonth", + fontSize = 20.sp, + color = Color.Black, + modifier = Modifier + .padding(vertical = 10.dp) + ) + Image( + painter = painterResource(R.drawable.ic_chevron_right), + contentDescription = null, + modifier = Modifier.clickable { + if (selectedMonth < 12) { + selectedMonth += 1 + } else { + selectedMonth = 1 + selectedYear += 1 + } + } + ) + } + CalendarComponent( + modifier = Modifier.padding(36.dp), + calendarConfig = calendarConfig, + selectedYear = selectedYear, + selectedMonth = selectedMonth, + selectedDay = selectedDay, + onDaySelect = { + selectedDay = it + }, + unClickableDays = setOf() + ) + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarConfig.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarConfig.kt new file mode 100644 index 00000000..c2860e63 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarConfig.kt @@ -0,0 +1,85 @@ +package ac.dnd.bookkeeping.android.presentation.common.view.calendar + +import java.util.Calendar + +class CalendarConfig { + private val calendar: Calendar = Calendar.getInstance() + private var maxDate = 0 + private var todayDay = calendar.get(Calendar.DAY_OF_MONTH) + + fun getCurrentCalendarDate( + year: Int, + month: Int + ): List { + calendar.apply { + set(Calendar.YEAR, year) + set(Calendar.MONTH, month - 1) + maxDate = getActualMaximum(Calendar.DATE) + } + val dataSet = ArrayList() + makeMonthDate(dataSet) + return dataSet + } + + fun getCalendarYear() = calendar.get(Calendar.YEAR) + fun getCalendarMonth() = calendar.get(Calendar.MONTH)+1 + fun getCalendarDay() = todayDay + fun getDayOfWeek() = DAY_OF_WEEK + + private fun makeMonthDate(dataSet: ArrayList) { + calendar.set(Calendar.DATE, 1) + todayDay = 0 + val prevTail = calendar.get(Calendar.DAY_OF_WEEK) - 1 + makeBeforeDays(prevTail, dataSet) + makeCurrentDays(dataSet) + val nextHead = LOW_OF_CALENDAR * WEEK_DAY_COUNT - (prevTail + maxDate) + makeNextHeadDays(nextHead, dataSet) + } + + private fun makeBeforeDays( + prevTail: Int, + dataSet: ArrayList, + ) { + var maxOffsetDate = maxDate - prevTail + for (i in 1..prevTail) { + dataSet.add( + CalendarDay( + day = ++maxOffsetDate, + dayType = CalendarDayType.OTHER_MONTH + ) + ) + } + } + + private fun makeCurrentDays(dataSet: ArrayList) { + val todayCalendar = Calendar.getInstance() + val calenderDate = todayCalendar.get(Calendar.DATE) + val todayDate = todayCalendar.get(Calendar.YEAR) * 12 + todayCalendar.get(Calendar.MONTH) + val currentDate = calendar.get(Calendar.YEAR) * 12 + calendar.get(Calendar.MONTH) + var currentDayType = + if (todayDate < currentDate) CalendarDayType.AFTER_TODAY else CalendarDayType.BEFORE_TODAY + + for (i in 1..maxDate) { + if (todayDate == currentDate && i == calenderDate) { + todayDay = i + dataSet.add(CalendarDay(i, CalendarDayType.TODAY)) + currentDayType = CalendarDayType.AFTER_TODAY + } else { + dataSet.add(CalendarDay(i, currentDayType)) + } + } + } + + private fun makeNextHeadDays( + nextHead: Int, + dataSet: ArrayList + ) { + for (i in 1..nextHead) dataSet.add(CalendarDay(i, CalendarDayType.OTHER_MONTH)) + } + + companion object { + private val DAY_OF_WEEK = listOf("일", "월", "화", "수", "목", "금", "토") + const val WEEK_DAY_COUNT = 7 + const val LOW_OF_CALENDAR = 6 + } +} diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarDay.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarDay.kt new file mode 100644 index 00000000..85e3df14 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarDay.kt @@ -0,0 +1,6 @@ +package ac.dnd.bookkeeping.android.presentation.common.view.calendar + +data class CalendarDay( + val day: Int, + val dayType: CalendarDayType +) diff --git a/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarDayType.kt b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarDayType.kt new file mode 100644 index 00000000..49242ad1 --- /dev/null +++ b/presentation/src/main/kotlin/ac/dnd/bookkeeping/android/presentation/common/view/calendar/CalendarDayType.kt @@ -0,0 +1,8 @@ +package ac.dnd.bookkeeping.android.presentation.common.view.calendar + +enum class CalendarDayType { + OTHER_MONTH, + BEFORE_TODAY, + TODAY, + AFTER_TODAY, +}