Strong 감자의 공부

달력 만들기 본문

앱개발/StudyHub 앱 출시 회고

달력 만들기

ugyeong 2024. 12. 30. 17:56

안녕하세요!

이틀만에 다시 뵙습니다~! 😊

오늘은 달력 만들기를 소개하려해요!

 

해당 기능을 만들 때 타 블로그를 참고해서 만들었어요 차이점이 있다면 제가 구현한 달력은 시작날짜와 끝날짜를 선택해서 기간을 설정하는 방식이예요

 

1. 완성본

 

 

 

2. 구현

1, 2... 일 자가 리사이클러뷰 item이고 이를 반복해 달력을 만들었어요

저 하얀색 네모에 1, 2, 3 .. 날짜가 표시될 것이고 모여서 달력이 구성돼요

날짜를 클릭하면 색이 변경돼야하므로 이를 button으로 구성했어요

 

파일명 : calendar_call_item.xml

 

<?xml version="1.0" encoding="utf-8"?>
<layout ... >

    <data>

        <variable
            name="model"
            type="kr.co.gamja.study_hub.feature.studypage.createStudy.InfoOfDays" />
    </data>

    <LinearLayout
        android:layout_width="45dp"
        android:layout_height="45dp"
        android:background="@color/syswhite"
        android:orientation="vertical">

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/txt_day"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:background="@drawable/solid_syswhite"
            android:textColor="@color/sysblack1"
            android:textSize="18dp"
            tools:text="@{model.infoDay}" />
    </LinearLayout>

</layout>

 

 

 

파일명 : fragment_calendar.xml

<?xml version="1.0" encoding="utf-8"?>
<layout ...>

    <data>

        <variable
            name="viewModel"
            type="kr.co.gamja.study_hub.feature.studypage.createStudy.CreateStudyViewModel" />
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" ... >

        <View
            android:id="@+id/view_top"
            ... />
			
        // "완료" 버튼
        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/btn_ok"
           	... />
            
            
        // 이전 달로 가는 "<" 버튼
        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/btn_left"
            ... />
            
            
        // "month" 글자
        <TextView
            android:id="@+id/txt_Month"
            .../>
            
        // 다음 달로 가는 ">" 버튼
        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/btn_right"
            .../>
            
        // 일 ~ 토 요일 표현
        <LinearLayout
            android:id="@+id/layout_linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
          ...>

            <TextView
                android:id="@+id/txt_sunday"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/sunday"
                android:textColor="@color/BG_80"
                android:gravity="center"
                android:textSize="14dp" />

            <TextView
                android:id="@+id/txt_monday"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:text="@string/monday"
                android:textColor="@color/BG_80"
                android:textSize="14dp" />

            <TextView
                android:id="@+id/txt_tuesday"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/tuesday"
                android:gravity="center"
                android:textColor="@color/BG_80"
                android:textSize="14dp" />

            <TextView
                android:id="@+id/txt_wednesday"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/wednesday"
                android:gravity="center"
                android:textColor="@color/BG_80"
                android:textSize="14dp" />

            <TextView
                android:id="@+id/txt_Thursday"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/Thursday"
                android:gravity="center"
                android:textColor="@color/BG_80"
                android:textSize="14dp" />

            <TextView
                android:id="@+id/txt_friday"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/friday"
                android:gravity="center"
                android:textColor="@color/BG_80"
                android:textSize="14dp" />

            <TextView
                android:id="@+id/txt_Saturday"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:text="@string/Saturday"
                android:textColor="@color/BG_80"
                android:textSize="14dp" />

        </LinearLayout>


        // 1~몇 일 표현
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_day"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            ... />
    </RelativeLayout>

</layout>

 

 

이제 리사이클러뷰의 어댑터를 보러 갈게요

 

파일명 : CalandarAdapter.kt

package kr.co.gamja.study_hub.feature.studypage.createStudy

import ...


