본문 바로가기

BLE/안드로이드 앱

[Noise Detector] 그래픽 모듈 (5) : WaveBars

ArrayBundle은 WaveBars가 데이터를 처리하는 단위입니다. 

 

WaveBars에 입력된 ArrayBundle은 내부 버퍼인 mBundleList에 담기게 되며 내부 스래드인 mAnimateBars에 의해 하나씩 처리됩니다. 실시간 처리를 위해 WaveDataView의 getView()의 autoAdjustPeriod를 true로 설정하여 버퍼에 4개 이상 쌓이게 되면 처리 주기를 감소시키도록 설정할 수있습니다. 해당 과정은 WaveBars.animateBars()에서 확인할 수 있습니다.

 

mAnimateBars 스래드는 불필요한 스래드의 존재를 막기위해 mBundleLIst가 비워질 때까지 존재하며 이후 제거되고 새로운 ArrayBundle이 입력됐을 때 다시 생성됩니다.

그리고 ArrayBundle의 mMaxValue가 CriticalLine에게 받은 mCLValue보다 크면 WaveBars.WarningListener의 startWarning() 메소드를 호출하여, 이 인터페이스를 구현하고 있는 Warning에게 알려줍니다.

 

mAnimateBars 스래드는 하나의 ArrayBundle에 대하여 mNumOfFrame회 그리기를 수행하며 각 막대의 폭은 유지한체 높이만 한 회마다 조금씩 증가시키고 감소시키는 방법으로 사용자에게는 마치 움직이는 것 처럼 보여줍니다. mNumOfFrame는 WaveDataView의 getView()의 period와 oneFramePeriod를 나눈 값이며 이것의 의미는 전체 주기를 각 프레임의 주기로 나눈 값. 즉, 프레임 개수를 의미합니다. 단, 프레임 수를 증가시키면 더 부드러운 화면을 얻을 수 있지만 의도한 period를 만족하지 못할 수 있습니다. 앱을 배포하기 전에 프레임 수를 조절하여 원하는 period를 제대로 만족하는지 확인할 필요가 있습니다.

 

이 그리기 과정은 메인 스래드가 아닌 스래드에서 이루어집니다. 안드로이드에서 그래픽 작업은 기본적으로 메인 스래드가 담당하도록 되어 있고 애니메이션 클래스도 제공하지만, 이렇게 구현했더니 애니메이션 클래스가 작동할 때 CriticalLine은 끊김때문에 사용하기 힘들고 Warning또한 제대로 작동하지 않았습니다. 즉, 메인 스래드의 부담을 줄이기 위해 메인 스래드 이외의 스래드에서 그리기가 필요한데 안드로이드에서는 서피스뷰(SurfaceView)가 이러한 기능을 제공합니다. 처음에 서피스뷰로 애니메이션을 구현할 때 각 프레임마다 SurfaceHolder 객체에서 Canvas를 상속받고 그린 후 풀어주는 과정을 반복해서 오히려 더 느려지지 않을까 걱정했지만, 보급형 안드로이드 스마트폰인 갤럭시 S4 미니에서도 무난히 해내더군요. Cortex-M만 다루다가 스냅드래곤 CPU를 써보니 상상을 초월하는 연산능력에 좀 놀랐습니다. 

 

 

animateFluct()는 mAnimateBars에서 각 프레임마다 SurfaceHolder 객체에서 Canvas를 상속받고 그린 후 풀어주는 과정을 반복하는 메소드입니다. 실제 이 과정은 animateZeroToTop(), animateTopToLimit(), animateTopToZero() 메소드가 담당하며 animateFluct()는 전체 프레임(mNumOfFrame)을 각 메소드에 분배하는 역할만을 수행하는 래핑 메소드입니다.

animateZeroToTop()는 높이 0지점에서 mUsedHeight × (배열 요소 ÷ mMaxValue)까지 올라가는 애니메이션을 담당합니다.

