커스텀 캘린더 만들기 - 2.item class

K_Gs·2023년 8월 5일
1

기능구현

목록 보기
2/3
post-thumbnail

1-하이라이트 애니메이션

하이라이트는 만들었고!

이전에 아이템을 눌렀을 때 하이라이트되는 효과는 만들었다.
이번에는 본격적으로 캘린더 아이템을 만들면서 하이라이트나 필요한 여러기능을 넣어보자!

상세한 명세

상세한 디자인 명세가 나오면서 어떤 기능이 어떻게 필요한지 정의할 수 있게 되었다.

  • 요일, 날짜가 표시된다.
  • 토요일은 파랑색, 일요일은 빨간색이다.
  • 오늘 날짜는 핑크색 글씨에 핑크 테두리 원이 그려진다.
    이 원은 사라지지 않고, 하이라이트가 되었다 풀려도 남아있다.
  • 일정이 있는 날에는 노랑색으로 작게 원이 표시된다.
    이 원은 하이라이트보다 위에 그려진다.
  • 하이라이트 된 날짜는 핑크색 원이 그려지고, 글씨가 흰색이 된다.
  • 요일, 날짜가 없는(32일에 해당하는 아이템 or 0일에 해당하는 아이템)은 눌러도 하이라이트 되어선 안된다.
  • 월이 바뀌어 달력이 초기화될 때 모든 아이템이 올바르게 표시되어야 한다.

세팅

일단 텍스트, 원들을 그릴 페인트를 세팅해준다.

private val highlightPaint = Paint().apply {
        color = resources.getColor(R.color.base_primary_color)
}

private val todayPaint = Paint().apply {
        color = resources.getColor(R.color.base_primary_color)
        style = Paint.Style.STROKE
        strokeWidth = 3f
}
private val textPaint = Paint().apply {
        textAlign = Paint.Align.CENTER
        typeface = resources.getFont(R.font.base_font)
}
private var iconPaint = Paint().apply {
        color = resources.getColor(R.color.base_calendar_dot_color)
}

deprecated야!
getColor(id)는 deprecated되었다. getColor(id, theme)를 써야하지만 여기선 일단 전자를 사용한다.

그리고 원을 어느정도 크기로 그릴지, 텍스트를 어느정도 크기로 적을지도 정해야한다.
원 사이즈는 그릴때 인자로 넘기기에 변수로 만들고, 텍스트 사이즈는 페인터에 적용되기에 만들지 않는다.


private var iconSize = 0f
private var maxCircleSize = 0f
private var highlightSize = 0f
        set(value) {
            field = value
            postInvalidateOnAnimation()
        }    

maxCircleSize와 highlightSize
하이라이트 원의 크기와 오늘 날짜를 표시하는 원(테두리)의 크기는 같다. 그렇기에 같은 maxCircleSize로 원의 크기 최대치를 정해둔다.
highlightSize의 경우 선택되면 value animator에서 원 크기를 바꾸고, 원 크기가 바뀔때마다 뷰를 다시 그려야하기에 set(value)에 postInvalidateOnAnimation를 넣었다.

이 변수의 초기화는 뷰의 크기가 정해지고 난 뒤에 뷰 크기의 80% 이런식으로 정하는게 맞다 생각하여 변수로 정의해 두고 init에서 처리하였다.

