KUIT-앱 개발 프로젝트 동아리

프로젝트 개발 로그-1주차

농농씨 2024. 1. 15. 21:44

1주차 맡은 역할 : 리니어 캘린더 클릭시 캘린더 확장 기능 구현

 

목표 레이아웃

 

만든 기능

-다이얼로그로 화면 띄우기

: DialogFragment 클래스 이용, layout파일에 전체를 constraintLayout으로 한번더 감싼 다음 drawable로 style 적용해서 테두리 둥글게 함

-리사이클러뷰로 캘린더 만들기

-화살표로 지난달/다음달 이동하기

:클릭함수 및 LocalDate 클래스의 plusMonths() 함수와 minusMonths() 함수 이용

-'오늘'버튼 누르면 오늘 날짜로 세팅

-지난달/다음달 날짜 미리보기 및 글자 흐리게

: 데이터 클래스에 지난달/이번달/다음달 정보 넣어서 어댑터에서 설정

-알약 이미지 넣기(아직 db가 없으므로 일시적으로 데이터 몇개 바꿔서 처리)

-오늘 날짜에 색깔 넣기

-클릭하면 색깔 바뀌게

 

학습 기록

 

1. for 문에서 특정 조건을 만족하는 i를 반환하고 싶었는데 막혔었다.

그 i값을 저장할 변수를 for문 안에 선언해서 안되는 거였다.

