본문 바로가기

안드로이드/레이아웃 구현과 활용

[And] 달력 만들기 - recyclerview 활용

반응형

달력을 5번정도 구현했는데, 그때마다 '어떻게 하면 조금 더 효율적일까'를 고민했고 최근에 식물일기에 달력 기능을 추가할때도 고민했습니다. 

혹시 다음에 또 구현하게 된다면 보다 빠른 의사결정을 위해 포스팅합니다. 본 포스팅은 달력을 구현하는데 있어서 방향성을 잡는데 도움이 되도록 작성했습니다.

( Gridelayout으로 만든 시간표 포스팅도 구경하고 가세요 ! )

 

| 달력 레이아웃 구현

달력에는 매번 스와이프 기능을 넣었기 때문에 viewpager는 기본으로 하고, 세부 UI 구현은 크게 3가지로 나눠볼 수 있습니다.

1. Recyclerview로 만듬

2. textview로 하나씩 모두 그림 

     2-1. customView로 만들어서 사용 ( 또는 달력 전용 fragment )

     2-2. 구현하는 엑티비티 xml에 직접 구현

3. 라이브러리 사용 

 

먼저 3번은 제외 합니다. 효율적일 수 있으나 항상 커스텀을 염두에 뒀기 때문에 자체 구현을 했습니다. 

남은 1번과 2번중 달력이 한 화면을 차지하는 비율에 따라 나뉩니다. 전체화면인 경우 Recyclerview를 활용하면 스크롤이 생기기 때문에 효율적이지 못합니다. 따라서 2번을 써야 합니다. 

식물일기에서는 전체화면을 사용하지 않기 때문에 1번을 사용했습니다. ( 첨부된 구현 소스는 1번으로 개발했습니다.

실제 구현 화면 

만약 달력이 전체 화면을 채우는 경우 유지보수 측면에서 효율적인 2-1번 ( customView로 만들어서사용 )을 선호합니다.  

| Viewpager를 활용한 무한 스크롤링 ( infinite scroll ) 

앱내 달력을 보여줄때 좌우로 스와이프 하는 UI를 많이 접할 수 있는데, 무한처럼 보이지만 사실상 무한은 없구 무한인척 하는 유한 스크롤입니다.

adapter에 아이템 갯수를 많이 지정한 후 무한 스크롤 처럼 보이게 하는 방법을 사용했습니다.  


class CalendarItemAdapter(val activity: CalendarActivity) :
    RecyclerView.Adapter<CalendarItemAdapter.ItemViewHolder>() {

    val CALENDAR_MAX_SIZE = 50
    val INIT_POSITION = CALENDAR_MAX_SIZE/2
    
	....

    override fun getItemCount(): Int {
        return CALENDAR_MAX_SIZE
    }

}

위 코드는 총 50개 아이템으로 이뤄어져 있는 Recyclerview Adapter로, 각 아이템은 달별 날짜를 노출하는 화면으로 이루어져 있습니다. 달력 화면이 그려질때(onCreate), 전체/2 으로 viewpager를 이동해서 현재 월에 해당하는 달력을 보여줍니다.

( 코드상 전체 페이지는 50번째고 시작은 25번째입니다.) 

양쪽으로 각 25번씩 이동할 수 있고 끝에 다다를순 있지만 일부 유저들에겐 무한 처럼 보일수 있습니다.

( 앱의 특성에 따라 MAX_SIZE를 조정하면 됩니다. )

 


class CalendarActivity : BaseActivity<CalendarActivityBinding, CalendarVM>() {
	
    private var adapter = CalendarItemAdapter(this)
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        binding.vpCalendar.adapter = adapter
        binding.vpCalendar.setCurrentItem(adapter.INIT_POSITION, false)
		....
    }
}

위 코드는 실제 달력이 구현된 엑티비티로, viewpager2를 사용했고 oncreate에 adapter에서 설정한 INIT_POSITION으로 이동 합니다.

( 25번째, 위 CalendarItemAdapter.kt 참고 ) 

| 달력 뷰 규현

CalendarItemAdapter는 Viewpager2 아이템 어뎁터로 Recyclerview만을 자식뷰로 갖고 있습니다. 실제 홀더 부분은 아래 소스 입니다.

    inner class ItemViewHolder(private val binding: CalendarItemBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(position: Int) {
            binding.rvCalendar.adapter = CalendarDayItemAdapter(activity, getCalendar(position), position == INIT_POSITION)
            binding.executePendingBindings()
        }
    }

calendarDayItemAdapter.kt

해당 파일의 viewholder는 실제 날짜를 그리는데 주요 체크해야할 사항으로는 2가지가 있습니다.

- 1일이 무슨 요일부터 시작하는지

- 달의 마지막 날짜가 언제인지 

날짜와 관련된 작업은 주로 calendar를 활용합니다. 

var calendar = Calendar.getInstance()
calendar.set(year, month, day, hour, min, sec)

var dayWeek = calendar.get(Calendar.DAY_OF_WEEK) (요일, 일요일1 ~ 토요일7)
var lastDay = calendar.getActualMaximum(Calendar.DAY_OF_MONTH) ( 2번째 줄에서 설정한 월의 마지막 일)

set 메소드는 캘린더 변수에 날짜를 셋팅 해줄때 사용하는데 주의점으로 실제 적용하려고 하는 month 값에서 -1을 해야합니다. 

(ex. 2021년 4월 1일을 세팅 할때, calendar.set(2021, 3, 1) ) 

 

 

위 정보들을 활용한다면 달력을 구현하는데 큰 문제는 없을거라고 생각합니다. 구현하시다 궁금하신 부분은 댓글을 남겨주시면 확인하는데로 답변 드리겠습니다.

반응형