2010-12-20

GestureDetector 정복하기

From : http://blog.vizpei.kr/94697746

[Intro]

어플리케이션 개발을 하다보면 반드시 해야하는 것이 모션 이벤트 처리 입니다.
터치 이벤트 같은 것들은 DOWN - MOVE - UP의 단계를 거치면서
사용자가 어떤 동작을 입력 하는지 감지 할 수 있습니다.
이 입력의 어떤 조합으로 사용자가 어떤 동작을 했는지 감지 할 수 있겠죠.
하지만 직접 이런 제스쳐들을 구현하기란 쉬운 일만은 아닙니다. (무엇보다 귀찮죠~)

그래서 Android에서는 GestureDetector라는 클래스를 아얘 제공합니다.

[About GestureListener]
GestureDetector는 두 가지 Listener를 가지고 있습니다.
interface GestureDetector.OnDoubleTapListener
interface GestureDetector.OnGestureListener
http://developer.android.com/reference/android/view/GestureDetector.html

자세한 설명은 Reference를 보시면 됩니다.
OnDoubleTapListener는 이름 그대로 두번 터치 했을 때,
OnGestureListener는 일반적인 제스쳐들, 한번 터치나 스크롤 관련 Listner입니다.
그리고 저 두 가지 interface를 모두 가진 녀석이 있습니다.
class GestureDetector.SimpleOnGestureListener
보통 SimpleOnGestureListener만 extends 하면 모든 제스쳐를 다 사용 할 수 있습니다.

[Usage of GestureDetector]
사용법도 매우 간단합니다.
GestureDetector를 만들기만 하면 땡이죠.
mGestureDetector = new GestureDetector(this, new SimpleGestureListener());
mGestureDetector.onTouchEvent(event);
음... 너무 뜬금 없는 코드인가요? 일단 아주 간단하게 적어 봤습니다.
1. GestureDetector를 만들 때 GestureListener를 등록 하고
2. 감시할 MotionEvent를 onTouchEvent에 넣어 주면 GetstureListener가 호출이 되는 구조 입니다.

좀 더 자세하게 살펴 보면,


  private final class SimpleGestureListener
  extends GestureDetector.SimpleOnGestureListener {
      // Implementation
  }

  private GestureDetector mGestureDetector;

  @Override  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      mGestureDetector = new GestureDetector(this, new SimpleGestureListener());
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
      return mGestureDetector.onTouchEvent(event);
  }
위와 같습니다.

[Analyze Gestures]
사용법은 간단하지만 제스쳐는 그리 간단하지 않습니다.
그래서 각 제스쳐에 대해서 분석을 해봤습니다.
public boolean onDoubleTap(MotionEvent e)
public boolean onDoubleTapEvent(MotionEvent e)
public boolean onDown(MotionEvent e)
public boolean onFling(MotionEvent e1, MotionEvent e2, float vX, float vY)
public void onLongPress(MotionEvent e)
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dX, float dY)
public void onShowPress(MotionEvent e)
public boolean onSingleTapConfirmed(MotionEvent e)
public boolean onSingleTapUp(MotionEvent e)
일단 제스쳐 이벤트에 대해서 간단히 살펴 봅시다.
onDoubleTap은 두 번 터치입니다.
onDoubleTap은 두 번 터치하면 이벤트가 더 이상 발생 되지 않지만,
onDoubleTapEvent는 DOWN, MOVE, UP 이벤트를 모두 포함 합니다. (나중에 살펴 보겠습니다.)

onDown은 터치하려고 손을 대기만 해도 발생되는 이벤트며,
모든 제스쳐의 시작입니다.
onFling은 onScroll에서 끝을 살짝 튕기는 동작에서 발생하는 이벤트며,
onScroll은 말 그대로 스크롤 시에 발생하는 이벤트 입니다.
onLongPress는 길게 눌렀을 때 발생 하는 이벤트며
onShowPress는 onLongPress보다는 좀 더 짧은 시간동안 누르고 있으면 발생 하는 이벤트 입니다.
onSingleTap은 한 번 터치 했을 때 발생하는 이벤트며,
onSingleTabConfirmed는 한 번 터치 하고 다음에 다시 터치 이벤트가 들어오지 않았을 때,
한 번 터치가 확실 하다고 확인 시켜주는 이벤트 입니다.

자... 이제 시작입니다! 이건 간단히 살펴 본것 밖에 안되는...
본격적으로 실제 제스쳐에 따라서 어떤 이벤트가 호출 되는지 살펴 보겠습니다.
시간은 로그를 통해 한번만 계산된 시간이며,
대략 그 시간 범위에서 이벤트가 발생 한다고 보시면 됩니다.