animateTopToLimit()은 높이 mUsedHeight × (배열 요소 ÷ mMaxValue)부터 mUsedHeight × (배열 요소 ÷ mMaxValue) × limitRatio까지 내려가는 애니메이션을 담당합니다. limitRatio은 animateTopToLimit()의 파라미터이벼 mBundleList에 아직 아이템이 남아 있을 때 더 부드러운 처리를 위해 사용됩니다.

animateTopToLimit()은 높이 mUsedHeight × (배열 요소 ÷ mMaxValue)부터 0까지 내려가는 애니메이션을 담당하며 mBunleList의 마지막 아이템 처리를 위해 사용됩니다.

animateZeroToTop(), animateTopToLimit(), animateTopToZero() 메소드는 전체 프레임(mNumOfFrame)을 각 메소드에 입력되는 파라미터인 periodRatio로 나눈 값만큼 프레임을 담당하는데, animateFluct() 메소드는 자신에게 입력된 upPeriodRatio만큼을 animateZeroToTop() 메소드에 그대로 전달하고, animateZeroToTop() 메소드가 처리하고 남은 프레임을 계산하여 animateTopToLimit() 또는 animateTopToZero()의 periodRatio에 전달합니다.

animateZeroToTop() 메소드가 처리하고 남은 프레임은 mNumOfFrame - (mNumOfFrame ÷ upPeriodRatio)가 되는데, 이것을 mNumOfFrame에 대한 식으로 바꾸면 ( (upPeriodRatio-1) ÷ upPeriodRatio ) × mNumOfFrame이 됩니다. 하지만 위에서 언급했다시피 각 메소드는 mNumOfFrame에서 를 나눈 값만큼 프래임을 담당하므로 ( (upPeriodRatio-1) ÷ upPeriodRatio )의 역수인 ( upPeriodRatio ÷ (upPeriodRatio-1) )가 animateTopToLimit(), animateTopToZero() 메소드의 periodRatio로 입력됩니다.

위에서 언급했지만 animateZeroToTop(), animateTopToLimit(), animateTopToZero() 메소드는 각 프레임마다 SurfaceHolder 객체에서 Canvas를 상속받고 그린 후 풀어주는 과정을 반복합니다. 그리고 각 막대는 Canvas 객체의 drawRect() 메소드를 사용하여 그리는데 각 막대마다 그리고 각 프레임마다 막대의 높이를 바꿔주기 위해 두개의 for문을 사용합니다. 바깥 for문은 프레임 횟수(frame)를 증가/감소시키고, 안쪽 for문은 배열의 인덱스(bundleIndex)를 증가시킵니다. 하지만 모든 계산을 매번 새로 하기에는 안드로이드 전체 시스템에 무리를 줄 수도 있다고 생각하여 변수 이외의 대부분의 값은 상수화시켜서 for문 밖으로 뺴놓았습니다. 각 메소드에서 constant가 붙은 지역변수가 이 역할을 담당합니다. 아래 그림은 animateZeroToTop() 메소드의 연산 방법을 요약해 놓은 것입니다.

animateZeroToTop()과 animateTopToZero()는 서로 반대되는 알고리즘을 갖지만, animateZeroToLimit()은 다른 두 메소드와 약간 다릅니다. 각 배열 요소의 최대값에 해당하는 높이부터 시작하지만 0까지 내려가지 않고, 각 배열 요소의 최대값을 해당 메소드 호출시 입력된 limitRatio로 나눈 값에 해당하는 높이만큼만 내려갑니다. 메소드 내부에서는 곱하기로 수행되므로 계산에는 그 역수값인 contantLimitRatio가 사용됩니다.

그리고 animateZeroToTop(), animateTopToZero()와 달리, 감소될 수 있는 최소 높이를 상수로 만들고, 나머지 부분에 대해서만 할당된 프레임 횟수로 높이를 감소시키는 방법을 사용합니다. 역시 중복 연산이 필요 없는 부분은 모두 for문 밖으로 빼놨습니다. 나름 신경 썼음에도 불구하고 안드로이드 프로파일러로 CPU 사용량을 보면 크게 눈에 띄는 효과는 없어 보이네요.