상세 컨텐츠

본문 제목

안드로이드 Making Sense of Multitouch

스마트기기개발관련/안드로이드 개발

by AlrepondTech 2011. 7. 18. 17:21

본문

반응형

 

 

 

 

=================================

=================================

=================================

 

 

 

 

 

 

출처: http://blog.naver.com/PostView.nhn?blogId=zommer11&logNo=20109444042

  • 원문

http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html

 

 

 

멀티터치란 말을 주위에서 많이 하지만 사람들이 무엇을 말하는지 분명하지 않다.

어떤 사람들은 하드웨어의 지원을 뜻하고 어떤 사람들은 소프트웨어로 지원하는 제스쳐를 뜻한다.

여러분이 무엇을 뜻하든지, 오늘 우리는 스크린에서 여러 개의 손가락으로 멋있게 동작하는 앱이나 뷰를 만드는 방법을 알아볼 것이다.

 

글에는 많은 예제 코드가 있다. 예제들은 터치 이벤트에 응답하는 커스텀 뷰를 생성하는 방법과 사용자가 뷰안에 그려진 객체를 조정하는 법을 보여 것이다.

예제를 이해하기 위해서는 액티비티 설정과 안드로이드 UI 시스템에 대한 기본 지식이 필요하다.

 

그럼, 특정 위치에 객체를 그리는 새로운 클래스부터 시작하자.

 

public class TouchExampleView extends View {
    private Drawable mIcon;
    private float mPosX;
    private float mPosY;
   
    private float mLastTouchX;
    private float mLastTouchY;
   
    public TouchExampleView(Context context) {
        this(context, null, 0);
    }
   
    public TouchExampleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
   
    public TouchExampleView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mIcon = context.getResources().getDrawable(R.drawable.icon);
        mIcon.setBounds(0, 0, mIcon.getIntrinsicWidth(), mIcon.getIntrinsicHeight());
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
       
        canvas.save();
        canvas.translate(mPosX, mPosY);
        mIcon.draw(canvas);
        canvas.restore();
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // More to come here later...
        return true;
    }
}

 

 

MotionEvent

 

안드로이드 프레임웍에서 터치 데이터를 얻기 위해서 android.view.MotionEvent 클래스를 참조한다.

커스텀 뷰에서 onTouchEvent onInterceptTouchEvent 메쏘드를 이용해 MotionEvent 받을 있다.

MotioEvent 통해 X/Y 좌표와 포인터의 크기와 압력을 있다. MotionEvent.getAction() 어떤 종류의 이벤트가 발생했는지 알려준다.

터치 입력의 일반적인 사용법 하나는 화면에 있는 아이콘을 드래그하는 것이다.

우리의 클래스에 터치 이벤트를 추가해 기능을 구현해 보자.

 

@Override
public boolean onTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    switch (action) {
    case MotionEvent.ACTION_DOWN: {
        final float x = ev.getX();
        final float y = ev.getY();
       
        // Remember where we started
        mLastTouchX = x;
        mLastTouchY = y;
        break;
    }
       
    case MotionEvent.ACTION_MOVE: {
        final float x = ev.getX();
        final float y = ev.getY();
       
        // Calculate the distance moved
        final float dx = x - mLastTouchX;
        final float dy = y - mLastTouchY;
       
        // Move the object
        mPosX += dx;
        mPosY += dy;
       
        // Remember this touch position for the next move event
        mLastTouchX = x;
        mLastTouchY = y;
       
        // Invalidate to request a redraw
        invalidate();
        break;
    }
    }
   
    return true;
}

 

코드는 멀티터치를 지원하는 디바이스에서 버그가 발생한다.

스크린 상에서 이미지를 드래그하는 도중 두번째 손가락을 스크린에 터치하고 첫번째 손가락을 떼면 이미지가 다른 곳에 나타난다.

이미지가 이동할 거리를 제일 나중에 입력된 위치를 기준으로 계산하므로, 첫번째 손가락을 떼면 두번째 손가락의 위치가 기본 위치가 되고 이동할 거리가

커지기 때문이다.

 

싱글 포인터의 위치를 알고 싶다면 MotionEvent.getX() MotionEvent.getY() 메쏘드를 사용하면 된다.

안드로이드 2.0에서 MotionEvent 멀티 포인터 데이터와 멀티터치 이벤트를 지원하게 개선되었다.

MotionEvent.getPointerCouint() 포인터의 개수를 알려준다. getX, getY 포인터의 인덱스를 인수로 받아 해당하는 값을 알려준다.

 

 

Index vs. ID

 

상위 레벨에서는 입력된 하나의 터치 스크린의 데이터를 바로 사용하지 않을 것이다. 왜냐하면 제스쳐는 시간에 따른 모션이 있어야 하기 때문이다.

포인터 인덱스는 복잡한 이벤트에 적합하지 않다. 이것은 단지 MotionEvent에서의 데이터 위치만 표시한다.

포인터는 터치 이벤트에 맵핑된 ID 가진다.

