본문 바로가기
KUIT-앱 개발 프로젝트 동아리

10주차 실습(1)-캘린더(calendarView, materialCalendarView)

by 농농씨 2023. 12. 8.

10주차는 캘린더와 그래프를 다룰 것이다.

 

1. 뷰바인딩 설정을 먼저 해주자

app수준 그래들에는 아래 코드를

buildFeatures {
    viewBinding = true
}

메인액티비티에는 아래 코드를 먼저 작성해준다.

class MainActivity : AppCompatActivity() {
    lateinit var binding : ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}

 

2. 캘린더 레이아웃 설정

2-1. 캘린더 뷰 추가

메인액티비티 xml에 캘린더 뷰를 추가해주자

https://developer.android.com/reference/android/widget/CalendarView

 

CalendarView  |  Android Developers

 

developer.android.com

공식 문서를 참고할 수 있다. 하지만 많은 것들이 deprecated돼서 잘 사용하지 않는다.

 

xml에 <calendarView 사용하면 오른쪽과 같은 실행화면이 나타난다

 

2-2. 속성추가

deprecated 되지 않은 함수들을 사용해보자.

weekDayTextAppearance에서 weekday는 요일을, 

dateTextAppearance의 date는 날짜를 의미하는데 이것들 써보자

 

2-2-1. style 추가

다음과 같이 요일과 일에 대한 text style을 정의해준다.

<resources xmlns:tools="http://schemas.android.com/tools">
    ...
    <style name="calendarWeekText" parent="TextAppearance.AppCompat.Button">
        <item name="android:textStyle">bold</item>
        <item name="android:textColor">#FF00FF</item>
    </style>

    <style name="calendarDateText" parent="TextAppearance.AppCompat.Button">
        <item name="android:textStyle">bold</item>
        <item name="android:textColor">#A0A0A0</item>
    </style>
</resources>

 

2-2-2. style적용

이런식으로 style을 적용해준다 코드는 다음과 같다.

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <CalendarView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:weekDayTextAppearance="@style/calendarWeekText"
        android:dateTextAppearance="@style/calendarDateText"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

bold 설정 있고없고가 차이가 없는 것 같아서 다시 try

 

 

2-3. 콜백함수 활용

캘린더에서 날짜를 선택했을때 콜백함수가 호출되는 것을 이용해보자.

 

2-3-1. 캘린더뷰에 id 설정

<CalendarView
    android:id="@+id/calendar"
    ...
/>

 

2-3-2. 콜백함수 호출

binding.calendar.setOnDateChangeListener {} 

를 호출한다

package com.iyr.a10thweek2

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.iyr.a10thweek2.databinding.ActivityMainBinding
import com.iyr.a10thweek2.ui.theme._10thWeek2Theme

class MainActivity : ComponentActivity() {
    lateinit var binding : ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        binding.calendar.setOnDateChangeListener { view, year, month, dayOfMonth ->
            Log.d("qwerty", view.toString() + " " + year.toString() + " " + month.toString() + " " + dayOfMonth.toString())
        }
    }
}

위와 같이 콜백 함수 안에 로그를 추가해서 뷰, 연, 월, 일을 출력하도록 하면 각 날짜를 클릭했을 때 다음과 같은 로그가 뜬다

 

프로젝트에서 날짜를 가져와야 하면 이런 식으로 콜백함수를 이용할 수도 있고, datePicker라는 함수도 존재한다.

 

 

 

3. material calendar

매트리얼 디자인은 디자인을 위한 라이브러리이다.

구글에 material calendar view라고 검색하면 최상단에 뜨는 레포지토리로 들어가서 하란대로 해주면 된다.

https://github.com/prolificinteractive/material-calendarview

 

GitHub - prolificinteractive/material-calendarview: A Material design back port of Android's CalendarView

A Material design back port of Android's CalendarView - GitHub - prolificinteractive/material-calendarview: A Material design back port of Android's CalendarView

github.com

wiki 를 클릭하면 오른쪽에 customization/customization builder/decorators 등이 있으므로 들어가볼 수 있다.

 

3-1. 레포지토리 경로, 의존성 추가

// setting.gradle 파일에
allprojects {
  repositories {
    ...
    maven { url 'https://jitpack.io' }
  }
}
코틀린으로는
maven ("https://jitpack.io")

