본문 바로가기

ANDROID/UI - UX

[안드로이드 | 코틀린] 사용자 정의 커스텀 달력 만들고 ViewPager에 접목하기

커스텀 달력 구현을 위해 자료를 찾던 중 Medium에서 참조할만한 레퍼런스를 찾았으나

 

자바코드로 구성되어 있는것은 물론 일부 생략된 부분들이 있어

 

구현에 조금 어려움이 있었다.

 

 

우여곡절 끝에 기본구성을 마치고 구성에 필요했던 과정과 준비물들을 정리해두고자 한다.

 

 


 

준비물

 

1. 뷰로 선언할 캘린더의 xml Layout (calendar_layout)

2. 달력의 일자에 적용할 xml Layout (calendar_day_layout)

3. LinearLayout을 확장해 만든 커스텀 캘린더 클래스

4. 캘린더의 일자 구성, 색상변경등을 수행할 캘린더 어댑터 클래스

 

 

구성 방법

 

1. 뷰로 선언할 캘린더의 xml Layout (calendar_layout) 만들기

 

캘린더의 전체 틀로써 크게 LinearLayout으로 짠 요일 섹션과 GridView를 사용한 일자 섹션으로 구성됨.

 

요일 섹션의 경우 사용자의 입맛에 따라 디자인을 하면 되며, 가장 상위 레이아웃의 경우 필자는 ConstraintLayout을 사용하였음.

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:padding="20dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

<LinearLayout
    android:id="@+id/calendar_header"
    android:layout_width="match_parent"
    android:layout_height="30dp"
    android:gravity="center_vertical"
    android:orientation="horizontal">

	<!-- 달력의 요일 섹션 구성 -->
    
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center_horizontal"
        android:textColor="#222222"
        android:text="MON"/>

    ...일요일까지 반복.
    
    구성하고자 하는 달력 스타일에 따라 커스터마이징 할 것.
    
</LinearLayout>

<!-- 달력의 일자 섹션 구성 -->
<GridView
    android:id="@+id/calendar_grid"
    android:layout_width="match_parent"
    android:layout_height="240dp"
    android:numColumns="7"/>
    
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

calendar_layout을 만들고 나면 대략 이런 형태를 갖추게 됨.

2. 달력의 일자에 적용할 xml Layout (calendar_day_layout)

 

달력의 개별 일자 디자인을 설정하는 xml 레이아웃. TextView로 선언해야 함.

<?xml version="1.0" encoding="utf-8"?>
<TextView
    android:layout_width="38dp"
    android:layout_height="38dp"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:text="1"
    android:gravity="center"
    android:textSize="22sp"
    android:padding="5dp"
    android:layout_margin="15dp"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"/>

 

 

대략 이런 모습이며 직접 실행하면서 전반적으로 디자인이 맞는지 확인해보아야.

3. LinearLayout을 확장해 만든 커스텀 캘린더 클래스

 

커스텀 캘린더 클래스커스텀 캘린더 어댑터와 함께 달력의 날짜를 어떻게 정의하고 디자인 할지를 지정하게 된다.

 

 

class CalendarView: LinearLayout {
    lateinit var header: LinearLayout
    lateinit var gridView: GridView

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attrs: AttributeSet?) : this(context,
        attrs, 0) {
        initControl(context, attrs!!)
    }

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

    private fun assignUiElements() {
        // layout is inflated, assign local variables to components
        header = findViewById(R.id.calendar_header)
        gridView = findViewById(R.id.calendar_grid)
    }
	
    fun updateCalendar(events: HashSet<Date>, inputCalendar: Calendar) {
        val cells = ArrayList<Date>()

        inputCalendar.set(Calendar.DAY_OF_MONTH, 1)
		
        // 여기서 빼주는 값 1의 경우 한 주의 시작요일에 따라 다르게 설정해주면 됨.
        // 필자가 쓴 캘린더의 경우 일요일부터 시작하는 관계로 1을 감산해주었음.
        val monthBeginningCell = inputCalendar.get(Calendar.DAY_OF_WEEK) - 1

        inputCalendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell)

        // 그리드에 집어넣을 cell들의 setup.
        while (cells.size < (Calendar.DAY_OF_MONTH) +
            inputCalendar.getActualMaximum(Calendar.DAY_OF_MONTH)) {
            cells.add(inputCalendar.time)
            inputCalendar.add(Calendar.DAY_OF_MONTH, 1)
        }

        // 그리드 업데이트
        gridView.adapter = CalendarAdapter(context, cells, events, inputCalendar.get(Calendar.MONTH))
    }

    private fun initControl(context: Context, attrs: AttributeSet) {
        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        inflater.inflate(R.layout.calendar_layout, this)
        assignUiElements()
    }

 

4. 캘린더의 일자 구성, 색상변경등을 수행할 캘린더 어댑터 클래스

 

커스텀 캘린더의 앞단을 마무리 짓는 클래스로, CalendarAdapter의 생성과 함께 넘어온 day ArrayList와

 

inputMonth를 활용해 ViewPager에 보여지는 월에 해당하는 일자만을 보여주도록 설정하게 된다.

 

+ 여기서 ViewPager를 쓰지 않을 경우에는 물론 inputMonth를 활용하지 않아도 될 것

 

