사용 의도
왜 달력을 만들었는가?
- 오늘 진행하고 있는 이벤트뿐만 아니라, 특정 날에 무슨 이벤트가 진행했는지를 파악하고 싶어 달력을 구현하여 특정 날짜를 클릭하여 해당 날짜에 진행한 이벤트 리스트를 보여줄 수 있도록 하였다.
학습 내용
RecyclerView와 ListAdapter를 이용하여 Item마다 ViewType을 정해놓고 달력 UI를 구현한다.
- 우리가 달력을 보면 날짜뿐만 아니라 상단에 일월화수목금토를 표시해주는데, 보통 요일과 날짜의 배경색을 다르게 나타내고 있다.
- 따라서 메이플 캘린더의 달력을 배경색을 날짜는 흰색, 요일은 회색으로 출력되도록 하기 위하여 ViewType을 요일(Header)과 날짜(Date)로 구분하고, 날짜에는 Click Listener를 달아줌으로써 클릭 시 이벤트 리스트를 띄워주도록 한다.
적용 방법
먼저 xml에 RecyclerView를 하나 생성한다.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_calendar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/rv_calendar_start"
android:layout_marginTop="@dimen/rv_calendar_top"
android:layout_marginEnd="@dimen/rv_calendar_end"
android:layout_marginBottom="@dimen/rv_calendar_bottom"
android:adapter="@{listAdapter}"
android:elevation="10dp"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_year_calendar"
app:spanCount="7"
app:submitData="@{vm.calendarData}"
tools:listitem="@layout/item_calendar_date" />
- 여기서 adapter와 layoutManager는 xml 내부에서도 설정할 수 있는데, adapter는 코틀린 코드에서 바인딩으로 ListAdapter로 초기화하며 그 ListAdapter는 후술할 CalendarListAdapter가 된다.
- 또한 layoutManager는 2차원 배열 형식으로 달력을 출력할 것이므로 GridLayoutManager로 결정하였다.
- 그리고 spanCount 속성을 7로 정의하여 한 줄에 일월화수목금토 총 7개의 요일에 해당하는 날짜가 출력되도록 한다.
- listitem은 Item의 UI를 결정하는 속성이고 기본적으로 흰색 바탕에 날짜를 출력할 TextView가 존재하고, Click Listener가 달려있다.
- submitData는 Binding Adapter를 사용해서 확장 함수를 정의하여 RecyclerView의 속성을 새로 만들었는데, ListAdapter의 submitList()로 Item List를 갱신하면 되는데 이 Item List에는 요일과 날짜 데이터가 포함된다.
@BindingAdapter("app:submitData")
fun <T, VH : RecyclerView.ViewHolder> RecyclerView.bindItems(items: List<T>) {
val adapter = this.adapter
adapter?.let {
val listAdapter: ListAdapter<T, VH> = it as ListAdapter<T, VH>
listAdapter.submitList(items)
}
}
이제 ListAdapter를 구현해야 한다. 그런데 그 전에 요일 및 날짜에 대한 ViewType과 속성을 정의해야 한다.
- 먼저 캘린더 화면에 대한 UiState를 Sealed Class로 정의한다.
sealed class CalendarUiState(val id: String = UUID.randomUUID().toString()) {
data class CalendarHeader(
val type: DayType,
val name: String,
val backgroundResId: Int = R.drawable.shape_calendar_header
) : CalendarUiState()
data class CalendarDate(
val type: DayType,
val name: String,
val backgroundResId: Int = R.drawable.shape_calendar_date
) : CalendarUiState()
companion object {
const val HEADER_VIEW_TYPE = 1
const val DATE_VIEW_TYPE = 2
}
}
- 요일과 날짜에 대한 요일 타입, 이름, 배경 색상을 정의하는데, 요일 타입은 평일인지, 토요일인지, 일요일인지를 구분하여 글자 색상을 타입별로 다르게 출력하도록 하기 위하여 추가하였다.
- 다음으로 ListAdapter를 구현한다.
class CalendarListAdapter(private val onDateClickListener: OnDateClickListener) :
ListAdapter<CalendarUiState, RecyclerView.ViewHolder>(calendarUiStateDiffUtil) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
HEADER_VIEW_TYPE -> CalendarHeaderViewHolder(
ItemCalendarHeaderBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
else -> CalendarDateViewHolder(
ItemCalendarDateBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
HEADER_VIEW_TYPE -> (holder as CalendarHeaderViewHolder).bind(currentList[position] as CalendarUiState.CalendarHeader)
DATE_VIEW_TYPE -> (holder as CalendarDateViewHolder).bind(
currentList[position] as CalendarUiState.CalendarDate,
onDateClickListener
)
}
}
override fun getItemViewType(position: Int): Int {
return when {
currentList[position] is CalendarUiState.CalendarHeader -> HEADER_VIEW_TYPE
else -> DATE_VIEW_TYPE
}
}
companion object {
val calendarUiStateDiffUtil = object : DiffUtil.ItemCallback<CalendarUiState>() {
override fun areItemsTheSame(oldItem: CalendarUiState, newItem: CalendarUiState) =
(oldItem.id == newItem.id)
override fun areContentsTheSame(oldItem: CalendarUiState, newItem: CalendarUiState) =
(oldItem == newItem)
}
}
class CalendarHeaderViewHolder(private val binding: ItemCalendarHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(header: CalendarUiState.CalendarHeader) {
binding.header = header
}
}
class CalendarDateViewHolder(private val binding: ItemCalendarDateBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(date: CalendarUiState.CalendarDate, clickListener: OnDateClickListener) {
binding.date = date
binding.clickListener = clickListener
}
}
}
- 먼저 DiffUtil을 정의하여 아이템을 id로 구분하는데, UiState의 생성자에서 UUID를 활용한 id 프로퍼티를 정의해두었다.
- ViewHolder는 요일, 날짜에 대한 ViewHolder 2개를 선언하고 데이터를 바인딩하는 bind() 메소드를 정의한다.
- 그리고 onCreateViewHolder와 onBindViewHolder를 재정의하여 ViewType에 따라 다른 ViewHolder를 생성하고 bind() 메소드로 데이터를 바인딩한다.
마지막으로, ClickListener를 정의해야 하는데, 나는 ViewModel이 Click Listener 인터페이스도 상속받도록 하여 ViewModel에서 onClick() 메소드를 재정의하였다.
override fun onClicked(calendarDate: CalendarUiState.CalendarDate) {
val date = calendarDate.name
_specificDate.value = "${_currentYear.value}년 ${_currentMonth.value}월 ${date}일"
val specificDay = _currentYear.value.toString().padStart(4, '0') + "-" + _currentMonth.value.toString().padStart(2, '0') + "-" + date.padStart(2, '0')
viewModelScope.launch {
_calendarUiEvent.emit(CalendarUiEvent.GetEventsOfDate)
val eventListOfDate = async { eventListReader.getEventList(specificDay) }.await()
if (eventListOfDate != null) {
_eventItemsOfDate.value = eventListOfDate.sortedBy { eventItem ->
eventItem.eventExp
}
} else {
_calendarUiEvent.emit(CalendarUiEvent.InternalServerError)
}
}
}
- 날짜 ViewType을 가진 Item을 클릭 시 해당 메소드를 호출하여 그 날에 진행중인 이벤트 리스트를 Firebase에 생성해둔 DB에서 불러올 수 있도록 하였다.
이제 Fragment 내에서 데이터 바인딩으로 ListAdapter를 초기화시키면 된다.
private lateinit var calendarListAdapter: CalendarListAdapter
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initListAdapter()
initRecyclerView()
binding.listAdapter = calendarListAdapter
}
private fun initListAdapter() {
calendarListAdapter = CalendarListAdapter(viewModel)
}
private fun initRecyclerView() {
binding.rvCalendar.setHasFixedSize(false)
}
- ListAdapter의 생성자에 있는 Click Listener 프로퍼티에 ViewModel을 대입한 이유는 아까 언급했듯이 ViewModel이 Click Listener 인터페이스도 상속받도록 하고 ViewModel 내에 onClick() 메소드를 재정의하였기 때문이다.
결과
버튼을 눌러 달력을 변경할 수 있고, 특정 날짜를 클릭하면 이벤트 리스트를 확인할 수 있다.
또한 요일은 배경색이 회색이며, 토요일은 글자색이 파란색, 일요일은 빨간색인 것을 확인할 수 있다.
코드
'개인 프로젝트 > 안드로이드' 카테고리의 다른 글
[메이플 캘린더] ViewPager2와 Custom View로 캐릭터 정보 조회 날짜를 선택하는 달력을 만들어보자 (0) | 2024.04.26 |
---|---|
[메이플 캘린더] 위로 스와이프 시 새로고침 기능을 구현해보자 (0) | 2024.01.24 |
[메이플 캘린더] 다크 모드에 대응해보자 (0) | 2024.01.18 |
[메이플 캘린더] AlarmManager로 메붕이들에게 오늘 끝나는 이벤트가 있음을 알려주자 (0) | 2024.01.17 |
[메이플 캘린더] 메이플 캘린더라는 앱을 만들어보았다! 그런데...? (0) | 2024.01.16 |