// gradle.app 파일에
dependencies {
  implementation 'com.github.prolificinteractive:material-calendarview:${version}'
}
코틀린으로는 implementation("com.github.prolificinteractive:material-calendarview:${version}")

version은 가장 최신 버전인 2.0.1을 넣어준다

 

3-2. material view 추가

기존의 calendarview를 잠시 주석처리하고 다음 코드를 넣어준다

<com.prolificinteractive.materialcalendarview.MaterialCalendarView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

여기까지만 하고 실행해보면 다음과 같은 오류가 뜬다

Duplicate class android.support.v4.app.INotificationSideChannel found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-27.0.2-runtime (com.android.support:support-compat:27.0.2)
Duplicate class android.support.v4.app.INotificationSideChannel$Stub found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-27.0.2-runtime (com.android.support:support-compat:27.0.2)
Duplicate class android.support.v4.app.INotificationSideChannel$Stub$Proxy found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-27.0.2-runtime (com.android.support:support-compat:27.0.2)
Duplicate class android.support.v4.os.IResultReceiver found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-27.0.2-runtime (com.android.support:support-compat:27.0.2)
Duplicate class android.support.v4.os.IResultReceiver$Stub found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-27.0.2-runtime (com.android.support:support-compat:27.0.2)
Duplicate class android.support.v4.os.IResultReceiver$Stub$Proxy found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-27.0.2-runtime (com.android.support:support-compat:27.0.2)
Duplicate class android.support.v4.os.ResultReceiver found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-27.0.2-runtime (com.android.support:support-compat:27.0.2)
Duplicate class android.support.v4.os.ResultReceiver$1 found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-27.0.2-runtime (com.android.support:support-compat:27.0.2)
Duplicate class android.support.v4.os.ResultReceiver$MyResultReceiver found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-27.0.2-runtime (com.android.support:support-compat:27.0.2)
Duplicate class android.support.v4.os.ResultReceiver$MyRunnable found in modules core-1.9.0-runtime (androidx.core:core:1.9.0) and support-compat-27.0.2-runtime (com.android.support:support-compat:27.0.2)

 

이는 추가한 라이브러리가 빌드 중에 기존 라이브러리들과 충돌해서 발생하는 오류이다.

 

gradle.properties에 다음 코드를 추가해서 해결하자.

android.useAndroidX=true
android.enableJetifier=true

android.useAndroidX=true : 기존의 Support Library 대신 적절한 AndroidX Library를 사용할 수 있다.
android.enableJetifier=true : 기존의 타사 라이브러리를 자동으로 AndroidX로 이전할 수 있다. 참고로 enableJetifier를 true로 설정할 시 빌드 시간을 지연시킬 수 있으므로, 꼭 필요한 경우가 아니라면 사용하지 않는 것이 좋다. 만약 이미 사용중이라면 빌드 분석 도구(Android Chipmunk version부터 사용 가능)로 Jetifier가 실제로 필요한지 확인할 수 있다.

reference: https://velog.io/@jeongminji4490/Error-Duplicate-class-found

 

 

<에러노트

나는 설정을 추가했는데도 material calendar view에서 다음과 같은 에러가 났다.

Binary XML file line #23 in com.iyr.a10thweek2:layout/activity_main: Binary XML file line #23 in com.iyr.a10thweek2:layout/activity_main: Error inflating class com.prolificinteractive.materialcalendarview.MaterialCalendarView

style에서 애플리케이션의 테마를 material에서 appcompat으로 다음과 같이 바꾸어줬더니 에러가 해결됐다.

전에도 테마를 material에서 다른 걸로 바꿨더니 해결된 적이 있었다.

// res폴더-values폴더-themes.xml 파일
<resources>

<!--  기존의 테마  <style name="Theme._10thWeek2" parent="android:Theme.Material.Light.NoActionBar" />-->
    <style name="Theme._10thWeek2" parent="Theme.AppCompat.Light" />
  
...
</resources>

 

/>

 

3-3. material calendar view의 다양한 속성들

https://github.com/prolificinteractive/material-calendarview/wiki/Customization

 

Customization

A Material design back port of Android's CalendarView - prolificinteractive/material-calendarview

github.com

material calendarview를 커스텀하는 다양한 속성들을 알려주는 페이지다.

