ViewPager2로 달력을 만들어보자

장달진·2021년 4월 18일
0

ViewPagerCalendar

목록 보기
1/1

업무 중 달력이 필요해서 라이브러리를 가져다가 썼는데 속도는 괜찮지만 각 셀마다 가로세로 비율 조절이 쉽지 않고 했다쳐도 layout measure 타이밍에 따른 버그가 있어 한번 직접 만들어보자고 마음 먹었다

단일 CustomView를 통하여 만들라고 하였지만 애니메이션 코드 관리가 엄두가 나지 않아서 ViewPager를 활용하여 애니메이션과 화면전환을 기본으로 넘기고 간단한 CustomView만 가지고 만들기로 결심!

구글링을 한 결과 대단하신 분이 만든 예제가 있었고
[참고 예제] => https://leveloper.tistory.com/174
해당 예제를 참고하여 재해석 하였습니다.

CalendarViewAdapter.kt

class CalendarViewAdapter : FragmentStateAdapter {

    companion object {
        const val START_POSITION = Int.MAX_VALUE / 2
    }

    constructor(fragmentActivity: FragmentActivity) : super(fragmentActivity)
    constructor(fragment: Fragment) : super(fragment)
    constructor(fragmentManager: FragmentManager, lifecycle: Lifecycle) : super(
        fragmentManager,
        lifecycle
    )


    override fun getItemCount(): Int = Int.MAX_VALUE
    override fun getItemId(position: Int): Long {
        val cal = Calendar.getInstance()
        var currentYear = cal.get(Calendar.YEAR)
        var currentMonth = cal.get(Calendar.MONTH) + 1


        val move = position - START_POSITION
        val bias = if(move < 0) -1 else 1

        val moveYear = abs(move) / 12 * bias
        val moveMonth = abs(move) % 12 * bias

        currentYear += moveYear
        when {
            (currentMonth + moveMonth) < 1  -> {
                currentMonth = 12 + (currentMonth + moveMonth)
                currentYear--
            }
            (currentMonth + moveMonth) > 12 ->  {
                currentMonth = (currentMonth + moveMonth) - 12
                currentYear++
            }
            else -> {
                currentMonth = (currentMonth + moveMonth)
            }
        }

        return (currentYear*100 + currentMonth).toLong()
    }

    override fun containsItem(itemId: Long): Boolean = 200000L < itemId && itemId < 210001L


    override fun createFragment(position: Int): Fragment {
        val itemId = getItemId(position)
        return CalendarFragment().apply {
            arguments = Bundle().apply {
                putLong("year" , itemId / 100L)
                putLong("month" , itemId % 100L)
            }
        }
    }
    
}

연월 계산은 사실 Calendar에 MONTH 값만 입력하면 자동으로 계산이 됩니다만 도전정신에 이끌려 직접 작성해봤습니다 :)

  • ViewPager의 각 Page의 ID는 YYYYMM으로 구별했습니다. 또한 연월 값을 Fragment로 넘겨 해당 연월을 계산하도록 했습니다.
  • 범위는 200000 < YYYYMM < 210001로 2000년 1월부터 2099년 9월까지 정도로 한정했습니다.

CalendarFragment.kt

class CalendarFragment : Fragment() {

    private lateinit var binding : FragmentCalendarBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentCalendarBinding.inflate(inflater , container , false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val year = requireArguments().getLong("year")
        val month = requireArguments().getLong("month")
        binding.calendarView.setYearMonth(year.toInt() , month.toInt())
    }
}
  • 연월 값을 그대로 CustomView로 넘겨줍니다

CalendarDayView.kt

class CalendarDayView : View {

    private val dayRects = List<Rect>(42) { _ -> Rect()}
    private val dayPaints = List<Paint>(42) { _ -> Paint().apply {
        color = when(Random.nextInt(0 until 3)) {
            0 -> Color.RED
            1 -> Color.BLUE
            else -> Color.GREEN
        }
        textSize = 30f
    }}
    private val dates = MutableList<Int>(42){ i -> -1}

