일정을 보여줄수 있는 캘린더를 만들어야해서 커스텀으로 캘린더를 만들게되었다.
맨날 xml로하다가 컴포즈로 하니 막막한,,,
일단 만들어야하는 캘린더는 대충 아래와 같다,,
(완성이미지와 영상 밑 코드는 밑에 있습니다)
이렇게 만들어야 하기에 개발순서를 상단, 중단, 하단으로 하여 상중하 순으로 내려갔다.
상단인 년월이 있으며, 좌우 버튼이있는 부분
/**
* 년월 설정 하는부분
* **/
@Composable
fun DrawYearDate(
targetDate : LocalDate,
onPreviousMonthClick : () -> Unit,
onNextMonthClick : () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "${targetDate.year}년 ${targetDate.monthValue}월",
style = MaterialTheme.typography.titleLarge,
color = LocalColors.current.textPrimary,
modifier = Modifier.weight(1f)
)
//down
CampIconButton(
kr.kwon.designsystem.R.drawable.icon_arrow_left,
LocalColors.current.textPrimary,
Modifier.size(24.dp),
onClick = {
onPreviousMonthClick.invoke()
}
)
Spacer(modifier = Modifier.width(8.dp))
//up
CampIconButton(
kr.kwon.designsystem.R.drawable.icon_arrow_right,
LocalColors.current.textPrimary,
Modifier.size(24.dp),
onClick = {
onNextMonthClick.invoke()
}
)
}
}
위의 코드는 상단부분이며, CampIconButton은 뭐 그냥 공통단으로 만들어 놓은 이미지버튼이다.
다음으로 중단코드
/**
* 요일 적는부분
* **/
@Composable
fun DrawDayOfWeek() {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
val dayOfWeek = listOf("일", "월", "화", "수", "목", "금", "토")
dayOfWeek.forEach {
Text(
text = it,
color = if(it == "토" || it == "일") LocalColors.current.interactive else LocalColors.current.textPrimary,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier
.weight(1f)
.height(24.dp)
.padding(start = 4.dp, top = 4.dp)
)
}
}
}
토, 일은 색상을 변경하여야하기에 위와같이 사용했다. 간단한 코드다.
다음은 하단의 뷰를 그리는 코드
/**
* 날짜 레이아웃 부분
* **/
@Composable
fun CustomDateView(
boxWidth : Dp,
dateStr : String,
state : DateCheckState
) {
Box(
modifier = Modifier.width(boxWidth).height(52.dp)
) {
val textColor = when(state) {
DateCheckState.Before -> LocalColors.current.textDisabled
DateCheckState.Current -> LocalColors.current.textPrimary
DateCheckState.Holiday -> LocalColors.current.interactive
}
Text(
text = if(dateStr.isNotEmpty()) "${dateStr.toConvertDate(DATE_FORMAT).dayOfMonth}" else "",
style = MaterialTheme.typography.labelMedium,
color = textColor,
modifier = Modifier
.fillMaxWidth(1f)
.height(52.dp)
.background(color = Color.Red)
.padding(start = 4.dp, top = 4.dp),
)
}
}
그냥 날짜를 받아서 그리는 부분이라서 이부분 또한 간단하다
이제 상중하를 이용해서 그리는 로직이 들어간 부분이다.
@Composable
fun CampDateRangeCalendarView(
modifier: Modifier = Modifier,
targetDate: LocalDate = LocalDate.now()
){
val insetHeight = 80.dp //년월 높이와 요일 높이 합친값
var rememberedDate by remember { mutableStateOf(targetDate) }
var parentSize by remember { mutableStateOf(IntSize.Zero) }
val density = LocalDensity.current
val boxWidth = with(density) { (parentSize.width / 7).toDp() }
rememberedDate = rememberedDate.withDayOfMonth(1)
Box(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Column(
modifier = modifier.onSizeChanged {
parentSize = it
},
) {
//상단
DrawYearDate(rememberedDate,
{
rememberedDate = rememberedDate.minusMonths(1)
}, {
rememberedDate = rememberedDate.plusMonths(1)
})
//중단
DrawDayOfWeek()
//하단
var rowDate = rememberedDate.minusMonths(1)
rowDate = rowDate.withDayOfMonth(rowDate.lengthOfMonth() - rememberedDate.toBeforeRemainDateCount())
val currentLastDate = rememberedDate.withDayOfMonth(rememberedDate.lengthOfMonth())
val days = (1..35).toList()
days.chunked(7).forEach { week ->
Row(
modifier = Modifier.padding(start = 4.dp, end = 4.dp)
) {
week.forEach { _ ->
CustomDateView(
boxWidth,
if(currentLastDate.isAfter(rowDate) || currentLastDate.equals(rowDate)) rowDate.format(DATE_FORMAT) else "",
when {
rowDate.month != rememberedDate.month -> DateCheckState.Before
rowDate.checkHoliday() -> DateCheckState.Holiday
else -> DateCheckState.Current
}
)
rowDate = rowDate.plusDays(1)
}
}
}
}
DrawCalcSchedule(
insetHeight,
boxWidth,
rememberedDate
)
}
}
상단, 중단은 그냥 그대로 호출한것이고, 하단은 이제 달력을 그리는 세팅부분, 현 달력에서 이전달 날짜를 빈공간에 보여줘야하기에
시작할 마지막 전달의 날짜를 구한다음 달력칸 35개를 돌린다. 여기에서 1주가 7일 이므로 7로 나누어서 Row에 다시 채워 넣어야하는
로직을 사용했다. 그리고 DrawCalcSChedule은 임의로 스케쥴이 있다면 스케쥴을 보여줘야하기에 그리는 부분이다.
/**
* 스케쥴 표시하는 부분
* **/
@Composable
fun DrawCalcSchedule(
insetHeight : Dp,
boxWidth : Dp,
currentDate : LocalDate
) {
val scheduleList = mutableListOf<CalendarSchedule>().apply {
add(CalendarSchedule("20241011", "20241103", "TEST"))
}
val currentFirstWeek = getWeekNumber(currentDate)
scheduleList.forEach { schedule ->
val startDate = schedule.startDate.toConvertDate(DATE_FORMAT)
val endDate = schedule.endDate.toConvertDate(DATE_FORMAT)
if(getWeekNumber(startDate) == getWeekNumber(endDate)) { //같은주에 있다
DrawSchedule(
RoundedCornerShape(8.dp),
boxWidth * startDate.toDrawWidthCount() + 6.dp,
(insetHeight + 35.dp + ((getWeekNumber(startDate) - currentFirstWeek) * 52).dp),
boxWidth * endDate.toDrawWidthCount(),
schedule.scheduleName
)
} else {
val totalWeeks : Int
val lastDate : LocalDate
val firstDate = if(currentDate.monthValue != startDate.monthValue) {
val rowDate = currentDate.minusMonths(1)
rowDate.withDayOfMonth(rowDate.lengthOfMonth() - currentDate.toBeforeRemainDateCount())
} else {
startDate
}
if(currentDate.monthValue != endDate.monthValue) {
lastDate = currentDate.withDayOfMonth(currentDate.lengthOfMonth())
totalWeeks = getWeekNumber(lastDate) - getWeekNumber(firstDate)
} else {
lastDate = endDate
totalWeeks = getWeekNumber(endDate) - getWeekNumber(firstDate)
}
(0..totalWeeks).forEach { index ->
var cornerShape = RoundedCornerShape(0.dp)
val startPadding : Dp
val topPadding : Dp
val width : Dp
when (index) {
0 -> { //처음
startPadding = boxWidth * firstDate.toDrawWidthCount() + 4.dp
topPadding = (insetHeight + 35.dp + ((getWeekNumber(firstDate) - currentFirstWeek) * 52).dp)
cornerShape = if(firstDate == startDate) RoundedCornerShape(topStart = 8.dp, bottomStart = 8.dp) else RoundedCornerShape(topStart = 0.dp, bottomStart = 0.dp)
width = boxWidth * (7 - firstDate.toDrawWidthCount())
}
totalWeeks -> { //끝
startPadding = 4.dp
topPadding = (insetHeight + 35.dp + ((getWeekNumber(lastDate) - currentFirstWeek) * 52).dp)
width = boxWidth * (lastDate.toDrawWidthCount() + 1)
cornerShape = if(lastDate == endDate) RoundedCornerShape(topEnd = 8.dp, bottomEnd = 8.dp) else RoundedCornerShape(topEnd = 0.dp, bottomEnd = 0.dp)
}
else -> { //중간
startPadding = 4.dp
topPadding = (insetHeight + 35.dp + ((getWeekNumber(firstDate) - currentFirstWeek + index) * 52).dp)
width = boxWidth * 7
}
}
DrawSchedule(
cornerShape,
startPadding,
topPadding,
width,
schedule.scheduleName
)
}
}
}
}
설명을 하자면,,,복잡하지만,,,같은주에 있는지,,시작달이 이번달인지 아닌지 체크,,,,끝나는달이 이번달인지 아닌지 체크,,,
현재달력에 표시되어야하는지 체크 등등이 들어가있다.
아래는 데이터 포맷과 유틸로 만들어 놓은것들이다.
data class CalendarSchedule(val startDate : String, val endDate : String, val scheduleName : String)
enum class DateCheckState {
Before, Current, Holiday
}
fun String.toConvertDate(formatter : DateTimeFormatter) : LocalDate {
return LocalDate.parse(this, formatter)
}
fun LocalDate.toBeforeRemainDateCount() : Int {
return when(this.dayOfWeek) {
DayOfWeek.TUESDAY -> 1
DayOfWeek.WEDNESDAY -> 2
DayOfWeek.THURSDAY -> 3
DayOfWeek.FRIDAY -> 4
DayOfWeek.SATURDAY -> 5
else -> 0
}
}
fun LocalDate.checkHoliday() : Boolean {
return this.dayOfWeek == DayOfWeek.SATURDAY || this.dayOfWeek == DayOfWeek.SUNDAY
}
fun getWeekNumber(date: LocalDate): Int {
val weekFields = WeekFields.of(Locale.KOREA)
return date.get(weekFields.weekOfWeekBasedYear())
}
fun LocalDate.toDrawWidthCount() : Int {
return when(this.dayOfWeek) {
DayOfWeek.MONDAY -> 1
DayOfWeek.TUESDAY -> 2
DayOfWeek.WEDNESDAY -> 3
DayOfWeek.THURSDAY -> 4
DayOfWeek.FRIDAY -> 5
DayOfWeek.SATURDAY -> 6
else -> 0
}
}
'프로그램 > Android' 카테고리의 다른 글
1. 안드로이드 Compose 네이버 지도 설정 (0) | 2025.01.03 |
---|---|
안드로이드 scrollview 안에 있는 webview 줌인아웃 처리 (1) | 2024.11.27 |
NFC 비접촉결제 체크/앱 배터리사용량 최적화 해제 (0) | 2024.11.08 |
코루틴을 이용한 비동기 처리방법 (0) | 2024.11.06 |
구글스토어 다국어 스토어설정 (1) | 2024.10.07 |