var ii = 0
if (item.isSelected == false) { // 선택 안된 것을 선택할 때
     for (i in dayList.indices) { // indices 속성 이용하여 인덱스와 for문으로 list 출력 
         if (dayList[i].isSelected == true) { // 기존의 아이템은 선택 해제(1개만 선택할 수 있도록)
             dayList[i].isSelected = false
         }
         ii = i // 선택해제된 아이템의 position 저장
}

 

몇시간을 고민하던게 괄호 하나의 위치라는 게 약간 허무하기도 하지만? 하다보면 이것도 빨리 찾아낼 수 있는 여유가 생기겠지 라는 마음가짐..

파트장님께서 payload라는 방식도 추천해주셨는데 시간이 되면 그것도 시도해볼 예정

 

2. 리사이클러뷰 선택시 다른 아이템은 선택해제

2-1. 우선 데이터 클래스로 선택 여부를 저장할 변수를 선언했다.

data class Day(
    var date: String, // 날짜 정보
    var month: String, // 지난달, 이번달, 다음달 정보
    var isSelected: Boolean = false, // 선택 여부
    var achieved: Boolean = false // 섭취 달성 여부
):Serializable

 

2-2. 오늘 날짜에는 이미 선택돼있도록 한다

// 오늘 날짜만 선택 상태로 변경
var nowDate = LocalDate.now().dayOfMonth.toString()
        for (i in dayList.indices) {
            if (dayList[i].date == nowDate && dayList[i].month == "now") {
                if (dayList[i].isSelected == false) {
                    dayList[i].isSelected = true
                }
            }
        }

LocalDate 라는 함수가 있어서 오늘의 일(date)정보를 얻어와서 for문과 indices를 이용해 오늘날짜는 '선택됨'상태로 변경해주었다.

대신 이 경우에는 지난달, 다음달로 넘어갔을 때에도 오늘 날짜가 선택된다. 예를들어 오늘이 1월 15일이면 2월 캘린더로 넘어가도 15일이 선택되어있는 상태이다.

(수정할 수도 있다.)

 

2-3. 클릭시 선택된 아이템과 선택해제된 아이템의 index 정보를 저장해서 recyclerView 갱신

// 클릭시 선택
dateAdapter.itemClickListener = object : DateAdapter.OnItemClickListener {
            override fun onItemClick(position: Int) {
                var item = dayList[position]

                if (item.month == "pre") { // 지난달 날짜 클릭시 이동
                    selectedDate = selectedDate.minusMonths(1)
                    setMonthView()
                } else if (item.month == "next") { // 다음달로 이동
                    selectedDate = selectedDate.plusMonths(1)
                    setMonthView()
                }

                // 선택해제된 아이템의 position을 저장할 변수 선언
                var ii = 0

                // 선택 안된 것을 선택할 때
                if (item.isSelected == false) {
                    for (i in dayList.indices) {
                        // 기존의 아이템은 선택 해제(1개만 선택할 수 있도록)
                        if (dayList[i].isSelected == true) {
                            dayList[i].isSelected = false
                            // 선택해제된 아이템의 position 저장
                            ii = i
                        }
                    }

                    // 선택한 아이템의 상태 변경
                    item.isSelected = true

                    // 선택한 item과 선택해제된 item의 position 전달해서 리사이클러뷰 갱신
                    dateAdapter.notifyItemChanged(ii)
                    dateAdapter.notifyItemChanged(position)
                } else { // 이미 선택된 것을 선택하면 선택해제
                    item.isSelected = false
                    dateAdapter.notifyItemChanged(position)
                }
                Toast.makeText(context, "${item} 클릭함", Toast.LENGTH_SHORT).show()
                Log.d("dayList", "${dayList[position].date}:${dayList[position].isSelected}")
                Log.d("dayList", "${dayList[ii].date}:${dayList[ii].isSelected}")
                Log.d ("fulldayList", "$dayList")
            }
        }

 

하나하나 살펴보자면

selectedDate: LocalDate 클래스 타입의 변수이고 오늘 날짜를 저장하는 변수이다.

setMonth() 함수: 요일 리스트(var dayList)를 작성하고 리사이클러뷰 어댑터를 초기화 하는 등의 작업을 작성한 함수이다.

// 선택해제된 아이템의 position을 저장할 변수 선언
var ii = 0

// 선택 안된 것을 선택할 때
if (item.isSelected == false) {
for (i in dayList.indices) {
// 기존의 아이템은 선택 해제(1개만 선택할 수 있도록)
if (dayList[i].isSelected == true) {
dayList[i].isSelected = false
// 선택해제된 아이템의 position 저장
ii = i
}
}
// 선택한 아이템의 상태 변경
item.isSelected = true

item은 내가 클릭한 아이템을 의미한다.(ex. 27일 클릭)

isSelected는 데이터 클래스에 선언한 변수로서, 선택여부를 의미한다.

만약 내가 선택한 아이템이 선택이 안돼있었다면, 기존에 선택된 아이템은 "선택안됨"상태로 변경한다. 이때도 for 문의 indices를 쓴다.

그리고 선택한 아이템은 "선택됨" 상태로 바꿔준다.


// 선택한 item과 선택해제된 item position 전달해서 리사이클러뷰 갱신
dateAdapter.notifyItemChanged(ii)
dateAdapter.notifyItemChanged(position)
} else { // 이미 선택된 것을 선택하면 선택해제
item.isSelected = false
dateAdapter.notifyItemChanged(position)
}

 

그리고 선택된 아이템과 선택해제된 아이템의 index를 어댑터의 notifyItemChanged() 함수를 이용하여 알리고 리사이클러뷰를 갱신하도록 한다.

참고로 notifyDataSetChanged() 라는 함수도 있지만, 데이터 전체가 유효하지 않다고 가정하고 전체를 갱신하는 거라서 비효율적이므로 가능하다면 일부만 바꾸는 것을 추천한다.

} else { // 이미 선택된 것을 선택하면 선택해제
item.isSelected = false
dateAdapter.notifyItemChanged(position)
}

이번에는 만약 내가 선택한 아이템이 이미 선택된 상태였다면 "선택안됨" 상태로 바꿔주고 다시 notifyItemChanged()함수로 리사이클러뷰를 갱신한다.

 

3. 어댑터에서 스타일 적용

고려해야할 것은 총 세가지였다.

  • 이번달인지 아닌지-글씨색
  • 선택 됐는지 안됐는지-배경색,글씨색
  • 알약 섭취 정보가 등록되어있는지-일(date)숫자 대신 알약이미지