class CalendarAdapter(private val context: Context) :
    RecyclerView.Adapter<CalendarAdapter.CalendarHolder>() {
    ...
    var daysInfo = ArrayList<InfoOfDays>() // 날짜
    private var onCalendarItemClickListener: OnCalendarItemClickListener? = null
    ...
	
    // CalendarHolder라는 ViewHolder 객체 생성 및 바인딩 객체를 ViewHolder에 전달하여, 나중에 데이터를 뷰와 연결.
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CalendarHolder {
        val binding =
            CalendarCellItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return CalendarHolder(binding)
    }
	
    // position을 통해 해당하는 데이터를 viewHolder에 넣어주기
    override fun onBindViewHolder(holder: CalendarHolder, position: Int) {
        val day = daysInfo.get(position)
        holder.setDays(day)
    }

    // onBindViewHolder로 받은 데이터 실제 뷰와 연결하기
    inner class CalendarHolder(val binding: CalendarCellItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun setDays(item: InfoOfDays) {
            binding.model = item // 1일... day

            // 날짜가 없는 경우 선택 불가
            if (daysInfo[absoluteAdapterPosition].infoDay.isNotEmpty()) {
            
            /*
            selectedPosition: 사용자가 선택한 아이템의 위치. absoluteAdapterPosition: 현재 ViewHolder가 관리하는 아이템의 위치.
            현재 ViewHolder가 selectedPosition의 아이템을 관리하고 있는지 확인.
            
            ⭐⭐⭐ 
            recycelrView는 뷰를 재활용하므로 꼭 확인해야함. 안그러면 재활용 된거와 전의 뷰 홀더 구분을 못하는거 같음
             
            */   
             
                if (selectedPosition == absoluteAdapterPosition) {
                   // 선택한것으로 표시
                } else {
                    // 선택 취소하기
                }
                // 날짜 클릭이벤트가 발생했을 경우 
                if (onCalendarItemClickListener != null) {
                    binding.txtDay.setOnClickListener {
                        onCalendarItemClickListener?.onItemClick(item, absoluteAdapterPosition)
                        if (selectedPosition != absoluteAdapterPosition) {
                            binding.setChecked(false)
                            notifyItemChanged(selectedPosition)
                            selectedPosition = absoluteAdapterPosition
                            // 기존 선택한 날찌 체크 풀기
                            selectedYearMonthDay.selectedYearMonth = null
                            selectedYearMonthDay.selectedDay = null
                        }

                    }
                }
            }
            
            if (startDate.selectedYearMonth.isNullOrEmpty() && startDate.selectedDay.isNullOrEmpty()) {
            	// 날짜 선택 x 경우 - 현재날짜보다 전날인경우 회색    
            } else {
                // 날짜 선택 o 경우 - 시작날짜 포함 전 날짜들은 회색 처리 +todo("현재날짜보다 전날인경우 회색")
                if (item.infoDay.isNotEmpty()) {
                    
                }
            }
        }
    }
    
    // 외부(CalendarFragment)에서 클릭 이벤트 처리하기 위함
    fun setOnCalendarItemClickListener(listener: OnCalendarItemClickListener) {
        onCalendarItemClickListener = listener // listener는 CalendarFragment에서 전달받은 것 
    }
    // 총 몇개의 데이터가 나타나야하는지 RecyclerView에게 알려주기 (viewHolder개수 아님)
     override fun getItemCount(): Int {
        return daysInfo.size
    }

    private fun CalendarCellItemBinding.setChecked(selected: Boolean) {
       // 선택된 날짜 색 바꾸기
    }


    private fun CalendarCellItemBinding.setUncheked() {
       // 시작 날짜 이전 날짜는 선택 못하게 하기 
    }

}

interface OnCalendarItemClickListener {
    fun onItemClick(item: InfoOfDays, position: Int)
}

 

 

 

파일명 : CalandarFragment.kt

 

이전(스터디 생성) 화면

 

시작하는 날 혹은 종료하는 날 박스를 클릭하면 달력이 나타나요. 아래 코드의 bundle로 시작하는 날과 끝 날짜로 구분해요

package kr.co.gamja.study_hub.feature.studypage.createStudy
import ...

class CalendarFragment : BottomSheetDialogFragment() {

    private lateinit var binding: FragmentCalendarBinding
    private val viewModel: CreateStudyViewModel by activityViewModels()
    
    private lateinit var selectedDate: LocalDate// 년 월
    private lateinit var today: String // 오늘 날짜
    private lateinit var currentYearMonth: String // 오늘이 속한달
    private lateinit var changedYear: String // 바뀐 년
    private lateinit var changedMonth:String // 바뀐 달
    private lateinit var formattedDate: String // 포멧한 값
    private lateinit var whatDay: String // 시작인지 끝날짜인지 구분 tag(스터디 생성에서 가져옴)
    private lateinit var changedYearMonth:String // 지난날짜 회색처리 위한 바뀐 날짜 yyyyMM
    var newSelectedYearMonth=InfoOfSelectedDay(null,null) // 선택된 날짜
    private lateinit var newStartDate:StartDate// 시작 날짜 < 끝 날짜
    
    
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_calendar, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.viewModel = viewModel
        binding.lifecycleOwner = viewLifecycleOwner
        
        val receiveBundle = arguments // 이전(스터디 생성: 위 사진) 화면에서 받아오는 값(달력Fragment 하나로 사용하기 때문)
        var startYearMonth =""
        var startDay = ""
        