1. 아주 살짝 터치
000ms    onDown : ACTION_DOWN
060ms    onSingleTapUp : ACTION_UP
306ms    onSingleTapConfirmed : ACTION_DOWN
일단 손을 대면 무조건 onDown 이벤트 발생입니다.
살짝 터치를 하게 되면 보통 30~60ms 정도 후에 손이 떨어지게 됩니다.
손이 떨어 지면 onSingleTapUp 이벤트가 발생하며,
onDown이벤트 발생 후 약 300ms 안에 다시 onDown 이벤트가 발생 하지 않는다면
onSingleTapConfirmed 이벤트가 발생하여 확실히 한 번 터치 되었다는 이벤트를 발생 시킵니다.

2. 살짝 터치
000ms    onDown : ACTION_DOWN
097ms    onShowPress : ACTION_DOWN
172ms    onSingleTapUp : ACTION_UP
303ms    onSingleTapConfirmed : ACTION_DOWN
역시나 onDown 이벤트 부터 시작입니다.
1번 보다는 살짝 길게, 약 90~100ms 정도 터치되면 onShowPress 이벤트가 발생합니다.
172ms 이후에 손을 떼었으며,
역시나 300ms 정도 지나면 onSingleTapConfirmed 이벤트가 발생 됩니다.

3. 약간 길게 터치
000ms    onDown : ACTION_DOWN
103ms    onShowPress : ACTION_DOWN
460ms    onSingleTapUp : ACTION_UP
2번보다 좀 더 길지만, LongPress는 아닌 상황입니다.
역시 이 때도 약 100ms 정도에 onShowPress 이벤트가 발생하긴 하지만
300ms 이후에 손을 떼었기 때문에 onSingleTapConfirmed 이벤트가 먹히는 현상이 일어납니다.

4. 아주 길게 터치
000ms    onDown : ACTION_DOWN
096ms    onShowPress : ACTION_DOWN
590ms    onLongPress : ACTION_DOWN
LongPress가 발생 하는 상황입니다.
100ms 정도에 onShowPress 이벤트가 발생 하며,
590~600ms 정도에 onLongPress 이벤트가 발생 합니다.
이때 onSingleTapUp 이벤트는 발생 하지 않습니다.

두 번 터치 하는 경우에는 이벤트가 두 가지이기 때문에,
두 가지 조합의 경우 모두 살펴 보겠습니다.

5. 두 번 터치 (onDoubleTap)
000ms    onDown : ACTION_DOWN
060ms    onSingleTapUp : ACTION_UP
140ms    onDoubleTap : ACTION_DOWN
140ms    onDown : ACTION_DOWN (지연 될 수 있음)
먼저 onSingleTapUp 이벤트가 발생 합니다.
그리고 onSingleTapConfirmed 가 발생 하기 전에
다시 onDown 이벤트가 들어오게 되면 onDoubleTap 이벤트가 발생 합니다.
참고로 두번째 들어오는 onDown 이벤트는 onDoubleTap 이벤트보다 늦게 들어 올 수 있습니다.
(항상 같이 들어 오는게 아니라 onDoubleTap이 먼저 발생 합니다.)

6. 두 번 터치 (onDoubleTapEvent)
000ms    onDown : ACTION_DOWN
041ms    onSingleTapUp : ACTION_UP
130ms    onDoubleTapEvent : ACTION_DOWN
130ms    onDown : ACTION_DOWN (지연 될 수 있음)
190ms    onDoubleTapEvent : ACTION_UP
onDoubleTap 이벤트와의 차이는 DOWN, MOVE, UP 이벤트까지 모두 캐치된다는 점이며,
마지막에 onDoubleTapEvent에 UP 액션이 들어오는 것을 확인 할 수 있습니다.
(위의 경우, 190ms 이후에 두번째 터치에서 손이 떨어졌다는 것을 확인 할 수 있습니다.)

7. 두 번 터치 (onDoubleTap + onDoubleTapEvent)
000ms    onDown : ACTION_DOWN
080ms    onSingleTapUp : ACTION_UP
200ms    onDoubleTap : ACTION_DOWN
200ms    onDoubleTapEvent : ACTION_DOWN (지연 될 수 있음)
200ms    onDown : ACTION_DOWN (지연 될 수 있음)
260ms    onDoubleTapEvent : ACTION_UP
같이 사용 하게 되면 onDoubleTap, onDoubleTapEvent 이벤트 둘 다 발생하며,
항상 onDoubleTap 이벤트가 먼저 발생 하게 됩니다.
(결과적으로 onDoubleTap - onDoubleTapEvent - onDown 순서로 발생 합니다.)

