본문 바로가기

Android/정리 노트

[Android/안드로이드] 손가락을 따라 회전하는 이미지 뷰(rotate image on touch)

개발을 하다보면 손가락을 따라 회전을 시켜야하는 이미지 View가 필요하다.


이를테면 룰렛, 시계, 방탈출 게임의 금고 손잡이 등등...


RotateAnimation 클래스를 이용해 특정 이벤트 발생시 얼만큼 회전시킬 수는 있지만,


터치를 따라 회전 하는 동작은 난감하다.


결국 정말 하기 싫었지만 삼각함수를 써서 구현했다...




SpinnableImageView class


먼저 클래스를 하나 생성.


public class SpinnableImageView extends android.support.v7.widget.AppCompatImageView {
private double mCurrAngle = 0;
private double mPrevAngle = 0;
private double mAddAngle = 0;

public SpinnableImageView(Context context) {
super(context);
}

public SpinnableImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
}


자바 문법에 대한 이해가 있으면 굳이 Custom View로 만들지 않고 Activity(혹은 Fragment)에서 진행해도 무관(그러나 눈에 거슬리는 warning이 있는데 처리하기 곤란하니)


커스텀 뷰를 만들어 xml layout에서 쓰려면 AttributeSet까지 인자로 받는 생성자가 필요하니 꼭 둘다 선언하자.


Context만 받는 생성자는 없어도 되지만, java코드로 동적 뷰 생성을 할 때에는 용의하니 필요에 따라 선언하자.


이 예제에서는 xml layout에서 뷰 생성함


멤버변수는 각도를 저장해 놓을 변수로 몇개는 로컬 변수로 해도 되겠지만 눈의 편의를 위해...




그리고 사용할 rotate 메소드를 하나 정의해준다.


private void rotate(View view, double fromDegrees, double toDegrees) {
final RotateAnimation rotate = new RotateAnimation((float) fromDegrees, (float) toDegrees,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);


rotate.setDuration(0);
rotate.setFillAfter(true);


view.startAnimation(rotate);
}

duration을 0으로 설정한 이유는 아주 작은 단위로 손을 따라 애니메이션을 동작해야하기 때문이다.

(처음에 100ms로 테스트했는데 별 차이는 없다.)


fillAfter속성은 true로 할 시 애니메이션이 끝났을 때 그 상태를 유지한다(영속성).


이를 false로 할 경우 손가락을 뗐다가 다시 회전을 시킬경우 이미지가 원상태인채로 다시 시작함




이제 터치를 받을 터치 리스너를 정의해준다.


@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
final float centerOfWidth = getWidth() / 2;
final float centerOfHeight = getHeight() / 2;
final float x = motionEvent.getX();
final float y = motionEvent.getY();

switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
mCurrAngle = Math.toDegrees(Math.atan2(x - centerOfWidth, centerOfHeight - y));
break;


case MotionEvent.ACTION_MOVE:
mPrevAngle = mCurrAngle;
mCurrAngle = Math.toDegrees(Math.atan2(x - centerOfWidth, centerOfHeight - y));
animate(this, mAddAngle, mAddAngle + mCurrAngle - mPrevAngle);
mAddAngle += mCurrAngle - mPrevAngle;
break;

case MotionEvent.ACTION_UP:
break;
}
return true;
}

대망의 삼각함수... 구현시기는 옛날이라 생각나서 포스팅하는거라 기억은 잘 안나 재해석을...


먼저 터치 신호가 들어오면 현재 뷰의 가로, 세로, 터치 좌표를 받아온다.


뷰의 가로 세로는 멤버 상수로 고정시켜 놓으면 되지 않느냐! 한다면 회전할때마다 뷰의 가로 세로 길이가 바뀌기 때문에 매번 새로 받아와야 한다.


뷰의 가로 세로의 중앙값을 저장해 놓은건 삼각함수를 이용해서 좌표와 뷰의 중앙값을 계산하여 각도를 구할 수 있기 때문이다

(이를 응용하면 꼭 중앙이 아니더라도 다른 축을 기준으로 회전 가능!)


