본문 바로가기

이상/Andrioid

[Android/Kotlin] 커스텀 달력 만들기

반응형

이전 글의 Infinite Loop ViewPager2를 이용해서 캘린더 화면을 만들어보려고 한다.

 

 

화면을 뜯어보면 이런 식이다.

 

1. MainActivity에 FragmentStateAdapter를 이용한 ViewPager2(Horizontal) + TabLayout를 적용하여 앱을 Tab형식으로 사용

 

2. 첫번째 Tab인 FirstFragment에 ViewPager2(Vertical)를 추가하고 수직방향으로 넘기는 캘린더 화면(CalendarFragmet)을 넣어 사용

 

MainActivity 위에 FirstFragment가 있고 그 위에 CalendarFragment가 올라간 꼴이다.

 

 

 

1. CalendarFragment 추가

 

이제 달력을 보여줄 Fragment를 추가한다.

 

레이아웃은 최상단에 년, 월을 보여주는 TextView,

 

바로 아래에 일~토를 표시해줄 LinearLayout,

 

그 아래에 달력을 보여줄 RecyclerView를 품고있는 LinearLayout으로 구성했다.

 

fragment_calendar.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    tools:context="com.example.workoutProject.fragment.first.CalendarFragment">
 
    <TextView
        android:id="@+id/calendar_year_month_text"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="2020년 01월"
        android:textSize="25sp"
        android:textStyle="bold"
        android:gravity="center_vertical|start"
        android:layout_marginStart="20dp"
        style="@style/TextViewStyle"/>
 
    <View
        android:id="@+id/div"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        style="@style/LineView"/>
 
    <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:text="SUN"
            style="@style/TextViewStyle"/>
        <TextView android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center_horizontal"
            android:text="MON"
            style="@style/TextViewStyle"/>
        <TextView android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center_horizontal"
            android:text="TUE"
            style="@style/TextViewStyle"/>
        <TextView android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center_horizontal"
            android:text="WED"
            style="@style/TextViewStyle"/>
        <TextView android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center_horizontal"
            android:text="THU"
            style="@style/TextViewStyle"/>
        <TextView android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center_horizontal"
            android:text="FRI"
            style="@style/TextViewStyle"/>
        <TextView android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center_horizontal"
            android:text="SAT"
            style="@style/TextViewStyle"/>
 
    </LinearLayout>
    <LinearLayout
        android:id="@+id/calendar_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/calendar_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:spanCount="7"
            android:adjustViewBounds="true"
            app:layout_constrainedHeight="true"
            tools:listitem="@layout/list_item_calendar"/>
    </LinearLayout>
</LinearLayout>
cs

 

굳이 LinearLayout으로 RecyclerView를 감싸주지 않아도 되지만

 

나중에 CalendarFragment에 Adapter를 붙일 때 item_view의 높이를 조절해주기 위해서 감싸줬다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class CalendarFragment : Fragment() {
 
    private val TAG = javaClass.simpleName
    lateinit var mContext: Context
 
    var pageIndex = 0
    lateinit var currentDate: Date
 
    lateinit var calendar_year_month_text: TextView
    lateinit var calendar_layout: LinearLayout
    lateinit var calendar_view: RecyclerView
    lateinit var calendarAdapter: CalendarAdapter
 
    companion object {
        var instance: CalendarFragment? = null
    }
 
    override fun onAttach(context: Context) {
        super.onAttach(context)
        if (context is MainActivity) {
            mContext = context
        }
    }
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        instance = this
    }
 
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_calendar, container, false)
        initView(view)
        return view
    }
 
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
    }
 
    fun initView(view: View) {
        pageIndex -= (Int.MAX_VALUE / 2)
        Log.e(TAG, "Calender Index: $pageIndex")
        calendar_year_month_text = view.calendar_year_month_text
        calendar_layout = view.calendar_layout
        calendar_view = view.calendar_view
    }
 
    override fun onDestroy() {
        super.onDestroy()
        instance = null
    }
}
 
cs

 

CalendarFragment에서 쓰일 변수에 대해 initView()에서 초기화해준다.

 

45라인의 pageIndex부분에 대해 풀어보면

 