8. 두 번 터치, 두 번째 터치시 스크롤 (onDoubleTapEvent)
000ms    onDown : ACTION_DOWN
080ms    onSingleTapUp : ACTION_UP
179ms    onDoubleTapEvent : ACTION_DOWN
179ms    onDown : ACTION_DOWN (지연 될 수 있음)
280ms    onDoubleTapEvent : ACTION_MOVE
289ms    onShowPress : ACTION_DOWN
290ms    onDoubleTapEvent : ACTION_MOVE
...
779ms    onLongPress : ACTION_DOWN
800ms    onDoubleTapEvent : ACTION_UP
평소에 절대 나올법한 제스쳐지만 onDoubleTapEvent의 특성을 살펴보기 위한 제스쳐 입니다.
두 번째 onDown 이벤트 이후에 MOVE 이벤트가 들어 오는 것을 확인 할 수 있으며,
한가지 특이한 점은 계속 스크롤 되지 않고 onLongPress 이벤트가 발생하면 끝난다는 점입니다.
손을 뗄 수 밖에 없는 상황이 오게 되죠.

위에서 보시다 시피,
조금 길게 눌러짐에 따라 onShowPressonLongPress가 도중에 발생 할 수도 있습니다.

9. 스크롤
000ms    onDown : ACTION_DOWN
030ms    onScroll : ACTION_DOWN, ACTION_MOVE
...
스크롤 이벤트는 간단 합니다.
최소 30ms 이후 부터는 onScroll 이벤트가 발생 할 수 있으며,
플링시키지 않고 살며시 손을 떼면 끝까지 onScroll 이벤트만 연속으로 발생 합니다.

10. 플링
000ms    onDown : ACTION_DOWN
030ms    onScroll : ACTION_DOWN, ACTION_MOVE
...
900ms    onFling : ACTION_DOWN, ACTION_UP
마지막에 손가락을 슬며시 튕기는 플링 동작입니다.
스크롤 이벤트와 비슷하지만, 마지막에 UP 액션과 함께 onFling 이벤트가 동작합니다.
스크롤과 플링 제스쳐 모두 시간에 따라 onShowPress 이벤트가 발생 할 수 있습니다.


[Outro]
Android에 기본으로(API Level 1) 들어 있는 GestureDetector에 대해서
조금은 자세하게 알아 봤습니다.
그냥 막연하게 이벤트 이름만 보고서 프로그래밍을 하기 보다는,
정확하게 어떻게 동작이 되는지 확인 하고 프로그래밍을 하면
좀 더 자신이 원하는 제스쳐를 캐치하여 좀 더 나이스한 어플리케이션을 만들 수 있을 것입니다.

엄청나게 강력하게 보이는 GestureDetector지만,
여기에도 한가지 단점이 있답니다.

다음에는 이 단점을 극복 할 수 있는 클래스를 만들어 보도록 하겠습니다.


[Intro]

 저번에 GestureDetector에 대해서 포스팅을 하면서 마지막에
GestureDetector에 단점이 존재 한다고 했었습니다.
살짝 치명적인 단점이 있는데,
그건 바로 이벤트 종료 시점을 알 수 없다는 것입니다.

이벤트가 시작될 때는 무조건 onDown 이벤트가 발생이 됩니다.
한 번 터치 하면 onSingleTap관련, onShowPress 같은 이벤트가 발생이 되고,
길게 누르면 onLongPress, 두 번 터치하면 onDoubleTap관련 이벤트가 발생이 됩니다.
onFling도 마지막 손을 떼는 순간 발생하죠.

 위와 같은 이벤트들은 이벤트가 발생 한 후에 손가락이 떨어졌는지 검사할 필요가 없습니다.
한 번 터치나 두 번 터치하는 동작은 손가락이 떨어진 다음에 발생 하고,
플링 동작도 손가락이 화면에서 떨어지며 발생 합니다.
LongPress 동작도 손가락을 오래 눌러야 하는 동작이니 떼는것과 무관하죠.

 가장 문제가 되는건 onScroll 이벤트입니다.

 [Case Study]

그렇다면 왜 문제가 되는지 잠깐만 생각해 봅시다.


터치를 하여 빨간 사각형을 이동시키는 프로그램을 만들고자 합니다.
손가락을 계속 화면에 붙이고 있을 땐, 사각형이 계속 따라다니는 프로그램인데,
GestureDetector를 사용하면 onScroll 이벤트로 쉽게 만들 수 있습니다.
(한번 만들어 보세요~)

하지만 여기에다가 손을 뗐을때 다시 원래 위치로 돌아가는 기능을 추가 하고자 한다면..
SimpleOnGestureListener 만으로 가능 할까요?

답은 "안됩니다"

스크롤 이벤트는 플링이 아닌 이상
onDown - onScroll 반복 - onScroll로 끝나는 흐름을 가지고 있습니다.
onScroll 이벤트는 ACTION_DOWNACTION_MOVE 동작만 감지하기 때문에
ACTION_UP 동작은 캐치 할 수 없습니다.