class CalendarAdapter(context: Context, days: ArrayList<Date>, eventDays: HashSet<Date>,
                      inputMonth: Int) :
    ArrayAdapter<Date>(context, R.layout.calendar_layout, days) {
    
    // for view inflation
    private val inflater: LayoutInflater = LayoutInflater.from(context)
    private val inputMonth = inputMonth - 1

    override fun getView(position: Int, view: View?, parent: ViewGroup): View {
        
        var view = view
        val calendar = Calendar.getInstance()
        val date = getItem(position)

        calendar.time = date
        val day = calendar.get(Calendar.DATE)
        val month = calendar.get(Calendar.MONTH)
        val year = calendar.get(Calendar.YEAR)

        // 오늘에 해당하는 캘린더를 가져옴
        val today = Date()
        val calendarToday = Calendar.getInstance()
        calendarToday.time = today

        // 날짜 디자인으로 먼저 만들어 둔 calendar_day_layout을 inflate
        if (view == null) {
            view = inflater.inflate(R.layout.calendar_day_layout, parent, false)
        }
        
        // 여기에서 기호에 따라 뷰의 생김새와 일자의 디자인을 변경이 가능.
        (view as TextView).setTypeface(null, Typeface.NORMAL)
        view.setTextColor(Color.parseColor("#56a6a9"))

        // inputMonth는 ViewPager의 해당 페이지에 출력하는 Month를 표시.
        if (month != inputMonth || year != calendarToday.get(Calendar.YEAR)) {
        
        	// 아래의 경우 해당월이 아닌 경우에는 GridView에 표시되지 않도록 설정한 예.
            view.visibility = View.INVISIBLE
        }

        if (month == calendarToday.get(Calendar.MONTH) && year == calendarToday.get(Calendar.YEAR) &&
            day == calendarToday.get(Calendar.DATE)) {
            
            // 오늘의 날짜에 하고싶은 짓(?)을 정의
        }
        

        // 날짜를 텍스트뷰에 설정
        view.text = calendar.get(Calendar.DATE).toString()

        return view
    }
}

 

 

완성된 Custom calendar. 

생각보다 일자 설정과 기본적인 레이아웃 구성에 시간이 걸려 로드가 있던 작업이었는데,

 

한번 마무리짓고 나니 다양하게 설정해 쓸 수 있는 캘린더의 토대를 얻었다는 생각이 든다.

 

다만 캘린더 페이지로 들어갈 때와 ViewPager에서 페이지를 넘길 때에 생기는 약간의 버퍼가 있어

 

최적화가 필요한 부분은 최적화가 들어가야되지 않을까 싶다.

 


 

기본적인 커스텀 캘린더 설정은 위에서 다루었고, 이를 ViewPager에 접목하고자 활용한 과정은 아래와 같다.

 

 

1. 설정은 똑같이 하되 개발을 하면서 쓴 커스텀 캘린더를 ViewPager의 페이지 레이아웃에 선언하고

 

2. ViewPagerAdapter의 instatiateItem에 페이지 변경시마다 변경된 페이지에 해당하는 월을 설정하게 되면 마무리된다.

 

2-1. 여기에서 필자는 캘린더의 월 설정을 위해 최초 페이지를 ViewPager의 마지막 페이지로 설정하고

 

2-2. 현재의 월과 페이지 변경으로 인해 바뀌는 월의 차이를 캘린더 뷰에 적용하는 것으로 구현을 마칠 수 있었다.

 

override fun instantiateItem(container: ViewGroup, position: Int): Any{
        val eventDays: HashSet<Date> = HashSet()
        eventDays.add(Date())
 		
        // monthGap은 현재 월과의 차이를 저장.
        monthGap = pageCount - 1 - position
 
        val calendar = Calendar.getInstance()
 
        calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) - monthGap)
 
        view.findViewById<CalendarView>(R.id.calendar_view).updateCalendar(eventDays,calendar)
 
        container.addView(view)
}

** ViewPager 설정에 관해 보고자 하면 아래 페이지를 슬쩍 살펴봐보자 ** 

 

https://mparchive.tistory.com/138

 

[안드로이드/Kotlin] ViewPager로 온보딩 페이지 간단히 구현하기

코틀린 환경에서 온보딩 페이지를 구현할 일이 생겼는데, 유투브에서 매우 친절한 영상을 찾아 1시간도 못되는 시간안에 온보딩 페이지를 구현할 수 있었다. (물론 영상에서는 자바로 진행하지만) 간단히 아래의..

mparchive.tistory.com

 

 

< 참조 >

 

https://medium.com/meetu-engineering/create-your-custom-calendar-view-10ff41f39bfe

 

Create your own custom Calendar View

For the last week, i spend most of my days trying to find a quick and easy library/packages to display a calendar view for MeetU schedules…

medium.com

https://www.toptal.com/android/android-customization-how-to-build-a-ui-component-that-does-what-you-want

 

Android Customization: How to Build a UI Component That Does What You Want

Default Android UI components get the job done, but what if they aren't exactly what you want? Good news! Android customization with the Android UI is easy. In this tutorial, Toptal developer Ahmed Al-Amir demonstrates how to build a custom calendar compon

www.toptal.com