FirstFragment에서 ViewPager2의 Adapter를 통해 Int.MAX_VALUE/2를 인자값으로 setCurrentItem()를 호출하면

 

Adapter의 createFragment()에서 Int.MAX_VALUE/2번째 CalendarFragment를 반환할 것이다.

 

때문에 CalendarFragment의 pageIndex에는 position값(Int.MAX_VALUE/2)이 들어가게 되고

 

FirstFragment - ViewPager2의 첫번째 CalendarFragment가 될 것이므로 현재 월의 달력화면이 된다.

 

추가로 캘린더를 이동할 때 현재 캘린더의 위치 식별을 편하게 하기 위해 pageIndex를 0으로 만들었다.

 

 

 

2. initView(view: View)

 

이제 최상단 TextView에 년월을 표시해주자.

 

현재 날짜를 가져와서 "yyyy년 MM월" 포맷으로 표시해준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fun initView(view: View) {
    pageIndex -= (Int.MAX_VALUE / 2)
    Log.e(TAG, "Calender Index: $pageIndex")
    calendar_year_month_text = view.calendar_year_month_text
    calendar_layout = view.calendar_layout
    calendar_view = view.calendar_view
    // 날짜 적용
    val date = Calendar.getInstance().run {
        add(Calendar.MONTH, pageIndex)
        time
    }
    currentDate = date
    Log.e(TAG, "$date")
    // 포맷 적용
    var datetime: String = SimpleDateFormat(
        mContext.getString(R.string.calendar_year_month_format),
        Locale.KOREA
    ).format(date.time)
    calendar_year_month_text.setText(datetime)
}
 
cs

 

처음엔 말일에 캘린더를 이동하면 꼬일까봐

 

해당하는 달의 1일을 기준으로 CalendarFragment의 년월을 지정하기 위해

 

set(Calendar.DAY_OF_MONTH, 1)을 해줬다.

 

하지만 테스트 해보니 1월 30일로 설정하고 월을 올려도 2월 28일, 3월 30일 이런식으로 자동계산이 되길래 없앴다.

 

add(Calendar.MONTH, pageIndex)는

 

캘린더를 이동할 경우 이동한만큼 년월 값도 바뀌어야하기 때문에 pageIndex를 이용하여 바뀌도록 해줬다.

 

여기까지하고 실행해보면 아래처럼 작동한다.

 

 

 

CalendarFragment

 

 

 

 

3. Custom Calendar

 

MainActivity - FirstFragment - CalendarFragment 순으로 만들고 CalendarFragment의 년월까지 표시했고

 

이제 CalendarFragment의 캘린더를 보여주기만 하면 된다.

 

Custom Calender Class를 만들어서 [이전 달 끝부분 + 현재 달 + 다음 달 앞부분] 이렇게 생긴 리스트를 넘기기만 하면 된다.

 

Custom Calender Class는 이 분이 잘 정리를 해둔게 있어서 조금 수정하여 사용했다. (감사합니다!)

 

FurangCalendar.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class FurangCalendar(date: Date) {
 
    companion object {
        const val DAYS_OF_WEEK = 7
        const val LOW_OF_CALENDAR = 6
    }
 
    val calendar = Calendar.getInstance()
 
    var prevTail = 0
    var nextHead = 0
    var currentMaxDate = 0
 
    var dateList = arrayListOf<Int>()
 
    init {
        calendar.time = date
    }
 
    fun initBaseCalendar() {
        makeMonthDate()
    }
 
    private fun makeMonthDate() {
 
        dateList.clear()
 
        calendar.set(Calendar.DATE, 1)
 
        currentMaxDate = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
 
        prevTail = calendar.get(Calendar.DAY_OF_WEEK) - 1
 
        makePrevTail(calendar.clone() as Calendar)
        makeCurrentMonth(calendar)
 
        nextHead = LOW_OF_CALENDAR * DAYS_OF_WEEK - (prevTail + currentMaxDate)
        makeNextHead()
    }
 
    private fun makePrevTail(calendar: Calendar) {
        calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) - 1)
        val maxDate = calendar.getActualMaximum(Calendar.DATE)
        var maxOffsetDate = maxDate - prevTail
 
        for (i in 1..prevTail) dateList.add(++maxOffsetDate)
    }
 
    private fun makeCurrentMonth(calendar: Calendar) {
        for (i in 1..calendar.getActualMaximum(Calendar.DATE)) dateList.add(i)
    }
 
    private fun makeNextHead() {
        var date = 1
 
        for (i in 1..nextHead) dateList.add(date++)
    }
 
}
 