<com.prolificinteractive.materialcalendarview.MaterialCalendarView
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/calendarView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:mcv_showOtherDates="option" // 선택된 달(Month) 외 날짜 보이기 
    app:mcv_arrowColor="color" // deprecated 됨
    app:mcv_selectionColor="color"
    app:mcv_headerTextAppearance="style"
    app:mcv_dateTextAppearance="style" // 요일 글씨 스타일
    app:mcv_weekDayTextAppearance="style" // 일 글씨 스타일
    app:mcv_weekDayLabels="array" // 요일 글자 어떻게 표시할지 설정
    app:mcv_monthLabels="array"
    app:mcv_tileSize="dimension"
    app:mcv_tileWidth="dimension"
    app:mcv_tileHeight="dimension"
    app:mcv_firstDayOfWeek="enum" // 일주일의 시작을 (일요일로) 설정
    app:mcv_leftArrowMask="drawable"
    app:mcv_rightArrowMask="drawable" 
    app:mcv_selectionMode="range"    // 캘린더를 Range Mode로 설정
    app:mcv_rightArrow="@drawable/ic_arrow_right_cal"    // 우측으로 아이콘
    app:mcv_leftArrow="@drawable/ic_arrow_left"     // 좌측으로 아이콘
    app:mcv_calendarMode="mode"
    />

활용법 참조 👇

이중에서 tile은 '날짜가 들어가는 칸'을 의미한다. 이걸 먼저 커스텀해보자.

 

3-3-1. tile 커스텀

1) dimension xml 파일 만들기

dimension은 안드로이드에서 크기 속성을 저장하는 value 이다.

value-new-values resource file로 새로운 value 파일을 만들어준다

// dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="tile_height">50dp</dimen>
</resources>

 

2) calendar view에 적용

그리고 이걸 

<com.prolificinteractive.materialcalendarview.MaterialCalendarView
    ...
    app:mcv_tileHeight="@dimen/tile_height"
    />

위와같이 적용해주면

tile 크기 100dp 로 적용하기 전/후

 

이렇게 타일 크기가 넓어진 것을 알 수 있다.

(가로 간격은 디바이스 크기에 맞춰서 그대로고 세로 간격만 늘어난 것 같다.)

 

 

3-3-2. 요일 커스텀

1)string array 만들기

이번에는 요일 커스텀을 해보자.

app:mcv_weekDayLabels="array"는 요일에 어떤 글자를 넣을지에 대한 속성이다.

 

그런데 데이터 바인딩을 안 했는데 어떻게 array를 적용할 수 있을까?

values-strings.xml에 들어가면 string array를 xml로 만들 수 있다.

// strings.xml
<resources>
    <string name="app_name">10thWeek2</string>
    <string-array name="week_label">
        <item>M</item>
        <item>T</item>
        <item>W</item>
        <item>T</item>
        <item>F</item>
        <item>S</item>
        <item>S</item>
    </string-array>
</resources>

 

2) string array 적용하기

다음과 같이 string array를 레이아웃에 적용해준다.

<com.prolificinteractive.materialcalendarview.MaterialCalendarView
        ...
        app:mcv_weekDayLabels="@array/week_label"
        />

 

그럼 다음과 같이 바뀐 걸 확인할 수 있다.

 

이때 strings.xml에서 string array의 item개수가 7개가 아니면 적용되지 않는다.

 

3-3-3. text Appearance 커스텀

아까 calendarView에서도 요일과 일의 textAppearance 속성을 커스텀 해보았다.

이번에는 materialCalendarView에 아까 정의한 style에서 Header의 text까지 추가해서 적용해보자

// activity_main.xml
<com.prolificinteractive.materialcalendarview.MaterialCalendarView
    ...
    app:mcv_weekDayTextAppearance="@style/calendarWeekText"
    app:mcv_dateTextAppearance="@style/calendarDateText"
    app:mcv_headerTextAppearance="@style/calendarHeaderText"
    />

 

결과는 다음과 같다.

 

 

3-3-4. arrow 커스텀

arrow의 버튼을 수정해보자.

1) 아이콘 저장

drawable파일에서 아이콘을 가져와보자.

drawable-new-vector asset을 클릭하고

 

안드로이드 모양이 있는 clip art 버튼을 클릭해주면 다양한 아이콘이 뜬다.

적당한 아이콘을 골라서 drawable 폴더에 저장해준다.

 

2) xml에 적용

// activity_main.xml
<com.prolificinteractive.materialcalendarview.MaterialCalendarView
    ...
    app:mcv_leftArrow="@drawable/baseline_align_horizontal_left_24"
    app:mcv_rightArrow="@drawable/baseline_align_horizontal_right_24"
    />

 

