출처
: http://blog.naver.com/doogie96/220380919463
1. 안드로이드 뷰 프레임웍
기본적으로 모든 안드로이드 어플리케이션은 회전이 가능하도록 되어있습니다. AndroidManifest.xml에서 Activity의 속성을 Portrait나 Landscape로 고정시키지 않는 이상, 어플리케이션은 자동적으로 회전에 대응해서 UI 레이아웃을 재배치합니다. RelativeLayout과 DIP(Desity Independent Pixel) 단위를 사용하여 레이아웃을 작성해 놓으면 정교하지는 않지만 어느정도 화면의 크기나 회전에 대응할 수 있는 GUI를 구현할 수 있습니다. 이 부분에 대해서는 이미 많은 안드로이드 개발참고서에서 다루고 있으므로 자세한 설명은 생략하도록 하겠습니다.
안드로이드 뷰 프레임웍에서 제공되는 회전기능은 제약사항이 있습니다. Orientation이 변경되면 현재 실행중인 Activity가 종료되고 새로운 Activity가 실행이 된다는 것입니다. 이런 구조에서는 화면상의 아이콘이 회전하는 효과와 같은 구성요소의 회전이나 변환은 구현이 불가능합니다.
2. onDraw함수의 오버라이딩을 이용한 회전
android.view.View를 상속받는 뷰 객체에서는 onDraw함수를 오버라이딩하여 시스템의 Canvas 객체를 전달받아 UI 구성요소들을 그릴 수 있습니다. Canvas 객체는 android.graphics.Matrix 객체를 사용하여 매트릭스 연산을 이용한 좌표 변환이 가능합니다. 매트릭스 좌표변환을 이해하기 위해 컴퓨터 그래픽스에서 기본이 되는 매트릭스 연산들을 간단히 설명드리겠습니다.
* 2D transformation
2D 공간은 (x, y)로 좌표를 표시할 수 있습니다. 그리고, 2D 공간의 객체는 이런 좌표들의 집합이라고 할 수 있습니다. 좌표로 표현된 객체는 매트릭스 연산을 통해 움직이거나(translation), 크기를 변경하거나(scaling), 회전(rotation)시킬 수 있습니다.
translation은 다음과 같이 표현할 수 있습니다.
이것을 매트릭스로 표현하면,
같은 방법으로 Scaling과 Rotation을 매트릭스 연산으로 표시하면 다음과 같습니다.
위와 같이 좌표를 1x2 행렬로 표시할 경우, translation은 덧셈으로, scaling과 rotation은 곱셈으로 표현이 가능합니다. 매트릭스 연산을 모두 곱셈으로 일반화하기 위해서 Cartesian coordinates를 도입합니다. Cartesian coordinates는 가상의 1차원을 더해서 가상차원의 좌표를 항상 1이 되도록 하는 것입니다. 쉽게 설명하면 2차원 공간에서 (x, y)로 표시되는 좌표를 (x, y, 1)로 표시하는 것입니다. 이것의 수학적 근거를 설명하자면 homogeneous coordinates를 설명해야 하는데, 이 부분은 생략하도록 하겠습니다. (사실 제가 수학에 좀 약합니다) 아뭏든 Cartesian coordinates를 사용하면 각 transformation은 다음과 같이 표현될 수 있습니다.
android.graphics.Matrix 객체에는 위와같은 메트릭스 연산을 편리하게 할 수 있는 각종 함수가 구현되어 있습니다.
Rotation을 하기 위해서는 preRotate나 postRotate 함수를 사용하여 Rotation 메트릭스를 생성할 수 있습니다.
Matrix로 표현된 Transformation들은 서로 중첩(곱셉)하여 사용이 가능합니다. 여기서 기억해야 할 부분은, 메트릭스 연산의 특성상 가장 마지막에 곱해지는 transformation이 가장 먼저 적용됩니다. 때문에. android.graphics.Matrix객체에서 제공하는 transformation 함수들은 post와 pre의 두 가지 형태가 존재합니다. (preRotate, postRotate) pre는 현재 메트릭스의 앞에 곱하도록, post는 현재 메트릭스에 뒤에 곱하도록 동작하는 함수입니다.
Canvas 객체에는 그 위에 그려지는 모든 객체의 좌표들을 일괄적으로 계산하기 위해서 android.graphics.Matrix 객체를 설정할 수 있도록 되어있습니다.
public void setMatrix (Matrix matrix)
Completely replace the current matrix with the specified matrix.
If the matrix parameter is null, then the current matrix is reset to identity.
Parameters
matrix | The matrix to replace the current matrix with. If it is null, set the current matrix to identity. |
---|
이제 뷰를 회전시키기 위한 모든 이론적인 준비가 되었습니다. 다음과 같이 구현하면 뷰를 회전시킬 수 있습니다.
@Override protected void onDraw(Canvas canvas) { Matrix m = new Matrix(); m.postRotate(90); canvas.setMatrix(m);
// draw object here ... } |
onDraw를 오버라이딩하여 구현한 뷰 객체는 위와 같이 Matrix를 설정해 줌으로써 회전시킬 수 있습니다. 하지만, 안드로이드 뷰 프레임웍에서 기본제공되는 TextView나 ImageButton 등과 같은 뷰 객체들은 이미 onDraw 함수를 내부적으로 오버라이딩하여 구현되어 있는 상태입니다. 따라서, 이런 기본 뷰 객체들을 상속받는 하위 뷰 객체들에서 회전을 적용하기 위해서는 onDraw함수에서 setMatrix를 호출한 후 super.onDraw를 호출하는 형태로 구현이 되어야 합니다. 이런 방식은 어딘가 깔끔하지 못하고 코딩 실수를 유발하기가 쉬워 보입니다. (super.onDraw를 호출하지 않는 경우 등)
onDraw를 이중으로 오버라이딩 해야하는 문제점을 근본적으로 해결하기 위해, 모든 뷰 객체의 기저클래스인 android.view.View 클래스에는 상속 가능한 dispatchDraw함수가 구현되어 있습니다. dispatchDraw함수는 뷰 프레임웍에서 onDraw를 호출하기 직전에 먼저 호출되는 함수입니다. 즉, 기본 뷰 객체의 하위 객체들에서도 상위객체의 onDraw가 불리기 이전에 어떤 동작을 할 수 있도록 만들어진 함수인 것입니다.
protected void dispatchDraw (Canvas canvas)
Called by draw to draw the child views. This may be overridden by derived classes to gain control just before its children are drawn (but after its own view has been drawn).
이로써, 안드로이드 기본 뷰 객체를 사용하는 경우에도 dispatchDraw를 오버라이딩하여 회전을 시킬 수 있게 되었습니다. 그런데, 한 가지 남은 일이 더 있습니다. 뷰 객체의 좌표가 회전되더라도, 터치 이벤트의 좌표는 회전되지 않은 상태입니다. 90도 회전된 (스크롤이 가능한) 뷰 객체를 상하로 스크롤 할 경우, 좌우로 움직이는 90도 역회전된 이벤트 좌표가 발생할 것입니다. 이것을 바로잡기 위해서 dispatchDraw와 유사하게, dispatchTouchEvent를 사용할 수 있습니다.
public boolean dispatchTouchEvent (MotionEvent event)
Pass the touch screen motion event down to the target view, or this view if it is the target.
dispatchDraw와 dispatchTouchEvent를 사용하여 뷰 객체와 터치 이벤트의 좌표를 90도 회전시키는 예제코드는
다음과 같습니다.
@Override protected void dispatchDraw(Canvas canvas) { Matrix m = new Matrix(); m.postRotate(90); canvas.setMatrix(m);
super.dispatchDraw(canvas); }
@Override protected boolean dispatchTouchEvent(MotionEvent event) { Matrix m = new Matrix(); m.postRotate(-90); float p[] = new float[2]; p[0] = e.getX(); p[1] = e.getY(); m.mapPoints(p); event.setLocation(p[0], p[1]);
return super.dispatchTouchEvent(event); } |
3. OpenGL을 사용한 뷰의 회전
onDraw함수나 dispatchDraw함수에 setMatrix를 사용하여 뷰 객체에 변형을 주는 방법은 간단하지만, 조금 복잡한 레이아웃에 적용하는 경우나 세밀한 transformation을 요구하는 경우에는 사용하기가 어렵습니다. 또한, transformation을 적용해야 하는 객체가 많아지는 경우에는 성능의 문제까지 발생합니다. 이런 문제를 해결하기 위해서 OpenGL을 사용하여 뷰를 그릴 수 있습니다.
사실 OpenGL을 사용하는 것은 그리 간단한 일은 아닙니다. OpenGL을 사용하려면 안드로이드에서 제공하는 기본 뷰 객체들을 사용할 수 없으며, 객체들의 세부사항을 모두 직접 그려야 합니다. OpenGL은 점, 선, 면등의 기본적인 구성요소를 그리기 위한 저수준의 API를 제공하기 때문에 OpenGL을 사용하여 객체를 그리는 것은 상당히 복잡한 일이 될 수 있습니다.
OpenGL을 사용하는 경우, 안드로이드 뷰 시스템을 제한적으로 사용하면서, 별도의 뷰시스템을 만들어서 올릴 수 있습니다. 복잡한 구조의 어플리케이션에 전반적으로 OpenGL을 적용하는 경우에는 개별적으로 객체들을 그리는 것 보다는 OpenGL을 사용하는 별도의 뷰시스템을 만드는 것이 훨씬 효율적입니다.
안드로이드 뷰 객체에 onDraw가 함수가 존재하듯, OpenGL에서는 onDrawFrame함수가 있습니다. onDrawFrame함수는 Canvas대신 GL10(GL11) 객체를 전달받습니다. 이렇게 전달받은 GL10(GL11) 객체에 glMultMatrix라는 함수를 통해 매트릭스를 설정할 수 있습니다.
GL10(GL11) 객체에서는 android.graphics.Matrix로 표현된 메트릭스를 사용하지 않습니다. android.graphics.Matrix는 기본적으로 Cartesian 3x3 행렬의 2D를 바탕으로 하고있는데, OpenGL은 Cartesian 4x4 형태의 3D 메트릭스를 사용하기 때문입니다. OpenGL에서는 4x4의 변환행렬을 나타내기 위한 별도의 메트릭스 객체를 제공하지는 않습니다. OpenGL에서는 float[16] 타입으로 직접 표현된 4x4 행렬을 사용하며, 개발자의 구현의 편의를 위해 android.opengl.Matrix 클래스에서 float[16]타입의 행렬을 다루는 유틸리티 함수들을 제공하고 있습니다. 아래는 android.opengl.Matrix 유틸리티를 사용하는 회전UI 예제입니다.
import android.opengl.Matrix; ... @Override public void onDrawFrame(GL10 gl) { float m[] = new float[16]; Matrix.setIdentityM(m); Matrix.translateM(m, 0, currentPivot[0], currentPivot[1], 0); Matrix.rotateM(m, 0, 90, 0, 0, -1); Matrix.translateM(m, 0, currentPivot[0], currentPivot[1], 0); gl.glMultMatrixf(m, 0);
// draw textures ... } |
객체를 표현할 때에, 계층구조를 가지도록 표현하는 것이 수월한 경우가 많습니다.
예를들어 사람을 모델링 하는 경우에, 팔이나 다리의 움직임은 지면을 기준으로 높이와 각도로 표현하는 것보다,
사람의 몸통을 기준으로 회전한 각도를 표현하는 것이 훨씬 효율적입니다.
하나의 ViewGroup에는 이미지버튼이나 텍스트박스 등의 하부 구성요소들이 배치가 될 것입니다.
그리고, ViewGroup을 회전시키거나 위치를 변경할 경우에는 해당 ViewGroup에 포함된 구성요소들 또한 이에 따라 변환이 될 것입니다.
이렇게 될 경우 transformation의 문제는 더욱 복잡해집니다.
OpenGL은 행렬스택을 제공하여 이런 계층화된 transformation을 단순화하여 구현할 수 있습니다.
import android.opengl.Matrix; ... @Override public void onDrawFrame(GL10 gl) { float m[] = new float[16]; Matrix.setIdentityM(m); Matrix.translateM(m, 0, currentPivot[0], currentPivot[1], 0); Matrix.rotateM(m, 0, 90, 0, 0, -1); Matrix.translateM(m, 0, currentPivot[0], currentPivot[1], 0); gl.glMultMatrixf(m, 0);
for (int i = 0; i < mGLViews.size(); i++) { gl.glPushMatrix(); view.onDrawFrame(gl); gl.glPopMatrix(); } } |
따라서 for루프안의 모든 TwGLView객체들은 90도 회전된 메트릭스의 좌표계 위에서 그려지게 됩니다.
그런데, 만약 for루프 안의 특정 TwGLView객체가 추가적으로 메트릭스에 변환을 가하게되면, 다음 루프에서 그려지게 될 TwGLView 객체에도 앞의 객체에 의해 변환된 좌표계가 적용이 될 것입니다. 이런 계층 구조상의 문제를 해결하려면 매번 루프를 돌기 전에 현재 메트릭스 상태를 저장하고, 뷰를 그린 후 다시 이전의 메트릭스를 복원해주는 것이 필요합니다.
- Start Time
- 에니메이션의 시작시간. 시작시간을 기준으로 특정 시점의 메트릭스를 계산하기 위해서 사용되는 파라미터.
- Duration
- 에니메이션의 전체 변환이 완료되어야 하는 시간. 특정 시점의 메트릭스를 계산하기 위해서 사용되는 파라미터.
- Interpolation
- 시작 상태에서 마지막 상태까지 진행되는 방법을 설정. Linear, 가속도, Bounce 등을 설정할 수 있다.
public static float[] toGLMatrix(float v[]) { v[15] = v[8]; v[13] = v[5]; v[5] = v[4]; v[4] = v[1]; v[12] = v[2]; v[1] = v[3]; v[3] = v[6]; v[2] = v[6] = v[8] = v[9] = 0; v[10] = 1; return v; } |
RotateAnimation mAnimation; Transformation mTransformation; float[] mAnimMatrix = new float[16];
...
public void init() { mAnimation= new RotateAnimation(degree, 0, Animation.ABSOLUTE, 0, Animation.ABSOLUTE, 0); }
public void onDrawFrame(GL10 gl) { if (mAnimation.getTransformation(System.currentTimeMillis(), mTransformation)) { mTransformation.getMatrix().getValues(mAnimMatrix); toGLMatrix(mAnimMatrix); gl.glMultMatrixf(mAnimMatrix); } // draw textures ... } |
5. 마치는 글
이 번 글에서는 UI의 회전에 대해서 다루어 보았습니다. 회전은 기본적인 UI효과 중의 하나이지만, 구현하기가 쉽지많은 않은 효과이기도 합니다. 개발자들이 이 글에서 설명드린 회전효과에 대해서 익숙해진다면, 다른 복잡한 효과들에도 어느정도 쉽게 접근이 가능할 것이라고 생각됩니다. 안드로이드 개발자들에게 도움이 되기를 바라며 글을 마치겠습니다.
'IT_Programming > Android_Java' 카테고리의 다른 글
Multidex를 사용할 수 없는 Eclipse에서 JAR에 dex를 추가하여 메소드 65536개 오류 방지하기 (0) | 2015.06.23 |
---|---|
[Android] jar, apk의 메소드 아이디 카운트 체크 (0) | 2015.06.23 |
JNI Local Reference Changes in ICS ( ICS 부터 바뀌는 JNI Local Reference ) (0) | 2015.06.05 |
Android property animation (0) | 2015.06.01 |
Android Deep Link 구현하기 (0) | 2015.05.31 |