[Android] ProgressBar 증/감소 애니메이션 쉽게 하기

양현진·2022년 6월 23일
2

Oh My Android

목록 보기
2/22
post-thumbnail

Android의 프로그래스바란 어떤 상태줄을 표시할 수 있는 widget이다.


주로 이용했던 경우는 서버에서 펌웨어를 다운받을 때 진행상태를 사용자에게 알려주거나, 설문 조사에서 어디쯤 완료했는지, 얼마나 설문이 남았는지 같은 경우에 이용했다.

위 그림을 보면 자연스럽게 생각나는 것이 바로 상태가 변할 때 마다 애니메이션을 주면 더 좋은 UX를 제공하지 않을까 싶어 여태 여러가지 도전을 해봤다.

처음엔 Lottie에서 뒤적거리면 원하는 모양이 나오지 않을까 싶었지만 원하는 모양을 구하기가 힘들었다.

그 다음엔 아주 단순하게 생각해낸 것이 for문을 이용해서 progress를 진행시키면 어떨까 싶어 구현해봤다.

결과는 아주 이상했다

우선 눈에 보기에 너무 버벅거렸고, 원하는 시간에 애니메이션을 실행하기 위해 Thread.sleep을 주면 예상보다 훨씬 늦게 for문이 끝나 시간마저 조절하기 애매했다.

결론은 RxJava다. 요즘 Rx를 열심히 파고있었는데 interval이라는 함수를 공부하다 문득 생각이 나 프로그래스바의 애니메이션을 만들어보기로 했다.

구글링을 하다보면 여러가지 방법들이 있는데 그 중에서도 가장 간단하고 커스텀하기도 쉬운 방법이라 생각한다. 물론 RxJava를 dependencies에 추가하긴 해야한다.

private fun startAnimation() {
    binding.run {
        btn.setOnClickListener {
            val currentProgress = prg.progress
            getInterval().subscribe {
                prg.progress = currentProgress + it.toInt()
            }
        }
    }
}

private fun getInterval(): Observable<Long> =
    Observable.interval(1L, TimeUnit.MILLISECONDS).map { interval ->
        interval + 1
    }.take(100)

이벤트를 콜하는 버튼 클릭 리스너를 제외하면 코드가 7줄밖에 안된다. 댑악

추가로 위와같은 progressbar이 색과 모양을 주는 xml코드

drawable

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:id="@android:id/background">
        <shape>
            <solid android:color="#96A6FA" />
            <corners android:radius="5dp" />
        </shape>
    </item>
    <item android:id="@android:id/progress">
        <clip>
            <shape>
                <corners android:radius="5dp" />
                <solid android:color="#1B5AF4" />
            </shape>
        </clip>
    </item>

</layer-list>

activity_main

<ProgressBar
        android:id="@+id/prg"
        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
        android:layout_width="0dp"
        android:layout_height="7dp"
        android:max="1000"
        android:progress="100"
        android:progressDrawable="@drawable/progressbar_color"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.3"
        app:layout_constraintWidth_percent="0.8" />

...이게 실기기로 보면 엄청 부드러운데 gif파일이라 많이 버벅여 보인다. 실제로는 매우 부드럽다.

커스텀을 위해 코드를 살펴보자

private fun getInterval(): Observable<Long> =
    Observable.interval(3L, TimeUnit.MILLISECONDS).map { interval ->
        interval + 1
    }.take(100)

일정 간격으로 데이터를 발행하는 interval함수로 Observable을 생성한다.
interval의 첫번째 파라미터는 간격을 정하는 Long타입으로 두번째 파라미터인 TimeUnit에서 ms로 할지 s로 할지는 편의대로 정하면 된다만 부드러운 애니메이션을 위한거라면 ms로 해야된다.

그 후에 map함수는 해당 객체가 가지고 있는 원소들을 각각 어떤식으로 바꿀지 정하는 함수다.
위는 발행되어 나오는 Long데이터를 +1씩 한 이유는 디폴트가 0이라 첫 발행부터 증가를 주기 위해 1부터 시작했다.

그 후 take함수는 몇번 데이터를 발행할지 정하는 함수다.

이렇게 하면 3ms간격으로(0.003초) 100번 반복하니 총 0.3초동안 애니메이션이 실행된다 생각하면 된다.
fps로 따지면 무려 333fps다.

0.3초가 빠르다 싶으면 interval의 1을 2나 3으로 늘리면 된다.
욕심있게 이보다 더 부드럽게 하려면 interval을 낮추고, take를 증가시키면 된다. 물론 그에 따른 총 시간간격은 계산을 잘해야한다.

take를 100으로 하는게 편한것이 200, 350 이렇게 해두면 progress의 max값도 생각해서 계산해야 해서 불편하다.

기존 프로그래스바를 썼다면 max값을 대부분 10아니면 100으로 해두는데 프로그래스바의 progress속성이 Int타입이라 float로 대체하지 못해서 max값을 늘려야 한다.
그래서 max값이 1000인 이유기도 하다.

위는 증가 코드인데 감소 코드도 투척

val currentProgress = prg.progress
getInterval().subscribe {
    prg.progress = currentProgress - it.toInt()
}

변한건 +에서 -밖에 없다. 상황에 따라 증감을 하려면 if문을 사용해서 적절하게 사용하면 될 듯 하다.

val currentProgress = prg.progress
getInterval().subscribe {
	prg.progress = if (isRaise) currentProgress + it.toInt() else currentProgress - it.toInt()
}

수평 프로그래스 바 외에도 수직, 원형 등 progress와 max속성이 있는 widget 전부 적용 가능하다.


추가로 심심해서 계산으로 0.3초라 했지만 오로지 시간만 계산한 값이라 과연 실제 값은 얼마나 차이나는지 궁금해서 시간을 재봤다.

결과는 놀랍게도 0.3초에 근접했다. Rx..

binding.run {
    btn.setOnClickListener {
        val currentProgress = prg.progress
        val start = System.currentTimeMillis()
        getInterval()
            .doOnComplete {
                val end = System.currentTimeMillis()
                Log.d("animation", "Rxjava: ${(end - start) / 1000f}초")
            }.subscribe {
                prg.progress = currentProgress + it.toInt()
            }
    }
}

profile
Android Developer

3개의 댓글

comment-user-thumbnail
2022년 7월 24일

compose 는 사용안하시나요? (궁금)

1개의 답글
comment-user-thumbnail
2022년 7월 26일

good boy meow~

답글 달기