rlqja1107 / LocationService

Based on Location Service, We would put a electronic kick-board option on Finding Way
0 stars 0 forks source link

Custom Calendar 구현(RecyclerView로 구현) #9

Open rlqja1107 opened 4 years ago

rlqja1107 commented 4 years ago

Custom Calendar 구현

1. UI

calendar activity
위 캘린더에 나와있는 정보는 지도 상에서 마커로 기록해놓은 정보를 의미한다. 지도상의 데이터를 연결하여 달력에 표시한 것이다.

2. 구글 라이브러리 달력의 한계

구글에 달력 라이브러리가 존재하지만 달력 메모 기능을 이용하기 위해서는 Custom 달력이 필요했다. 또 구글 달력은 달력에서 일정을 넣었다 해도 UI에서 일정이 보이지 않는다. 따라서 Custom 달력을 만들게 되었다. 디자인도 내 맘대로 조절할 수 있기 때문도 있었다.

3. Data Binding

이번 달력만들기에서는 Data Binding을 이용했다. Data Binding은 class코드(.kt)와 layout 코드(.xml)을 연결시켜준다. 자바에서는 layout에서 layout xml tag를 이용하기 위해서는 일일이 findViewById 함수를 통해 불러와야하지만, Data Binding을 통해 직접 이용할 수 있게 되었다. 물론, 현재 이용하고 있는 코틀린에서는 함수언어로 일일이 Id를 매칭시키지않고 이용해도 된다. 대신 Data Binding을 통해 xml에서도 class코드를 이용할 수 있게되었다. Data Binding하기 위해서는 먼저 허용해주어야 한다.

Build.gradle(app)

android {
    dataBinding{
        enabled=true
    }
}

그러고 xml에 layout xml코드 상위에 다음과 같이 tag를 선언해주어야한다. class 코드(.kt)에서 xml 코드를 사용하려고 할때, xml 구별하기 위한 class 명을 선언하고 xml에서 class 코드에서 사용하고 싶은 변수나 함수가 있으면 variable tag에 선언해준다. 내부 속성 name으로 xml에서 사용할 수 있게 된다. 예시를 보면 다음과 같다. ConstraintLayout 아래에는 기존에 사용하던 layout xml을 이용하면된다.

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data class="DayItemBinding">

        <import type="android.view.View" />

        <variable
            name="model"
            type="com.example.toyou.ui.CalendarViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >
...
...

아래는 layout(xml)에서 class(.kt)코드를 이용하기 위해 class에서의 코드다.