cs

 

FurangCalendar를 사용할 때 CalendarFragment의 currentDate값을 넘겨서

 

해당하는 달의 달력에 표시할 Int값 42개를 받아와서 RecyclerView에 보여주면 된다.

 

 

 

4. CalendarFragment.RecylerView - CalendarAdapter

 

우선 Adapter에서 사용할 item의 레이아웃을 만들어 준다.

 

list_item_calendar.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <TextView
        android:id="@+id/item_calendar_date_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="1"
        android:textSize="20sp"
        android:gravity="center_horizontal|top"
        style="@style/TextViewStyle"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
cs

 

이제 CalendarAdapter라는 이름으로 RecyclerViewAdapter를 만든다.

 

CalendarAdapter는 CalendarFragment에서 인자값으로 currentItem을 받아

 

FurangCalendar로 넘겨 날짜 리스트를 받아오고,

 

RecyclerView를 감싸고 있는 LinearLayout의 높이를 이용하여

 

list_item_calendar의 높이를 지정하게 한다.

 

CalendarAdapter.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 높이를 구하는데 필요한 LinearLayout과 FurangCalender를 사용할 때 필요한 date를 받는다.
class CalendarAdapter(val context: Context, val calendarLayout: LinearLayout, val date: Date) :
    RecyclerView.Adapter<CalendarAdapter.CalendarItemHolder>() {
 
    private val TAG = javaClass.simpleName
    var dataList: ArrayList<Int> = arrayListOf()
 
 
    interface ItemClick {
        fun onClick(view: View, position: Int)
    }
 
    var itemClick: ItemClick? = null
 
    override fun onBindViewHolder(holder: CalendarItemHolder, position: Int) {
        holder?.bind(dataList[position], position, context)
        if (itemClick != null) {
            holder?.itemView?.setOnClickListener { v ->
                itemClick?.onClick(v, position)
 
            }
        }
    }
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CalendarItemHolder {
        val view =
            LayoutInflater.from(context).inflate(R.layout.list_item_calendar, parent, false)
        return CalendarItemHolder(view)
    }
 
    override fun getItemCount(): Int = dataList.size
 
    inner class CalendarItemHolder(itemView: View?) : RecyclerView.ViewHolder(itemView!!) {
 
        var itemCalendarDateText: TextView = itemView!!.item_calendar_date_text
        var itemCalendarDotView: View = itemView!!.item_calendar_dot_view
 
        fun bind(data: Int, position: Int, context: Context) {
            
        }
 
    }
}
 
cs

 

 

Adapter에 FurangCalendar변수와 init을 추가해서 FurangCalendar를 이용하여 날짜 리스트를 받아온다.

 

그리고 onBindViewHolder에서 calendarlayout을 높이를 지정해준다.

 