        if (receiveBundle != null) {
            val value = receiveBundle.getString("whatDayKey")
            startYearMonth= receiveBundle.getString("startYearMonth").toString()
            startDay=receiveBundle.getString("startDay").toString()
            newStartDate=StartDate(startYearMonth,startDay)
            if (value != null) whatDay = value
            else Log.e(tag, "시작날짜인지 끝인지 못받아옴")
        }
        if(whatDay=="0"){ // 시작 날짜 선택이면
            selectedDate = LocalDate.now()
            today = LocalDate.now().dayOfMonth.toString()
            currentYearMonth = toYearMonth(LocalDate.now())
        }else{ // 끝 날짜 선택이면 
            val combined = "$startYearMonth$startDay"
            val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
            selectedDate =LocalDate.parse(combined,formatter)
            today = selectedDate.dayOfMonth.toString()
            currentYearMonth = startYearMonth
        }

        setMonthView() // 달력 업데이트
        
        // 이전 달 보기 버튼
        binding.btnLeft.setOnClickListener {
            selectedDate = selectedDate.minusMonths(1)
            setMonthView() // 달력 업데이트
        }
        // 다음 달 보기 버튼
        binding.btnRight.setOnClickListener {
            selectedDate = selectedDate.plusMonths(1)
            setMonthView() // 달력 업데이트
        }
        
        binding.btnOk.setOnClickListener {
        	// 날짜 선택 완료
        }

    }
	
    // ✔️ 달력 업데이트 함수 
    fun setMonthView() {
        binding.txtMonth.setText(monthYearFromDate(selectedDate)) // "yyyy-MM" 달력 상단 년도 월 표시
        
        val newDayList = daysInMonthArray(selectedDate) // 해당 달에 해당하는 "일"들 만들어서 리턴 
        
        val adapter = CalendarAdapter(requireContext()).apply {
        
            setOnCalendarItemClickListener(object:OnCalendarItemClickListener{
                override fun onItemClick(item: InfoOfDays, position: Int) {
                    
                    newSelectedYearMonth=InfoOfSelectedDay(item.yearMonthDay,item.infoDay) // 선택된 날짜
                    formattedDate = "${changedYear}년 ${changedMonth}월 ${item.infoDay}일" // 이전 화면 표시용 날짜
                    binding.btnOk.isEnabled = true
                    binding.btnOk.setTextColor(ContextCompat.getColor(requireContext(), R.color.O_50))
                }
            })
        }
        val infoOfDaysList:ArrayList<InfoOfDays> =ArrayList()
        for(day in newDayList){ // day 는 1,2 ~, daysInMonthArray함수에서 받은 ArrayList<String> -> ArrayList<InfoOfDays> 맞추기 위해 
            val info = InfoOfDays(yearMonthDay = changedYearMonth, infoDay =day, isSelected = false)
            infoOfDaysList.add(info)
        }
        adapter.daysInfo=infoOfDaysList
        adapter.currentYearMonth=currentYearMonth // yyyyMM
        adapter.currentDay=today // 오늘 날짜 표시 
        adapter.selectedYearMonthDay=newSelectedYearMonth // 선택된 년도 달
        adapter.startDate=newStartDate // 시작 날짜
        
        binding.recyclerDay.adapter = adapter
        binding.recyclerDay.layoutManager = GridLayoutManager(requireContext(), 7)
    }
    
    
    
    // ✔️ 달력 만들기
    fun daysInMonthArray(newDate: LocalDate): ArrayList<String> {
        val newDayList = ArrayList<String>() // 달력 "일" 리스트 
        val newYearWithMonth = toYearMonth(newDate) // yyyyMM 형식으로 변경
        changedYear=getOnlyYear(newDate) // yyyy 형식으로 변경
        changedMonth=getOnlyMonth(newDate) //MM 형식으로 변경 
        changedYearMonth=toYearMonth(newDate) // yyyyMM
        val newMonth = YearMonth.from(newDate) // 2023-10 형식
        
        val firstDayOfMonth = selectedDate.withDayOfMonth(1)
        val lastDayOfMonth = newMonth.lengthOfMonth()
        val dayOfWeek = firstDayOfMonth.dayOfWeek.value

        // 지난달 조회 불가 < 화살표 예) 202310>202409
        if (newYearWithMonth.toInt() > currentYearMonth.toInt()) {           
        } else {
        
        ...
        
        }
        // 7*6 자리로 달력 만들기 
        for (i in 1 until 42) {
            if (i <= dayOfWeek || i > lastDayOfMonth + dayOfWeek)
                newDayList.add("")
            else newDayList.add((i - dayOfWeek).toString())
        }
        return newDayList
    }
   
}

data class InfoOfDays(
    var yearMonthDay:String, // yyyyMM
    val infoDay:String,
    var isSelected:Boolean
)

 

 

필요없는 코드들 최대한 뺐지만... 여전히 복잡해보이네요🥹🥹

 

코드에 붙인 이모티콘 위주로 봐주시면 됩니다!