// 이번달이 아니면 회색으로 처리
if (item.month == "now"){
    // 선택 여부에 따른 drawable 적용
    if (item.isSelected == true) {
        binding.clItemCalendar.setBackgroundResource(R.drawable.bg_shape_selected)
        binding.tv.setTextColor(ContextCompat.getColor(binding.tv.context, R.color.white))
    } else {
        binding.clItemCalendar.setBackgroundResource(R.color.white)
        binding.tv.setTextColor(ContextCompat.getColor(binding.tv.context, R.color.black))
    }
} else {
    binding.clItemCalendar.setBackgroundResource(R.color.white)
    binding.tv.setTextColor(ContextCompat.getColor(binding.tv.context, R.color.gray4))
}

우선 이번달이면 선택 여부에 따라 setBackgroundResource 함수를 이용하여

이런 이미지를 넣어주었다.

참고로 binding.tv에서 tv는 아이템레이아웃의 TextView를 의미하고 clItemCalendar는 아이템 레이아웃의 전체를 의미한다.

TextView에 drawable을 background로 적용했더니 글자크기에 딱맞게 타원이 되었기 때문이다.

그래서 아이템 전체를 정사각형으로 맞추고 id를 설정해서 drawable을 적용했다.

 

// 일시적으로 알약 이미지 적용 위해 데이터 변경
if (item.date == "1" || item.date == "30") {
    item.achieved = true
}

// 섭취 달성 상태일 때
if (item.achieved == true) {
    if (item.month == "now") { // 이번달이면 초록 알약 이미지 적용
        binding.iv.setImageResource(R.drawable.img_achieve)
        binding.tv.setTextColor(ContextCompat.getColor(binding.tv.context, com.google.android.material.R.color.mtrl_btn_transparent_bg_color))

    } else { // 이번달이 아니면 회색 알약 이미지 적용
        binding.iv.setImageResource(R.drawable.img_achieve_disable)
        binding.tv.setTextColor(ContextCompat.getColor(binding.tv.context, com.google.android.material.R.color.mtrl_btn_transparent_bg_color))
    }
} else {
    binding.iv.setImageResource(0)
}

아직 섭취 데이터가 없어서 data class에 섭취달성 여부를 나타내는 변수를 선언하고

특정 날짜 몇개에 "섭취달성" 상태를 설정했다.

그리고 달성했다면

-if 이번달일때

초록알약이미지를 넣었다

-if 이번달 아닐때

회색 알약 이미지를 넣었다.

 

달성안했다면

하얀색배경을 설정했다.

 

<오류노트>

사실 setImageResource(0)으로 아무것도 안넣은 상태로 넣어도 되는데

무슨이유인지... 한 날짜를 여러번 클릭하면 캘린더의 맨왼쪽 맨위의 아이템(dayList의 첫번째 요소)이 선택됨 상태가 되어서 곤란했다. 

로그로 확인해봐도 isSelected 값은 false여서 초록 drawable이 적용될 일이 없는데.. 혼자 갱신되는 것 같았다.

일단은 하얀색 drawable을 적용했다.

 

4. 이미지 적용

Figma에서 알약 이미지 컴포넌트를 넣으려다가,

backBackgroundResource를 사용하니 이미지를 크게 넣어도 안 변하는 걸 깨달았다.

알고보니 background에 적용되는 이미지는 무조건 배경에 꽉차게 적용되는 거였다.

그래서 ImageView를 선언해서 textView 뒤에 겹쳐있게 하고 레이아웃 상에서 크기를 조정했다.

 

결과는 다음과 같다.

 

 

 

 

참조

아이템클릭 참고

https://uknowblog.tistory.com/125

 

https://superwony.tistory.com/146

뷰페이저 활용…?(아직 사용 x)

 

https://aries574.tistory.com/345

캘린더 구세주…

 

https://yoon-dailylife.tistory.com/12

setTextColor

 

https://kadosholy.tistory.com/55

리사이클러뷰 갱신

 

https://kkangsnote.tistory.com/65

for문으로 리스트 다 출력(indices)

 

https://learn.microsoft.com/ko-kr/dotnet/api/system.windows.controls.primitives.selector.isselected?view=windowsdesktop-8.0

https://zzandoli.tistory.com/9

Selector 속성…?(사용안함

 

디자인 어셋 파일 명명 가이드

https://blog.cracker9.io/2018/04/01/Design_Asset_Name_Guide/

 

https://itjy2.tistory.com/183

폰트설정