달력에 표시할 날짜가 42개 이므로
list_item_calendar의 높이는 calendar_layout으로 지정해준 영역의 1/6이어야 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 높이를 구하는데 필요한 LinearLayout과 FurangCalender를 사용할 때 필요한 date를 받는다.
class CalendarAdapter(val context: Context, val calendarLayout: LinearLayout, val date: Date) :
    RecyclerView.Adapter<CalendarAdapter.CalendarItemHolder>() {
 
    private val TAG = javaClass.simpleName
    var dataList: ArrayList<Int> = arrayListOf()
 
    // FurangCalendar을 이용하여 날짜 리스트 세팅
    var furangCalendar: FurangCalendar = FurangCalendar(date)
    init {
        furangCalendar.initBaseCalendar()
        dataList = furangCalendar.dateList
    }
 
    interface ItemClick {
        fun onClick(view: View, position: Int)
    }
 
    var itemClick: ItemClick? = null
 
    override fun onBindViewHolder(holder: CalendarItemHolder, position: Int) {
        
        // list_item_calendar 높이 지정
        val h = calendarLayout.height / 6
        holder.itemView.layoutParams.height = h
 
        holder?.bind(dataList[position], position, context)
        if (itemClick != null) {
            holder?.itemView?.setOnClickListener { v ->
                itemClick?.onClick(v, position)
 
            }
        }
    }
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CalendarItemHolder {
        val view =
            LayoutInflater.from(context).inflate(R.layout.list_item_calendar, parent, false)
        return CalendarItemHolder(view)
    }
 
    override fun getItemCount(): Int = dataList.size
 
    inner class CalendarItemHolder(itemView: View?) : RecyclerView.ViewHolder(itemView!!) {
 
        var itemCalendarDateText: TextView = itemView!!.item_calendar_date_text
 
        fun bind(data: Int, position: Int, context: Context) {
          
        }
 
    }
}
 
cs

 

CalendarItemHolder의 bind()에서 data를 TextView에 setText()하여 날짜를 표시하도록 한다.

 

날짜에 따라 list_item_calendar를 꾸미는 것도 여기서 처리하면 된다.

 

오늘 날짜는 Bold처리하고,

 

현재 달을 제외하고 다른 달의 TextView는 글자색을 회색으로 처리한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 높이를 구하는데 필요한 LinearLayout과 FurangCalender를 사용할 때 필요한 date를 받는다.
class CalendarAdapter(val context: Context, val calendarLayout: LinearLayout, val date: Date) :
    RecyclerView.Adapter<CalendarAdapter.CalendarItemHolder>() {
 
    private val TAG = javaClass.simpleName
    var dataList: ArrayList<Int> = arrayListOf()
 
    // FurangCalendar을 이용하여 날짜 리스트 세팅
    var furangCalendar: FurangCalendar = FurangCalendar(date)
    init {
        furangCalendar.initBaseCalendar()
        dataList = furangCalendar.dateList
    }
 
    interface ItemClick {
        fun onClick(view: View, position: Int)
    }
 
    var itemClick: ItemClick? = null
 
    override fun onBindViewHolder(holder: CalendarItemHolder, position: Int) {
        
        // list_item_calendar 높이 지정
        val h = calendarLayout.height / 6
        holder.itemView.layoutParams.height = h
 
        holder?.bind(dataList[position], position, context)
        if (itemClick != null) {
            holder?.itemView?.setOnClickListener { v ->
                itemClick?.onClick(v, position)
 
            }
        }
    }
 
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CalendarItemHolder {
        val view =
            LayoutInflater.from(context).inflate(R.layout.list_item_calendar, parent, false)
        return CalendarItemHolder(view)
    }
 
    override fun getItemCount(): Int = dataList.size
 
    inner class CalendarItemHolder(itemView: View?) : RecyclerView.ViewHolder(itemView!!) {
 
        var itemCalendarDateText: TextView = itemView!!.item_calendar_date_text
 
        fun bind(data: Int, position: Int, context: Context) {
            val firstDateIndex = furangCalendar.prevTail
            val lastDateIndex = dataList.size - furangCalendar.nextHead - 1
 
            // 날짜 표시
            itemCalendarDateText.setText(data.toString())
 
            // 오늘 날짜 처리
            var dateString: String = SimpleDateFormat("dd", Locale.KOREA).format(date)
            var dateInt = dateString.toInt()
            if (dataList[position] == dateInt) {
                itemCalendarDateText.setTypeface(itemCalendarDateText.typeface, Typeface.BOLD)
            }
 
            // 현재 월의 1일 이전, 현재 월의 마지막일 이후 값의 텍스트를 회색처리
            if (position < firstDateIndex || position > lastDateIndex) {
                itemCalendarDateText.setTextAppearance(R.style.LightColorTextViewStyle)
                itemCalendarDotView.background = null
            }
        }
 
    }
}
 
cs

 

캘린더 완성!

 

 

CalendarFragment 완성

 

 

 

끝.

 

 

 

+ 21.09.07
bitbucket 주소

반응형