본문 바로가기
안드로이드

Kotlin CustomCalendarView 구현

by 베어 그릴스 2021. 10. 10.
320x100

커스텀 캘린더뷰 구현

 

기존의 캘린더뷰는 디자인 퍼블리싱 하기 상당히 까다로운 면이 많다.

그래서 내 마음대로 디자인할 수 있는 캘린더뷰를 커스텀해보기로 한다.

<?xml version="1.0" encoding="utf-8"?>
<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"
    android:paddingHorizontal="18dp">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="36dp"
        android:orientation="horizontal"
        >

        <TextView
            android:id="@+id/sunday_text"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:textColor="@color/black"
            android:text="일"
            android:textSize="15sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/monday_text"
            />

        <TextView
            android:id="@+id/monday_text"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="월"
            android:textColor="@color/black"
            android:textSize="15sp"
            app:layout_constraintStart_toEndOf="@id/sunday_text"
            app:layout_constraintEnd_toStartOf="@id/tuesday_text"
            />

        <TextView
            android:id="@+id/tuesday_text"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="화"
            android:textColor="@color/black"
            android:textSize="15sp"
            app:layout_constraintStart_toEndOf="@id/monday_text"
            app:layout_constraintEnd_toStartOf="@id/wednesday_text"
            />

        <TextView
            android:id="@+id/wednesday_text"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="수"
            android:textColor="@color/black"
            android:textSize="15sp"
            app:layout_constraintStart_toEndOf="@id/tuesday_text"
            app:layout_constraintEnd_toStartOf="@id/thursday_text"
            />

        <TextView
            android:id="@+id/thursday_text"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="목"
            android:textColor="@color/black"
            android:textSize="15sp"
            app:layout_constraintStart_toEndOf="@id/wednesday_text"
            app:layout_constraintEnd_toStartOf="@id/friday_text"/>

        <TextView
            android:id="@+id/friday_text"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="금"
            android:textColor="@color/black"
            android:textSize="15sp"
            app:layout_constraintStart_toEndOf="@id/thursday_text"
            app:layout_constraintEnd_toStartOf="@id/saturday_text"/>

        <TextView
            android:id="@+id/saturday_text"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="토"
            android:textColor="@color/black"
            android:textSize="15sp"
            app:layout_constraintStart_toEndOf="@id/friday_text"
            app:layout_constraintEnd_toEndOf="parent"/>
    </LinearLayout>
    <!--달력뷰-->
    <GridView
        android:id="@+id/calendar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scrollbars="none"
        android:gravity="center"
        android:foregroundGravity="center"

        android:verticalSpacing="14dp"
        android:numColumns="7"
        />



</LinearLayout>

캘린더 레이아웃 캘린더뷰의 가장큰 부모 레이아웃이 될 레이아웃이다.

GridView에 동적으로 해당하는 달의 날짜가 들어가게 되고, 이렇게 GridView를 직접 만들어주기 때문에 디자인 퍼블리싱 하기에 간편하다.

 

<?xml version="1.0" encoding="utf-8"?>
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/text"
    android:layout_width="25dp"
    android:layout_height="25dp"

    android:gravity="center"
    android:textSize="15sp"
    android:text="1"
    />

GridView의 내부 아이템 레이아웃

 

package com.example.customcalendar

import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.widget.GridView
import android.widget.LinearLayout
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView


import java.util.*
import kotlin.collections.ArrayList

class CalendarView: LinearLayout {
    interface OnItemClickListener{
        fun onItemClick(v: View, year:Int,month:Int,day:Int)
    }
    private var listener : OnItemClickListener? = null
    fun setOnItemClickListener(listener : OnItemClickListener) {

        this.listener = listener

    }
    lateinit var gridView: GridView


    var year=0
    var month=0
    var day=0