터치가 시작되면(ACTION_DOWN) 터치를 한 각도를 저장한다.


저걸 어떻게 아크탄젠트를 써서 저렇게 썼는지는 1년전 나에게 물어보길...


여튼 아크탄젠트로 각도를 파이단위(이름도 기억안남..)로 받아서 toDegree 메소드를 통해 우리에게 익숙한 각도로 변환해 저장한다.


드래그가 시작되면(ACTION_MOVE) 이전 각도를 저장 해놓고 새로운 각도를 구해 애니메이션을 진행한다.


그리고 mAddAngle에 애니메이션을 진행한 값을 누적시켜 놓는다.


지금으로써도 난해한 감이 있어서 이건 이렇게 했으면 됐을텐데 하고 수정해보면 뷰가 미친듯이 돌아 간다던지 돌다 만다던지 하니 수정을 포기했다!

(며칠 머리싸맨 과거의 나는 대단한듯!)


이렇게 한 채로 레이아웃에 커스텀뷰를 넣고 실행하여 회전시켜보면 빙빙 잘돌아간다.



<com.raonstudio.imagescaling.SpinnableImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
app:srcCompat="@drawable/resource"/>



이러면 찝찝한게 하나 남는다. 위에서 얘기했던 warning이 onTouchEvent에 걸려있다.


액티비티에서 따로 한사람은 스스로 해결해보고 저도 알려주면 감사하겠습니다ㅎㅎ


해결법은 다음과 같다(사실 해결 안 해도 잘만된다).



@Override
public boolean performClick() {
return super.performClick();
}

먼저 SpinnableImageView에 이 메소드를 Override해준다.


그리고 onTouchEvent에 


@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
final float centerOfWidth = getWidth() / 2;
final float centerOfHeight = getHeight() / 2;
final float x = motionEvent.getX();
final float y = motionEvent.getY();

switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
mCurrAngle = Math.toDegrees(Math.atan2(x - centerOfWidth, centerOfHeight - y));
break;

case MotionEvent.ACTION_MOVE:
mPrevAngle = mCurrAngle;
mCurrAngle = Math.toDegrees(Math.atan2(x - centerOfWidth, centerOfHeight - y));
animate(this, mAddAngle, mAddAngle + mCurrAngle - mPrevAngle);
mAddAngle += mCurrAngle - mPrevAngle;
break;

case MotionEvent.ACTION_UP:
performClick();
break;

}
return true;
}

ACTION_UP에 performClick()메소드를 호출해준다.


그럼 warning도 해결!







이를 응용하면 특정 각도(ex. 90도)마다 멈추거나 이미지가 좀 느리게 따라오게 하는 등 좀더 실제 앱에 쓰기 적합한 상태를 만들 수 있을것 같지만 그건 나중에 시간나면...



full source



import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.RotateAnimation;

public class SpinnableImageView extends android.support.v7.widget.AppCompatImageView {
private double mCurrAngle = 0;
private double mPrevAngle = 0;
private double mAddAngle = 0;

public SpinnableImageView(Context context) {
super(context);
}

public SpinnableImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean performClick() {
return super.performClick();
}

@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
final float centerOfWidth = getWidth() / 2;
final float centerOfHeight = getHeight() / 2;
final float x = motionEvent.getX();
final float y = motionEvent.getY();

switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
mCurrAngle = Math.toDegrees(Math.atan2(x - centerOfWidth, centerOfHeight - y));
break;

case MotionEvent.ACTION_MOVE:
mPrevAngle = mCurrAngle;
mCurrAngle = Math.toDegrees(Math.atan2(x - centerOfWidth, centerOfHeight - y));
animate(this, mAddAngle, mAddAngle + mCurrAngle - mPrevAngle);
mAddAngle += mCurrAngle - mPrevAngle;
break;

case MotionEvent.ACTION_UP:
performClick();
break;

}
return true;
}

private void animate(View view, double fromDegrees, double toDegrees) {
final RotateAnimation rotate = new RotateAnimation((float) fromDegrees, (float) toDegrees,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
rotate.setDuration(0);
rotate.setFillAfter(true);
view.startAnimation(rotate);
}
}