커스텀 캘린더 만들기 - 1.하이라이트 애니메이션

K_Gs·2023년 6월 4일
3

기능구현

목록 보기
1/3
post-thumbnail

서론

우리 앱에는 캘린더가 들어간다.
와이어 프레임상으로는 대략적으로 아래와 같이 생겼다.

해당 디자인은 material datepicker에 붙어있던 달력을 때다가 와이어프레임용으로 붙여둔 것이다.

확인해봤을때 따로 material calendar 같은건 없는 것 같고, 기능도 몇개 바꿔야해서 원래 있던 캘린더를 상속하기 보단 새로 만들어보기로 하였다.

준비

일단 캘린더 자체가 아니라 조금 부가적으로 표시되는 정보로는

일정이 있는 날에는 날짜 위쪽에 작은 동그라미
오늘 날짜에는 속이 빈 동그라미
선택한 날짜에는 속이 찬 동그라미

이정도가 있다.

캘린더를 자체를 만드는건 시간이 부족하기에 다음에 하기로 하고 일단 뷰 위에 동그라미를 그려넣는 것 부터 하기로 하였다.

Canvas

기본적으로 커스텀뷰는 onDraw를 오버라이드하면 뷰가 어떻게 화면에 그려질지 정할 수 있다.

override fun onDraw(canvas: Canvas?) {
        canvas?.drawCircle(50f, 50f, size, paint);
}

이렇게 하면 해당뷰 좌측상단을 기준으로 (50,50)위치에 size 만큼의 크기를 가지는 원이 그려진다.

원을 그릴 수 있게 되었으니 코드를 조금 수정하고 onTouchEvent를 상속하여 클릭시 뷰 중앙에 원이 그려지게 해보았다.

override fun onDraw(canvas: Canvas?) {
	if(isSelcet){ 
        canvas?.drawCircle(width/2f, height/2f, size, paint)
    }
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
	when(event?.action){
    	MotionEvent.ACTION_DOWN -> {
        	isSelect = true
            invalidate()
        }

        else -> {}

    }

    return true
}


터치를 하면 원이 그려지긴 한다.
(속이 빈 원은 paint의 속성을 변경하여 fill을 없애고 stroke 설정을 하면 된다.)

밋밋한데

그런데 원이 뿅하고 나타난다.
그리고 isSelect가 false가 된 후에 사라지는 것도 갑작스럽게 사라진다.

기능상으로는 문제가 없고, 와이어프레임대로 된 것 이지만 조금 아쉬운 느낌이 들었다.

이거 애니메이션 집어넣을 수 없나?

ValueAnimator

속성 애니메이션

찾아보니 ValueAnimator라는게 있다는 걸 알게되었다.

a값에서 b값으로 지속시간동안 천천히 값을 변화시킨다.
또 반복 횟수, 애니메이션이 끝나고 다시 원상태로 돌아올지 등을 지정할 수 있다.

ValueAnimator의 생성은 아래와 같다.

ValueAnimator.ofFloat(0f, 50f)
//실행시 0f에서 50f로 값이 변화
//ofInt, ofObject 등도 있다.

이것을 사용하여 (0,50)을 집어 넣고 그 사이의 변하는 값을 원의 size로 저장, 그때마다 invalidate를 호출하여 원을 새로 그리기로 하였다.

또 바로 시작하는 것이 아니라 터치시 시작해야하기에 처음 init에서 객체를 생성해 저장해두고 사용했다.

init {
    startAnimator = ValueAnimator.ofFloat(0f, 50f).apply {
        addUpdateListener {
            size = it.animatedValue as Float
            postInvalidate()

        }
        duration = 100L
        repeatCount = 0
        interpolator = AccelerateDecelerateInterpolator()
    }
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
    when(event?.action){
        MotionEvent.ACTION_DOWN -> {
            startAnimator?.start()
        }

        else -> {}

    }

    return true
}

addUpdateListener : 값이 변경되었을때 호출
duration : 애니메이션 재생 시간
repeatCount : 반복횟수
interpolator : 보간


GIF라 그다지 티가 안나지만 애니메이션이 잘 작동한다.

하이라이트?

그런데 지금 이런 뷰가 여러개 있으면 전부 다 하이라이트 될 수 있다.

이런걸 막기 위해 이 calendar item을 관리하는 무언가를 만들어두고 거기서 highlight된 객체의 참조를 가지고 있다가

새로운 item이 highlight되면 기존 객체에서 원(highlight)을 없애는 애니메이션을 실행시키고자 하였다.

일단 원이 줄어드는 애니메이션을 만들고, 해당 애니메이션을 실행할 메소드도 만든다.

init {
        startAnimator = ValueAnimator.ofFloat(0f, 50f).apply {
            addUpdateListener {
                size = it.animatedValue as Float
                postInvalidate()
            }
            duration = 100L
            repeatCount = 0
            interpolator = AccelerateDecelerateInterpolator()
        }

        endAnimator = ValueAnimator.ofFloat(50f, 0f).apply {
            addUpdateListener {
                size = it.animatedValue as Float
                postInvalidate()
            }
            duration = 100L
            repeatCount = 0
            interpolator = AccelerateDecelerateInterpolator()
        }
}

private fun endAnimation(){
    endAnimator?.start()
}

원래라면 이 calendar item들을 담을 custom calendar에서 어떤 객체가 하이라이트인지 저장하겠지만 지금은 그런게 없으니 일단 Object 클래스안에 만들어둔다.

object nowDate {
    var now : CustomCalendarItemView? = null
}

그리고 item이 터치되었을때 체크하고 변경하도록 한다.

override fun onTouchEvent(event: MotionEvent?): Boolean {
        when(event?.action){
            MotionEvent.ACTION_DOWN -> {
                if(nowDate.now != null){
                    if(nowDate.now == this) return true
                    nowDate.now?.endAnimation()
                }
                nowDate.now = this
                startAnimator?.start()
            }

            else -> {}

        }

        return true
}

잘 작동한다!

profile
~(~o~)~

1개의 댓글

comment-user-thumbnail
2023년 6월 4일

흥미롭네요,,! 잘보고 갑니다!!

답글 달기