-
Notifications
You must be signed in to change notification settings - Fork 0
Custom Calendar 만들기
현재 우리 프로젝트는 외부 라이브러리를 사용하지 않고 캘린더를 직접 구현하고 있다.
캘린더의 대부분의 기능을 제공받을 수 있는 구글 캘린더 api 같은 경우 편리하게 사용가능하지만 직접 디자인을 커스텀할 수 없고, 모든 기능을 라이브러리를 사용하여 구현하는 것은 프로젝트의 취지에도 맞지 않는다고 생각했기 때문에 사용하지 않기로 결정했다. 이외에 디자인 커스텀을 지원하는 캘린더 라이브러리들을 검토해봤고, 대부분이 recycler view를 기반으로 구현되는 것을 확인하여 우리 팀도 recycler view를 통해 캘린더를 직접 구현해보기로 했다.
달력은 recycler view의 그리드 뷰를 적용하여 레이아웃을 만들고, Local Date Time 라이브러리를 사용하여 날짜 데이터를 채우는 방식으로 구현할 수 있었다.
캘린더에 일정을 어떻게 나타낼 것인지가 그 다음 문제이다. 대부분의 캘린더 어플의 달력에는 일정이 바(bar)형태로 표시된다. 이때, 여러 날 동안 이어지는 일정의 경우, 달력에서도 해당 일정의 바가 여러날을 거쳐 이어진다. 하지만 recycler view를 이용하여 캘린더의 날짜들이 각각의 레이아웃을 가지고 있는 현재 상황에서 어떻게 이것을 구현할 수 있을까?
고민하다가 나름의 묘수를 생각해냈다..!
우선 아래와 같은 데이터 클래스를 작성하였다
data class EventBar(
val id: Int,
val color: Int,
val isStart: Boolean,
val isEnd: Boolean,
val hiddenCount: Int
)
달력의 날짜마다 event bar 리스트를 할당하고, 각 이벤트 바는 아래 그림과 같이 isStart와 isEnd를 기준으로 그려지는 모양이 달라지도록 한다.
이렇게 설정한 후 각 날짜의 이벤트 바 순서만 적절히 배치한다면, recycler view만을 사용해서 여러날에 걸친 일정을 표시할 수 있게 되는 것이다..!
이제 남은 문제는 각 날짜의 일정들을 어떻게 배치해야 하는 지이다. 다른 캘린더 어플들을 관찰한 결과 다음과 같은 규칙을 발견했다.
- 다음날로 이어지는 일정이 오늘 끝나는 일정보다 위에 배치된다.
- 일정 시작 시간이 빠른 일정이 먼저 배치된다.
- 일정이 달력이 표시할 수 있는 양보다 많아지면 축약된다.
- 어제부터 이어지는 일정일 경우 어제에서 bar가 끝난다.
어제부터 이어지는 일정의 배치는 어제의 bar의 배치에 따라 결정되기 때문에 고려할 필요가 없다. 어제와 오늘의 일정을 비교해 나가면서 각 날짜의 eventbar들을 채워나가면 되는 것이다. 로직을 차근차근 따라가보겠다.
-
오늘의 일정들을 어제부터 이어지는 일정과 오늘 시작하는 일정으로 구분한다.
- kotlin collection 확장함수 중 groupBy를 활용하면 특정 기준에 따라서 list를 분류할 수 있다. 코드는 아래와 같다.
- 한주의 첫날이거나 그달의 첫날이면 이어지는 일정을 표시할 수 없기 때문에 모든 일정이 그날 시작한다고 치부했다
val continuity = todayEvents .groupBy { event -> today.dayOfWeek.value != 1 && today.dayOfMonth != 1 && daysInMonth[prev].eventBars.any { it?.id == event.id } }
-
null로 채운 리스트에 어제부터 이어지는 일정을 우선 배치한다.
- 일정 바 사이에 빈공간이 있을 수도 있어 null로 미리 채워두었다.
continuity[true]?.map { event -> val index = daysInMonth[prev].eventBars.indexOfFirst { it?.id == event.id } eventBars[index] = EventBar(...) }
-
나머지 공간에 오늘 시작하는 일정들을 배치한다.
- 이때, 다음날로 이어지는 일정이 오늘 끝나는 일정보다 우선해서 배치된다. 이를 비교해서 정렬하기 위해 Comparator를 구현하였다.
val comparator: (LocalDate) -> Comparator<EventSimple> = { today -> Comparator { event1, event2 -> val endToday1 = event1.endDateTime.toLocalDate() == today val endToday2 = event2.endDateTime.toLocalDate() == today if (endToday1 == endToday2) event1.startDateTime.compareTo(event2.startDateTime) else endToday1.compareTo(endToday2) } } continuity[false] ?.sortedWith(comparator(today)) ?.map { event -> val index = eventBars.indexOf(null) val eventBar = EventBar(...) if (index != -1) eventBars[index] = eventBar else eventBars.add(eventBar) }
-
일정이 정해진 수량을 넘길 경우 마지막 bar를 축약형으로 바꾼다.
if (eventBars.size > maxSize) { eventBars[maxSize-1] = EventBar(...) }
-
배치된 이벤트 바 list를 정해진 수량만큼만 해당 날짜에 배치한다.
val result = eventBars.take(maxSize)
몇가지 생략된 예외 케이스 처리를 하고 나니 원하는 대로 작동되는 것을 확인할 수 있다..!
야호~
아쉬운 점은 recyceler view를 사용하고 있기 때문에 인디케이터에 일정 이름을 텍스트로 표시해줄 수 없다는 점이다. 이건 결국 눈속임에 가까운 방법이라서.. 하지만 알고리즘을 만들어 두었기 때문에 이를 활용해 후에 커스텀 뷰로 달력을 구현하는 방식으로 바꿔볼 수 있을 것 같다~ 더 효율적인 방법이 없을 지도 생각해봐야겠다.
- Week1 - Day01
- Week1 - Day02
- Week1 - Day03
- Week1 - Day04
- Week2 - Day01
- Week2 - Day02
- Week2 - Day03
- Week2 - Day04
- Week3 - Day01
- Week3 - Day02
- Week3 - Day03
- Week3 - Day04
- Week4 - Day01
- Week4 - Day02
- Week4 - Day03
- Week4 - Day04
- Week4 - Day05
- Week5 - Day01
- Week5 - Day02
- Week5 - Day03
- Week5 - Day04
- Week6 - Day01
- Week6 - Day02