그렇기 때문에 만약 onScroll 이벤트가 발생 후
마지막으로 손을 떼었을 때 onScroll 이벤트로 끝이 났다면,
이게 손이 떨어졌는지 안떨어졌는지 알 방법이 없습니다.
그럼 어떻게 감지해야 할까요?

1. 오랫동안 onScroll이 호출 되지 않았다면 스크롤 끝난거 아닌가요?
    뭐 그 말도 일리는 있습니다만, onScroll은 터치 좌표가 변해야 호출 되기 때문에
    스크롤 후에 손 안떼고 가만히 있으면 onScroll 이벤트가 호출 되지 않습니다.
    하지만 손을 떼지는 않았으니... 이 방법은 아닌것 같네요.

2. 터치 스크롤링 하면 보통 플링 동작으로 끝나지 않나요?
    보통 많은 사람들이 그렇게 생각 할 수도 있지만, 그렇지 않습니다.
    동작에 따라 안 일어 날 수도 있어요!

그렇다면 방법은 ACTION_UP 이벤트를 감지 하는 수 밖에 없겠군요...

[AdvancedGestureDetectorWrapper]

 public class AdvancedGestureDetectorWrapper {
     // Interfaces
     public static interface OnFinishedListener {
         public abstract void onFinished(MotionEvent e);
     }

     // Classes
     public static class AdvancedOnGestureListener
     extends GestureDetector.SimpleOnGestureListener
     implements OnFinishedListener {
         @Override
         public void onFinished(MotionEvent e) {}
     }
    
     // Fields
     private GestureDetector mDetector;
     private AdvancedOnGestureListener mListener;
    
     // Constructors
     public AdvancedGestureDetectorWrapper(
                 Context context, AdvancedOnGestureListener listener) {
         mListener = listener;
         mDetector = new GestureDetector(context, mListener);
     }
 
     // Methods
     public boolean onTouchEvent(MotionEvent ev) { 
         boolean onTouchEvent = mDetector.onTouchEvent(ev);
         if(ev.getAction() == MotionEvent.ACTION_UP) {
             mListener.onFinished(ev);
         }
         return onTouchEvent;
     }
 
사용법은 보통 GestureDetector와 다르지 않습니다.


 private final class AdvancedGestureListener
 extends AdvancedGestureDetectorWrapper.AdvancedOnGestureListener {
     // Implementation
 }

 private AdvancedGestureDetector mGestureDetector;

 @Override public void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);
     setContentView(R.layout.main);
     mGestureDetectornew AdvancedGestureDetector(
                 this, new AdvancedGestureListener());
 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
     return mGestureDetector.onTouchEvent(event);
 }

이름이 너무 길어졌습니다.
그리고 직접 GestureDetector를 extends하지 않고 필드로 가지고 있기 때문에
이름에 Wrapper라고 붙여서 더 길어졌군요.

여튼 중요한 부분은 Wrapper의 onTouchEvent 메소드 부분입니다.
기본 원리는 기본 GestureDetector에게도 MotionEvent를 전달 하고,
별도로 한번 더 MotionEvent를 검사하는 원리입니다.

코드상으로 보면 ACTION_UP일 때 무조건 onFinished 이벤트를 호출 하게 끔 되어있습니다.
아주 간단한 원리죠!
이런 원리를 이용하면 개발자가 원하는 다른 동작들도 만들어서 사용 할 수 있습니다.
기존의 제스쳐와 새로 만든 제스쳐를 같이 사용 할 수 있는 것이죠.
(그런 의미에서 Advanced라고 이름을 붙였답니다.)

사실 구현은 어떻게 하든지 상관은 없습니다.
전 최대한 기존 GestureDetector와 비슷하게 만들기 위해서 조금 복잡하게 만들었지만,
사실 뜯어보면 별거 없습니다.

원리를 간파하신 분이라면 꼭 새로이 클래스를 만들지 않고서
바로 Activity의 onTouchEvent 메소드에 직접 때려 넣어도 되겠거니 생각 하셨을겁니다.
네... 맞습니다. 그냥 GetureDetector에 한번 넣고 그 뒤에 또 검사하면 됩니다.
Wrapper와 Listener를 따로 만들 필요 없이 하나로 묶어서 만들 수도 있고,
여러가지 방법으로 자신의 코딩 스타일에 맞추어 개발 하시면 되겠습니다.

[Outro]

Java이기 때문에 기존의 클래스를 마음대로 바꿔버릴 수 있어서 참 좋은것 같습니다.
이런식으로 자신만의 라이브러리를 하나하나 늘려 나가면
나중에 매우 유용하게 쓸 수 있을 것입니다.

-------------------------------------------------------
If it's not fun, why do it?

댓글 없음 :