init {
    this.post {
        maxCircleSize = min(height, width) / 10f * 4f
        iconSize = min(height, width) / 10f * 1f
        textPaint.textSize = min(height, width) / 10f * 4f
        ...

this.post
View 클래스의 post라는 함수이다.
현재 커스텀 뷰를 만들기에 부모에서 정의된 post를 this로 쓸 수 있다.
기능은 뷰 안의 UI 쓰레드에서 이 기능을 수행해라인데, 여기서는 뷰의 높이, 너비등이 다 정의되고 수행되는 특성을 활용해 높이, 너비 값을 사용하기 위해 사용했다.

이렇게 크기는 세팅되었다.

또 필요한 변수로는 날짜 텍스트, 요일 여부(요일을 표시하는 아이템인지), 요일 텍스트, 일정 정보, 오늘 날짜 여부 등이 있다.

private var weekText = ""
private var dayNumber = 0
private var eventList = listOf<BottomDetailDTO>()
private var isWeek = false
private var isToday = false

(지금 와서 보니 이벤트 리스트는 타입을 제네릭으로 바꾼다면 조금 더 확장성있을 듯하다.)

하이라이트 적용하기

이제 필요한 변수들이 세팅되었으니 하이라이트를 가져올 수 있다.
원래 init에서 초기화하던 startAnimator와 endAnimator를 가져와 this.post 안에서 초기화한다.

//init -> this.post
startAnimator = ValueAnimator.ofFloat(0f, maxCircleSize).apply {
    addUpdateListener {
        highlightSize = it.animatedValue as Float
        textPaint.color = Color.WHITE
    }
    duration = CIRCLE_DURATION
    repeatCount = 0
    interpolator = AccelerateDecelerateInterpolator()
}

endAnimator = ValueAnimator.ofFloat(maxCircleSize, 0f).apply {
    addUpdateListener {
        highlightSize = it.animatedValue as Float
        if (isToday) {
            textPaint.color = resources.getColor(R.color.base_primary_color)
        } else {
            textPaint.color = resources.getColor(R.color.base_font_color)
        }
    }
    duration = CIRCLE_DURATION
    repeatCount = 0
    interpolator = AccelerateDecelerateInterpolator()
}

날짜가 오늘인 경우 하이라이트가 풀렸을때 검정색 텍스트가 아니라 핑크색으로 돌아와야한다.
그렇기에 endAnimator에서만 확인해준다.

onDraw또한 가져오고 조건에 맞춰 변경시킨다.

  1. 요일이라면 요일만 표시한다.
  2. 요일이 아니고, 날짜가 0이라면 날짜가 없는걸로 취급해 그리지 않는다.
  3. 요일이 아니고, 날짜가 0이 아니라면 highlightSize에 따라 원을 그리고 텍스트도 그린다.
  4. 만약 날짜가 오늘이라면 핑크색 테두리 원을 그린다.
override fun onDraw(canvas: Canvas?) {
    if (isWeek) { // 1
        canvas?.drawText(
            weekText,
            width / 2f,
            ((height / 2) - ((textPaint.descent() + textPaint.ascent()) / 2)),
            textPaint
        )
        return
    }

    if (dayNumber <= 0) return // 2

    if (isToday) { // 4
        canvas?.drawCircle(width / 2f, height / 2f, maxCircleSize, todayPaint)
    }
    
    //이하 3
    canvas?.drawCircle(width / 2f, height / 2f, highlightSize, highlightPaint)
    if (eventList.isNotEmpty()) {
        canvas?.drawCircle(width / 2f, height / 2f - maxCircleSize, iconSize, iconPaint)
    }
    canvas?.drawText(
        dayNumber.toString(),
        width / 2f,
        ((height / 2) - ((textPaint.descent() + textPaint.ascent()) / 2)),
        textPaint
    )
}

이제 터치 이벤트에 따라 애니메이터를 실행시키면 된다.

부모와 연결

포커스된 아이템은 부모에서 관리한다(custom calendar)
(이 부모는 상속관계의 부모가 아니라 아이템을 가질 클래스를 말한다)

아이템이 눌렸을때 포커스를 바꾸고, 포커스가 이미 되어있는 아이템이라면 아무 일도 없어야한다.
그렇기에 부모의 포커스된 아이템이 무엇인지 알아야하고, 이는 부모를 알아야 한다고 할 수 있다.
부모와 연결하는 함수를 만든다.

private lateinit var parent: CustomCalendar

//반드시 실행 되어야 함.
fun setParent(nowParent: CustomCalendar) {
    parent = nowParent
}

부모에도 포커스 아이템이 뭔지 알려주고, 세팅할 수 있게 getFocusedItem, setFocusedItem을 만들어둔다.
(setFocusedItem은 아이템이 눌렸다는 의미이기도 하므로 이벤트 리스트를 같이 보낼 수 있게 해뒀다)

이제 아이템이 눌렸을 때 부모로 부터 포커스 정보를 받아와 처리할 수 있다.

override fun onTouchEvent(event: MotionEvent?): Boolean {
    if (dayNumber <= 0 || isWeek) return true

    when (event?.action) {
        MotionEvent.ACTION_DOWN -> {
            val focused = parent.getFocusedItem()
            if (focused != null) {
                if (focused == this) return true
                focused.startAnimator?.cancel()
                focused.endAnimator?.start()
            }
            parent.setFocusedItem(this, eventList)
            endAnimator?.cancel()
            startAnimator?.start()
        }
    }

    return true
}
  • 요일, 날짜가 없는(32일에 해당하는 아이템 or 0일에 해당하는 아이템)은 눌러도 하이라이트 되어선 안된다.

를 위해 처음에 if문으로 걸러준다.

포커스 된 아이템이 바뀌었다면 startAnimator를 실행해준다.
또한 포커스가 풀린 아이템은 endAnimator를 실행해준다.

cancel

  • start와 end는 별도의 애니메이터이기에 동시 실행 될 수 있다.
    end ->(end실행 중) start 하면 원이 50 -> 0 -> 49 -> 1 ... 이런식으로 변한다.
  • 한 애니메이터는 한번씩만 실행된다.
    end -> (end실행 중) start -> (end,start실행 중) end 하면 마지막 end는 실행되지않고 둘 중 더 늦게 실행된 start의 상태가 최종 상태이다.

이는 focus된 아이템만 start이고, 나머지는 다 end상태여야 하는 상황에서 focus가 두개 인 것 처럼 보이게 될 수 있다.
그렇기에 end를 실행하면 start를 cancle해주고 start를 실행하면 end를 cancel해준다.


이렇게 해서 아이템 class의 세팅이 끝났다!

profile
~(~o~)~

2개의 댓글

comment-user-thumbnail
2023년 8월 5일

글 재미있게 봤습니다.

답글 달기
comment-user-thumbnail
2023년 8월 6일

흥미롭게 읽었습니당

답글 달기