    private var year = Calendar.getInstance().get(Calendar.YEAR)
    private var month = Calendar.getInstance().get(Calendar.MONTH) + 1

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)

        val widthAspect = w / 7
        val heightAspect = h / 6

        var stackWidth = 0
        var stackHeight = 0
        for(i in 0 until 6) {
            stackWidth = 0
            for(j in 0 until 7) {
                dayRects[i * 7 + j].set(stackWidth , stackHeight , stackWidth + widthAspect , stackHeight + heightAspect )
                stackWidth += widthAspect
            }
            stackHeight += heightAspect
        }
    }


    fun setYearMonth(year : Int , month : Int) {
        this.year = year
        this.month = month

        val cal = Calendar.getInstance()
        cal.set(Calendar.YEAR , year)
        cal.set(Calendar.MONTH , month)
        cal.set(Calendar.DATE , 0)
        val dayOfMonth = cal.get(Calendar.DAY_OF_MONTH)
        cal.set(Calendar.DATE , 1)
        val dayOfWeek = cal.get(Calendar.DAY_OF_WEEK)

        cal.set(Calendar.MONTH , month -1)
        cal.set(Calendar.DATE , 0)
        var prevMonthLastDate = cal.get(Calendar.DAY_OF_MONTH)

        for(i in dayOfWeek-2 downTo 0) {
            dates[i] = prevMonthLastDate--;
            dayPaints[i].color = Color.GRAY
        }

        var date = 1
        for(i in dayOfWeek-1 until dayOfWeek+dayOfMonth-1) {
            dates[i] = date++
            dayPaints[i].color = when {
                (i % 7 == 0) -> Color.RED
                (i % 7 == 6) -> Color.BLUE
                else -> Color.BLACK
            }
        }

        var nextMonthDate = 1
        for(i in dayOfWeek+dayOfMonth-1 until 42) {
            dates[i] = nextMonthDate++
            dayPaints[i].color = Color.GRAY
        }


        invalidate()
    }

    override fun onDraw(canvas: Canvas?) {
        canvas?.let {
            for(i in 0 until 6) {
                for(j in 0 until 7) {
                    //it.drawRect(dayRects[i* 7 + j] , dayPaints[i * 7 + j])
                    it.drawText(
                        dates[i * 7 + j].toString() ,
                        dayRects[i * 7 + j].left.toFloat() ,
                        dayRects[i * 7 + j].bottom.toFloat(),
                        dayPaints[i * 7 + j]
                    )
                }
            }
        }
    }
}
  • 월 페이지 마다 보여줄 수 있는 최대치가 6주 * 7일해서 42칸입니다. 예시로 윈도우10의 달력입니다.
  • setYearMonth 42개의 Rect사이즈와 날짜를 계산합니다.
  1. 이번 달의 끝 날짜와 시작 요일을 기준으로 이번 달 날짜를 채웁니다.
  2. 이전 달의 마지막 날을 가져와서 앞쪽 채워지지 않은 부분 부터 채워줍니다. 아무리 많이 채워도 14칸이 넘어갈 수 없습니다. (42칸 - 28일 = 14칸) 그러므로 마지막날부터 1씩 빼서 넣어줍니다.
  3. 아무리 많이 채워도 14칸 이상이 채워질 수 없으니 다음 달의 마지막 날을 넘어가지 않으니 dates 배열 뒤쪽을 1부터 채워줍니다.
  • setYearMonth 토요일은 파랑색으로 칠해줍시다.
  • setYearMonth 일요일은 빨간색으로 칠해줍시다.
  • setYearMonth 마지막에 반드시 invalidate() 호출을 잊지맙시다.

2021년 4월

2021년 5월

profile
행복을 추구하는 개발자

0개의 댓글