TextBindingAdapter.kt

 @JvmStatic
            @BindingAdapter(*["setDayText"])
            fun setDayText(view: TextView, calendar: Calendar) {
                try {
                    var gregorian = GregorianCalendar(
                        calendar.get(Calendar.YEAR),
                        calendar.get(Calendar.MONTH),
                        calendar.get(Calendar.DAY_OF_MONTH),
                        0,
                        0,
                        0
                    )
                    view.text = DateFormat.getDate(gregorian.timeInMillis, DateFormat.DAY_FORMAT)

                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }

자바의 경우 static을 이용해야겠지만, 코틀린에서 static으로 선언해주기 위해 @JvmStatic을 이용했다. xml에서 함수명으로 사용하기 위해서 @BindingAdapter(*[""])을 선언해주어야한다. 그러고 내부함수에 xml에서 사용할 View의 타입을 먼저 넣어준다. 그러고 textView의 경우 넣어줄 text를 함수 내부에 선언하면된다. xml에서는 다음과 같다.

item_day.xml

 <TextView
                setDayText="@{model.MCalendar}"
                android:id="@+id/dayText"
                android:gravity="center"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="4dp"
                android:textSize="12sp"
                tools:text="1" />

data 태그 내부에서 varaible 속성인 name을 model로 선언했기 때문에 model.을 이용하여 해당 class의 변수나 함수를 이용하면된다. 그리고 @BindingAdapter에서 setDayText로 선언을 했으면 위의 코드처럼 이용하면된다. 함수명 = "@{model.변수}" 의 방식을 이용하면된다. 만약 해당 함수가 xml에서 RecyclerView와 같이 많이 사용되어질 수 있는 경우 Data Binding을 통해 코드를 간결화 시킬 수 있다.

4. 달력 Code 구현

코드 구현하기 전에 달력을 구현하는데 DataBinding을 이용했다.
위의 UI에서 볼 수 있듯이 RecyclerView에 필요한 xml layout이 3개가 있다. 하나는 위 UI에서 볼 수 있듯이 2020년 8월 등과 같은 헤더에 해당하는 layout이다. xml 코드는 다음과 같다. 코드가 긴 관계로 링크로 걸었다. 그리고 코드 아래 링크는 Data Binding 시킨 ViewModel 클래스다. item_calendar_header.xml
CalendarHeaderModel.kt 위 UI에서의 날짜 시작하기 전의 빈 공간은 다음 코드로 구현했다.
item_day_empty.xml
EmptyViewModel
날짜 UI에 해당하는 코드는 다음과 같다.
item_day.xml
CalendarViewModel
코드를 보면 알 수 있듯이 data태그를 이용하여 Data Binding을 이용했다. 각각은 ViewModel를 상속받은 클래스를 variable태그 속성으로 놓아서 사용했다. ViewModel를 이용해서 달력을 보일 때마다 RecyclerView의 adapter를 적용시키는 것이 아니라 앱이 종료되기 전까지 그 정보를 저장시켜놓았다. 정보를 저장시켜 달력을 보일 때 달력의 RecyclerView adapter 사이클을 줄일 수 있었다.

CalendarAdapter.kt

RecyclerView의 Adapter로 사용될 코드다. Adapter에 지도 메모 데이터 속성 중에 년,월,일을 사용해 Calendar 객체를 만들고 List로 매개변수로 넘겨 해당 날짜에 기록하는 방식을 이용했다.

 val calArray = ArrayList<Calendar>().apply {
            for (i in memoData)
                this.add(GregorianCalendar(i.year, i.month, i.day))
        }

지도 상의 메모로 이용되는 객체(MemoData)를 매개변수로 안넘기고 Calendar로 넘긴 이유는 BinarySearch하기 위함이다. 매번(하루씩) 그 날짜에 해당되는 기록(메모)이 있는지 체크해야되기 때문이다. 만약 For문으로 그 날에 해당되는 메모가 있는지 확인을 한다면 지도 메모수가 늘어날수록 그 횟수는 기하급수적으로 늘어날 것이다. 따라서 For문의 O(n)을 줄이기 위해 BinarySearch(O(logn)를 이용했다. 즉, 사이클 수를 줄이기 위해 Calendar 객체 list를 매개변수로 넘겨주었다.
코드는 다음과 같다.
CalendarAdapter.kt
달력상의 2018년 6월, 2018년 8월 등을 나타내는 header 부분은 Long으로 구별했고, 날짜 시작하기 전의 빈공간은 String으로 구별했고, 날짜가 있는 layout은 Calendar객체로 구별했다.
그리고 동일한 날짜에 여러개의 메모가 있을 수 있기 때문에 날짜 layout에 RecyclerView를 이용했다. CalendarAdapter.kt 내부에서 다음과 같이 구현했다.

 var find=calArray.binarySearch(item,0)
                if(find>=0){
                    var applyArray= findMemo(item)
                    holder.itemView.dailyRecyclerView.layoutManager=LinearLayoutManager(CalendarActivity.context)
                    holder.itemView.dailyRecyclerView.adapter=DailyListAdapter(applyArray)
                }

DailyListAdapter의 코드는 다음과 같다.
DailyListAdapter.kt

rlqja1107 commented 4 years ago

하지만 위의 코드와 같이 구현하면 화면을 넘길 때 날짜에 기록해둔 메모가 이동하는 현상이 일어난다. 이러한 버그가 일어나는데는 2가지 이유 중 하나로 잡았다. ViewModel를 사용으로 인한 버그 또는 Adapter를 ListAdapter를 상속받아 구현한 문제 2가지 중 하나로 잡았다. 아직 ViewModel를 이용했을 때, ListAdapter를 이용했을 때 생기는 다른 현상을 제대로 파악하지 못하여 생긴 버그라고 생각한다.

rlqja1107 commented 4 years ago

버그 수정 issue
issue 10