본문 바로가기

Android

[Android] 커스텀 뷰, 원형 프로그레스 구현기

원형으로 생긴 프로그레스(그림과 다르게 진행정도에 따라 한 칸씩 채워지는 효과)를

커스텀 뷰로 구현할 일이 생겼다.

기존 라이브러리를 써도 어찌저찌 되겠지만

변형이 많이 생길수도 있는 부분이라서 커스텀으로 구현해놓는게 속편하겠다고 판단했다.

 

자료 이미지

 

1. 원에서 막대의 각 위치를 구하기 위한 각도 계산 정의

 

원의 중심을 기점으로 반지름과 프로그레스 막대의 길이가 정의됐다고 했을 때

반지름 = r

막대의 길이 = h

라고 하겠다.

 

여기서 radian 의 개념이 필요한데,

 

degree 단위가 평소에 쓰는 60분법,

radian 단위는 반지름 대비 호의 길이인 호도법이다.

반지름 대비 호의 길이에 따른 각이라서 호도법이라 한다.

만약 반지름과 특정 구간의 호 길이가 같다면 1 radian 이 된다.

 

서로 변환하는 식은 아래와 같다.

degree = (180 / PI) * radian 

-> 180도의 각을 원주율로 나누면 반지름이 나오게 되고,

반지름과 radian 을 곱하면 60분법으로 표기된 각이 나온다.

 

radian = (PI / 180) * degree

-> 원주율을 180으로 나누고 60분법각을 곱하면 반지름 대비 호의 길이인 radian 이 나온다.

 

이 개념이 필요한 이유는 radian 으로 각도들을 구해서

삼각함수의 사인/코사인에 적용하려면 필요하기 때문이다.

 

sin = 삼각형의 높이 / 삼각형의 빗변

cos = 삼각형의 밑변 / 삼각형의 빗변

 

2. 다음 각도에서의 좌표 계산

 

반지름이 r 인 원이 있다고 했을 때

가장 윗부분 점의 좌표를 (x, y) 라고 했을 떄

특정 각도 다음의 좌표를 (x', y') 라고 하자.

x' = x(원의 중심) + cos(rad(degree)) * r

y' =  y(원의 중심) + sin(rad(degree)) * r

이런 식이 나온다.

 

아래 블로그 글이 매우 잘 정리돼있어서 좋은 참고가 됐다.

https://3001ssw.tistory.com/154

 

3. 실제 적용

 

2번에서는 매우 간단하게 정리됐지만

실제로 정리하려면 아래와 같은 문제가 발생한다.

 

1) 원을 그릴 공간이 변칙적으로 변해도 딱 떨어지는 만큼의 사각형을 그려야 한다.

2) 좌표 하나가 아니라 사각형의 네 점 좌표를 모두 구해야 한다.

 

- 해당 커스텀뷰를 그릴 공간의 크기가 width * height 라고 하자.

- 정사각형의 공간으로 만들어줄 예정이므로 원의 반지름 r = width / 2 라고 할 수 있다.

- 원의 중심 좌표 = (r, r) 이다.

- 막대의 길이 = h

- 막대의 두께 = w 라고 정의한다

 

1번부터 계산하자.

- 원의 둘레 = r * PI 다.

- 막대 사이의 공간 (내원 기준) = h 라고 하자(적당해보이는 간격)

그러면 막대만큼의 공간이 있는 막대 배열이 된다.

그리고 한 막대가 차지하는 공간이 2h 가 된다(막대길이 + 공간길이)

 

결론적으로

원의 둘레에 2h 가 딱 맞아떨어지면 규칙적인 배치가 될 것이다.

하지만 부동소수점 오차와 적용했을 때를 생각하면 조금의 오차를 허용하는 것이 맞다.

코드로 표현했을 때 배치돼야 하는 막대의 수 = ((int)원의 둘레 / h) / 2 가 된다.

 

1번은 해결됐다.

2번도 웬만큼 된 것 같지만 그래도 해보자.

 

우선 2번을 해결하려면 원은 한 개가 아니고 두 개가 돼야한다.

막대의 길이만큼 들어가있는 내원의 존재다.

 

내원의 반지름은 r - h 다.

원의 중심으로부터 r - h 를 반지름으로 가지고 (r - h) * PI 를 원의 둘레로 가지는 내원이 있는 것이다.

 

원래의 원을 편의상 외원 이라고 하고,

막대의 길이만큼 패인 원을 내원이라고 하면

 

내원과 외원의 막대좌표를 하나씩 알아야 한다.

 

실제 숫자로 대입해보자.

 

width = 1000

height = 1000

r = width / 2

원의 중심 좌표 = (r, r)

막대 길이 = 200

막대 두께 = 20

막대 사이 공간 = 20

내원 r = r - 200

원의 둘레 = r * PI

내원의 둘레 = 내원 r * PI

나누어 떨어지는 막대 갯수 = (내원의 둘레 / (막대두께 + 막대 사이 공간))

각도단위 = 360 / 막대 갯수

 

맨 위 막대부터 구현해보겠다

..라고 생각했는데

생각해보니 이상했다.

안드로이드의 canvas 에서 사각형을 그리는 방식은 모두 똑같다.

왼쪽변 x, 윗변 y, 오른쪽 변 x, 밑변 y 를 인자로 주면

(왼쪽변 x, 윗변 y), (오른쪽변 x, 윗변 y), (오른쪽변 x, 밑변 y), (왼쪽변 x, 밑변 y)

의 네 점으로 사각형이 그려진다.

여기서의 문제는 "틀어진 사각형을 그릴 수 없다"

네 점의 좌표를 직접 인자로 받지 않기 때문이다.

 

해결책은 canvas 를 degree 만큼 rotate 시켜서 그리는 것이다.

고정된 좌표에 사각형을 갯수만큼 그리면서 각도단위만큼 캔버스를 회전시키면

끝인 것이다.

 

고정좌표에 그리는 방법은

 

left = r

top = 0

right = r + w

bottom = h

 

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        float r = (float)canvasWidth / 2f;
        int barCount = (int)(((r - barHeight) * Math.PI) / (barWidth + betweenBar));
        float degree = 360f / barCount;

        Log.e(getClass().getSimpleName(),
                "\nCanvas width : " + canvasWidth
                        + "\nCanvas height : " + canvasHeight
                        + "\nBar width : " + barWidth
                        + "\nBar height : " + barHeight
                        + "\nBar between : " + betweenBar
                        + "\nDegree : " + degree
                        + "\nR : " + r
                        + "\nCount : " + barCount
        );

        for (int n = 0; n < barCount; n++) {

            RectF rectF = new RectF();
            rectF.set(r, 0, r + barWidth, barHeight);
            canvas.drawRect(rectF, progressRectPaint);
            canvas.rotate(degree, r, r);

        }


    }

 

막대 하나 그리고, 원의 중심을 기준으로 캔버스를 각도만큼 돌리고

다시 그리고의 반복이다.