MotionEvent.getPointerId(index) 함수를 이용해 ID 얻을 있고, MotionEvent.findPointerIndex(id) 함수를 이용해 ID index 찾을 있다.

 

 

FeelingBetter?

 

포인터 ID 이용해 위의 예제를 수정해 보자.

 

private static final int INVALID_POINTER_ID = -1;

 

// The active pointer is the one currently moving our object.

private int mActivePointerId = INVALID_POINTER_ID;

 

// Existing code ...

 

@Override

public boolean onTouchEvent(MotionEvent ev) {

    final int action = ev.getAction();

    switch (action & MotionEvent.ACTION_MASK) {

    case MotionEvent.ACTION_DOWN: {

        final float x = ev.getX();

        final float y = ev.getY();

       

        mLastTouchX = x;

        mLastTouchY = y;

 

        // Save the ID of this pointer

        mActivePointerId = ev.getPointerId(0);

        break;

    }

       

    case MotionEvent.ACTION_MOVE: {

        // Find the index of the active pointer and fetch its position

        final int pointerIndex = ev.findPointerIndex(mActivePointerId);

        final float x = ev.getX(pointerIndex);

        final float y = ev.getY(pointerIndex);

       

        final float dx = x - mLastTouchX;

        final float dy = y - mLastTouchY;

       

        mPosX += dx;

        mPosY += dy;

       

        mLastTouchX = x;

        mLastTouchY = y;

       

        invalidate();

        break;

    }

       

    case MotionEvent.ACTION_UP: {

        mActivePointerId = INVALID_POINTER_ID;

        break;

    }

       

    case MotionEvent.ACTION_CANCEL: {

        mActivePointerId = INVALID_POINTER_ID;

        break;

    }

   

    case MotionEvent.ACTION_POINTER_UP: {

        // Extract the index of the pointer that left the touch sensor

        final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK)

                >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;

        final int pointerId = ev.getPointerId(pointerIndex);

        if (pointerId == mActivePointerId) {

            // This was our active pointer going up. Choose a new

            // active pointer and adjust accordingly.

            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;

            mLastTouchX = ev.getX(newPointerIndex);

            mLastTouchY = ev.getY(newPointerIndex);

            mActivePointerId = ev.getPointerId(newPointerIndex);

        }

        break;

    }

    }

   

    return true;

}

 

 

코드에 몇가지 새로운 것들이 있다.

switching() action 사용하지 않고 action & MotionEvent.ACTION_MASK 수정했고, 새로운 MotinEvent 상수인 MotionEvent.ACTION_POINTER_UP 사용한다.

ACTION_POINT_UP ACTION_POINT_DOWN 두번째 포인터가 눌려지거나 떼어질 발생한다.

화면에 지점을 누른 상태에서 다른 위치를 누르면 ACTION_DOWN 대신 ACTION_POINTER_DOWN 발생한다.

그리고 스크린의 여러 지점을 누른 상태에서 어느 지점을 누르지 않으면 ACTION_UP 대신 ACTION_POINTER_UP 발생한다.

 

ACTION_POINTER_DOWN ACTION_POINTER_UP action value 다른 정보를 포함한다.

값을 MotionEvent.ACION_MASK AND 하면 action 상수를 있고, ACION_POINTER_INDEX_MASK AND 하면 눌려지거나 떼어진 포인터의 인덱스를 있다.

예제에서 ACTION_POINTER_UP 경우에 인덱스를 추출해 action pointer ID 누르지 않는 포인터를 가리키는지 확인한다.

그런 경우, 다른 포인터를 기준으로 삼고 포인터의 값을 저장한다. 저장된 값을 ACTION_MOVE에서 이동 거리 계산을 사용하므로 항상 정확한 위치의 데이터로

이동 거리를 계산할 있다.

 

이것으로 앱에서 제스쳐를 처리할 필요한 모든 것을 설명했다.

하지만 복잡한 제스쳐를 인식하기가 힘들 있다. GestureDetectors 보자

 

 

GestureDetectors

 

앱은 다양한 다른 요구를 가지므로 안드로이드는 특별히 요구하지 않는 고수준 이벤트에서 터치 데이터를 처리하기 위해 시간을 소비하지 않는다.

GestureDetectors MotionEvents 받아 listerner 지정한 고수준 제스쳐 이벤트를 처리한다.

안드로이드는 종류의 GestureDetectors 제공하며, 개발자가 새로운 Detector 생성할 수도 있다.

GestureDetectors 솔루션이 아니라 일종의 패턴이다. 이것으로 별을 그리는 것과 같은 복잡한 제스쳐를 인식할 수도 있고 fling이나 더블탭과 같은 간단한 제스쳐를 만들 수도

있다.

 

android.view.GestureDetector 스크롤, fling, long press 포함해 안드로이드에서 사용되는 몇가지 single-point 제스쳐를 위한 이벤트를 발생한다.

Froyo에는 pinch 줌과 같은 손가락을 이용한 제스쳐를 처리하기 위해 android.view.ScaleGestureDetector 추가되었다.

 