결과는 다음과 같다.

 

3-4. customization bulider

material calendar view의 customization에 이어 customization builder도 살펴보자

https://github.com/prolificinteractive/material-calendarview/wiki/Customization-Builder

mcv.state().edit()
   .setFirstDayOfWeek(Calendar.WEDNESDAY) // 캘린더 시작 요일
   .setMinimumDate(CalendarDay.from(2016, 4, 3)) // ~~년 ~~월부터
   .setMaximumDate(CalendarDay.from(2016, 5, 12)) // ~~년 ~~월까지만 보여주겠다.
   .setCalendarDisplayMode(CalendarMode.WEEKS) // 월 단위가 아닌 주 단위로 보기
   .setSaveCurrentPosition(true)
   .commit();

 

3-5. decorators

https://github.com/prolificinteractive/material-calendarview/wiki/Customization-Builder

 

decorators에서 

 

 

DayViewDecorator{} 라는 인터페이스를 사용해보자.

 

3-5-1. 클래스 생성

onCreate 밖에 material calendar view의 dayviewdecorator를 상속받는 class를 하나 만들어주고

class 이름에 빨간줄 뜨면 필요한 메서드들을 implement 해준다.

// MainActivity.kt
class MainActivity : ComponentActivity() {
    lateinit var binding : ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
//        binding.calendar.setOnDateChangeListener { view, year, month, dayOfMonth ->
//            Log.d("qwerty", view.toString() + " " + year.toString() + " " + month.toString() + " " + dayOfMonth.toString())
//        } // 일반 calendar view에서 날짜선택 콜백 함수 이용했었음
    }
}

class DayDecorator : DayViewDecorator{
    override fun shouldDecorate(day: CalendarDay?): Boolean {
        TODO("Not yet implemented")
    }

    override fun decorate(view: DayViewFacade?) {
        TODO("Not yet implemented")
    }

}

 

3-5-2. 메서드 내용 구체화

fun shouldDecorate(){} 는 해당 날짜를 꾸밀것인지 말것인지에 대한 메서드이고,

class DayDecorator : DayViewDecorator{
    override fun shouldDecorate(day: CalendarDay?): Boolean {
        // 해당 날짜를 꾸밀 것인 지에 대한 메서드
//        return true // 모든 날을 꾸민다.
        return day?.day ?:  == 1
        // (파라미터로 전달받은)day 객체의 day 속성(며칠인지)이 1, 즉 1일일때만 꾸민다.
    }

 

fun decorate(){}는 어떻게 꾸밀 것인지에 대한 메서드이다.

override fun decorate(view: DayViewFacade?) {
    // 어떻게 꾸밀 것인 지에 대한 메서드
    TODO("Not yet implemented")
}

 

모든 날짜를 회색으로 꾸며보자.

fun decorate()의 DayViewFacade 객체는 

addSpan : 배경색깔

setSelectionDrawable : 선택됐을 때 어떻게 보여줄지

setDaysDisablde() : 보일지 말지

등을 설정해 줄 수 있다. 이또한 공식문서를 참고할 수 있다.

 

 

2) xml 수정-selectionMode

캘린더의 선택 모드(selection mode)를 한 날짜만 선택할 수도, 여러 날짜(범위)를 선택할 수도 있다.

다음과 같이 범위를 선택할 수 있게 xml을 수정해주자.

// activity_main.xml
<com.prolificinteractive.materialcalendarview.MaterialCalendarView
    ...
    app:mcv_selectionMode="range" />

 

선택된 애들과 아닌 애들의 색을 fun decorate()를 이용해 변경해줄 것이다.

이때, 선택 여부에 따른 색변경을 코드가 아닌 drawable의 <selector>라는 태그를 이용할 수 있다.

 

3) selector drawable 만들기

drawable폴더-new-drawable resource 파일

클릭하고 다음과 같은 이름으로 selector 태그를 가진 파일을 만들어준다.

 

values-color.xml에서 다음과 같이 색을 정의해주고

<resources>
    <color name="gray">#A0A0A0</color>
    <color name="pink">#FF00FF</color>
</resources>

 

 

다음과 같이 선택 여부에 따른 색을 지정해준다.

checked는 날짜 단일 선택을 의미하고

pressed는 범위 지정시에 선택한 것을 의미한다.