    constructor(context: Context) : this(context, null){
        initControl(context)
    }
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) {
        initControl(context)
    }
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    private fun assignUiElements() {
        gridView = findViewById(R.id.calendar)!!
    }


    fun updateCalendar(inputCalendar: Calendar) {
        val cells = ArrayList<Date>()

        this@CalendarView.year=inputCalendar.get(Calendar.YEAR)
        this@CalendarView.month=inputCalendar.get(Calendar.MONTH)+1
        this@CalendarView.day=inputCalendar.get(Calendar.DATE)

        inputCalendar.set(Calendar.DAY_OF_MONTH, 1)//전부 첫날짜를 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)
        } // 그리드 업데이트


        val adapter=CalendarAdapter(context, cells,this.month,inputCalendar,)

        adapter.setOnItemClickListener(object :CalendarAdapter.OnItemClickListener{
            override fun onItemClick(v: View, year: Int, month: Int, day: Int) {
                listener?.onItemClick(v, year, month, day)
            }
        })

        gridView.adapter = adapter
    }

    @SuppressLint("ServiceCast")
    private fun initControl(context: Context) {
        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        inflater.inflate(R.layout.calender_layout, this)
        assignUiElements()
    }
}

실제 뷰가 될 클래스 linerlayout을 상속받았기 때문에 이제 진짜 xml상에서 이 클래스를 불러올 수 있다.

달력 내부의 날짜에 클릭이벤트를 주고 싶어서 interface를 통한 연결을 시도해주었다.

뷰를 만든 화면의 코드상에서 updateCalendar를 해주는 것을 반드시 기억하자! 어떤 달이 내부로 올건지에 따라 보이는 달력이 달라지기 때문에 updateCalendar를 해주어야 보인다.

 

package com.example.customcalendar

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import android.widget.Toast
import java.util.*
import kotlin.collections.ArrayList

class CalendarAdapter(context: Context, days: ArrayList<Date>, inputMonth: Int, calendar: Calendar) :
        ArrayAdapter<Date>(context, R.layout.calender_layout, days) {
    interface OnItemClickListener{
        fun onItemClick(v: View, year:Int,month:Int,day:Int)
    }
    private var listener : OnItemClickListener? = null
    fun setOnItemClickListener(listener : OnItemClickListener) {
        this.listener = listener
    }
    private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    private var inputMonth = inputMonth - 1
    private val calender=calendar

    override fun getView(position: Int, view: View?, parent: ViewGroup): View {
        var view = view
        val calendar = calender
        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.item_calendar, parent, false)
        } // 여기에서 기호에 따라 뷰의 생김새와 일자의 디자인을 변경이 가능.
        (view as TextView).setTypeface(null, Typeface.NORMAL)
        view.setTextColor(Color.parseColor("#595959")) // inputMonth는 ViewPager의 해당 페이지에 출력하는 Month를 표시.

        if (month != inputMonth) { // 아래의 경우 해당월이 아닌 경우에는 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()// 날짜를 텍스트뷰에 설정
        view.setOnClickListener {
            listener?.onItemClick(it,year, month+1, day)
        }

        return view
    }

}

여기선 각 날짜에 하고 싶은 것들을 정의할 수 있다. 외부에서 받아온 데이터가 있다면 만약 그 해당 날짜와 같다면 ~~ 작업을 수행시켜 주면 당연히 그 작업을 수행할 것이다.

 

 


후기

gridView로 만든 후에 보니 뭔가 Recyclerview에서 GridLayoutManager를 통해 만들고 싶은 욕심이 들기도 한다. 직접 만든뷰기 때문에 물론 코드에 접근해 디자인 퍼블리싱하기엔 좋지만, 언젠가 실력을 기르고 많은 사람들이 사용하는 오픈소스,뷰를 만들어 보고 싶다는 욕심이 생겼다! 

 

728x90

'안드로이드' 카테고리의 다른 글

[Android] RoomDB  (0) 2021.11.07
RecyclerView. No adapter attached; skipping layout 오류에 관하여  (0) 2021.10.02