제스쳐 검출은 MotionEvent에서 제공하는 public boolean 메쏘드를 이용한다.

메쏘드들은 이벤트를 처리할 경우 true 아니면 false 리턴한다. 제스쳐 검출기 콘텍스트에서 true 리턴한다는 것은 지금 처리되고 있는 제스쳐가 있다는 것을 암시한다.

다수의 제스쳐를 인식하기 위해 GestureDetector ScaleGestureDetector 사용한다.

 

제스쳐 검출기는 리스너 오브젝트를 이용해 검출된 제스쳐 이벤트를 생성자에게 알린다.

ScaleGestureDetector ScaleGestureDetector.OnScaleGestureListener 이용한다.

ScaleGestureDetector ScaleGestureDetector.OnScaleGestureListener help class 제공되며 확장할 있다.

 

예제에 scaling 지원하도록 수정해보자.

 

private ScaleGestureDetector mScaleDetector;

private float mScaleFactor = 1.f;

 

// Existing code ...

 

public TouchExampleView(Context context, AttributeSet attrs, int defStyle) {

    super(context, attrs, defStyle);

    mIcon = context.getResources().getDrawable(R.drawable.icon);

    mIcon.setBounds(0, 0, mIcon.getIntrinsicWidth(), mIcon.getIntrinsicHeight());

   

    // Create our ScaleGestureDetector

    mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());

}

 

@Override

public boolean onTouchEvent(MotionEvent ev) {

    // Let the ScaleGestureDetector inspect all events.

    mScaleDetector.onTouchEvent(ev);

   

    final int action = ev.getAction();

    switch (action & MotionEvent.ACTION_MASK) {

    case MotionEvent.ACTION_DOWN: {

        final float x = ev.getX();

        final float y = ev.getY();

       

        mLastTouchX = x;

        mLastTouchY = y;

        mActivePointerId = ev.getPointerId(0);

        break;

    }

       

    case MotionEvent.ACTION_MOVE: {

        final int pointerIndex = ev.findPointerIndex(mActivePointerId);

        final float x = ev.getX(pointerIndex);

        final float y = ev.getY(pointerIndex);

 

        // Only move if the ScaleGestureDetector isn't processing a gesture.

        if (!mScaleDetector.isInProgress()) {

            final float dx = x - mLastTouchX;

            final float dy = y - mLastTouchY;

 

            mPosX += dx;

            mPosY += dy;

 

            invalidate();

        }

 

        mLastTouchX = x;

        mLastTouchY = y;

 

        break;

    }

       

    case MotionEvent.ACTION_UP: {

        mActivePointerId = INVALID_POINTER_ID;

        break;

    }

       

    case MotionEvent.ACTION_CANCEL: {

        mActivePointerId = INVALID_POINTER_ID;

        break;

    }

   

    case MotionEvent.ACTION_POINTER_UP: {

        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)

                >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;

        final int pointerId = ev.getPointerId(pointerIndex);

        if (pointerId == mActivePointerId) {

            // This was our active pointer going up. Choose a new

            // active pointer and adjust accordingly.

            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;

            mLastTouchX = ev.getX(newPointerIndex);

            mLastTouchY = ev.getY(newPointerIndex);

            mActivePointerId = ev.getPointerId(newPointerIndex);

        }

        break;

    }

    }

   

    return true;

}

 

@Override

public void onDraw(Canvas canvas) {

    super.onDraw(canvas);

   

    canvas.save();

    canvas.translate(mPosX, mPosY);

    canvas.scale(mScaleFactor, mScaleFactor);

    mIcon.draw(canvas);

    canvas.restore();

}

 

private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

    @Override

    public boolean onScale(ScaleGestureDetector detector) {

        mScaleFactor *= detector.getScaleFactor();

       

        // Don't let the object get too small or too large.

        mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));

 

        invalidate();

        return true;

    }

}

 

 

From Example to Application

 

실제 앱에서 동작에 대해 미세 조정이 필요하다.

사용자는 ScaleGestureDetector.getFocusX() ScaleGestureDetector.getFocusY() 있는 제스쳐의 focal point 대한 줌의 값을 알고 싶어한다.

이런 세부값은 앱이 콘텐츠를 표현하고 그리는 방법에 따라 달라진다.

 

다른 터치스크린 하드웨어는 다른 능력을 갖는다. 터치스크린에 대한 정보는 PackageManager.hasSystemFeature() 이용해 런타임에서 확인할 있다.

 

사용자 인터페이스를 디자인할 사용자들이 다른 방식으로 그들의 디바이스를 사용할 있으며 모든 안드로이드 디바이스가 같지 않다는 것에 주의해야 한다.

어떤 이는 손을 사용하고 멀티터치에 서투를 수도 있다. 어떤 이는  패드나 트랙볼을 선호한다.

디자인된 제스쳐는 손가락으로 복잡한 기능이 가능하게 뿐만 아니라 제스쳐 이외의 수단으로 구현된 기능을 사용할 있게 하는 것이다.

 

 

 

=================================

=================================

=================================

 

 

반응형


관련글 더보기

댓글 영역