// calendar_selector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector
    xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true"
        android:drawable="@color/pink" />
    <item android:state_pressed="true"
        android:drawable="@color/pink" />
    <item android:state_checked="false"
        android:drawable="@color/gray" />
    <item android:state_pressed="false"
        android:drawable="@color/gray" />
</selector>

 

 

4) fun decorate에서 drawable 지정

// MainActivity.kt
class MainActivity : ComponentActivity() {
    ...
}

class DayDecorator(context: Context) : DayViewDecorator{
    val drawable : Drawable? = ContextCompat.getDrawable(context, R.drawable.calendar_selector)
    override fun shouldDecorate(day: CalendarDay?): Boolean {
        // 해당 날짜를 꾸밀 것인 지에 대한 메서드
        return true // 모든 날을 꾸민다.
//        return day?.day ?:  == 1
        // (파라미터로 전달받은)day 객체의 day 속성(며칠인지)이 1, 즉 1일일때만 꾸민다.
    }
    override fun decorate(view: DayViewFacade?) {
        // 어떻게 꾸밀 것인 지에 대한 메서드
        view?.setSelectionDrawable(drawable!!)
    }
}

 

drawable을 가져와서 변수로 선언하고 그것을 fun decorate에서 선택 여부에 따라 보이도록 했다.

 

5) 오늘 날짜 커스텀

class DayDecorator()을 복사해와서(또는 DayViewDecorator를 상속받아서 두 메서드를 implement 해주고 나서)

class TodayDecorator()로 이름을 바꾸고 오늘 날짜를 커스텀해보자.

val calendar : CalendarDay = CalendarDay.today()

material calendar view 자체에 있는 CalendarDay라는 객체에서 .today()를 호출하면 현재 시간을 기준으로 날짜 객체를 생성해준다.

이를 calendar라는 변수에 저장하자.

 

// MainActivity.kt
class TodayDecorator : DayViewDecorator{
    val calendar : CalendarDay = CalendarDay.today()
    override fun shouldDecorate(day: CalendarDay?): Boolean {
        return calendar == day
    }
    override fun decorate(view: DayViewFacade?) {
        view?.addSpan(object: ForegroundColorSpan(Color.BLUE) {} )
    }
}

fun shouldDecorate는

calendar 객체에 저장한 오늘의 날짜와, shouldDecorate 객체를 통해 전달받은 파라미터인 day 객체가 같을 때만 꾸미겠다는 뜻이다.

즉, 오늘일때만 꾸민다는 말이다.

 

fun decorate는 addspan으로 색을 blue로 설정해주겠다는 의미이다.

 

6) 캘린더 뷰에 decorate class 적용하기

material calendar view에 material_calendar라고 id 달아주고

class MainActivity : ComponentActivity() {
    lateinit var binding : ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
//        binding.calendar.setOnDateChangeListener { view, year, month, dayOfMonth ->
//            Log.d("qwerty", view.toString() + " " + year.toString() + " " + month.toString() + " " + dayOfMonth.toString())
//        }
        
        binding.materialCalendar.addDecorator(DayDecorator(applicationContext))
        binding.materialCalendar.addDecorator(TodayDecorator())
    }
}

위와 같이 캘린더 뷰를 바인딩해서 addDecorator로 아까 만든 클래스들을 넣어준다.

이때 DayDecorator는 context를 파라미터로 받아야 했으므로 함께 넣어주고

TodayDecorator는 인자가 필요없었으므로 그냥 넣어준다.

 

 

실행해보면 다음과 같다.

이때 글자색과 values-colors.xml 파일에 지정한 글자의 span(배경)색이 똑같아서 잘 안보이니 수정해주자

<color name="gray">#C0C0C0</color> // #A0A0A0에서 수정

 

today 글자가 파랗게 잘 나오고

선택되지 않은 날짜는 배경이 회색, 선택된 날짜는 핑크색으로 표시되고 있다.

 

실제로 프로젝트 할 때는 이것보다는 예쁘게 만들어주도록 하자.

 

 

하지만 이런 라이브러리를 활용해도 결국 커스텀에 한계가 있다.

다음 글에서는 리사이클러 뷰를 활용하여 직접 캘린더를 만들어 볼 것이다.

 

 

 

 

 

<에러노트

내 노트북에서만 블루투스가 깜빡깜빡 자기 혼자 켜졌다 꺼졌다를 반복했었는데, 

방해금지 모드를 켰다가 끄니까 해결됐다./>