제목은 뭔가 거창해 보이지만, 네이밍 센스가 없어서 그냥 그렇습니다. :)

눈치 빠른분들은 아시겠지만, 멀티터치를 통해서 view 를 이동, 크기변환, 회전 시키는 방법에 대해서 살펴보려고 합니다.


사실 살펴본다기보다, 그냥 코드공유 정도가 되겠네요.

사설이지만, 언제부터인가 블로깅을 한다는게 개인적으로는 무의미한다는 생각이 들더라고요...;;;

블로깅하려면, 아는 내용이어야 하고, 아는 내용이면 사실 저에게는 의미가 없지요;;

개인적인 기록용이나 투철한 홍익인간 정신이 아니고서야 블로깅은 역시나 어렵습니다.

그나마 기록용이라는것도 github 에 개인 프로젝트를 통째로 올리는 시대이므로 불필요하고요


모쪼록 사설이 길었습니다. :)





Android: Transform View via Multi-Touch


해당 코드는 인터넷에 해당기능 관련 코드는 많은데, 제가 딱 원하는 것이 없어서 따로 만들게 되었습니다.

제가 필요한 기능은 다음과 같았습니다.



필요했던 기능/제약(지원하는 기능)



1. view 의 size 가 wrap_content 이어야 함

 - 간혹 인터넷에 있는 몇몇 소스들을 보면, view 가 전체(match_parent) 임


2. translate / scale / rotate 모두 지원되어야 함

 - 몇몇 소스들은 몇개만 지원됨


막상 적고나니 조건이 몇개 없네요 :)



기본 원리는 Touch 시 각 Gesture 를 받아서 저장한 뒤, matrix 에 반영하여 출력하는 것입니다.

※ Touch Gesture 는 외부 소스코드를 그대로 사용하였습니다.


보시면 아시겠지만, FlexibleView 는 ImageView 를 상속받고 있는데, 꼭 ImageView 일 필요는 없습니다.

View 를 상속받고 있는 클래스면 무엇이든 상관없습니다.


상황에 맞게 상속받아서 사용하시면 되겠네요.

자세한 설명은 소스코드로 대체하겠습니다.




[FlexibleView]
package kr.co.crystalcube.translatable;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.Button;
import android.widget.ImageView;

import kr.co.crystalcube.translatable.gesture.MoveGestureDetector;
import kr.co.crystalcube.translatable.gesture.RotateGestureDetector;

/**
* Created by Naver on 2015-12-09.
*/
public class FlexibleView extends ImageView {
public enum FlipDirection { NONE, VERTICAL, HORIZONTAL }

private ScaleGestureDetector mScaleDetector;
protected float mScaleFactor = 1.f;
private static final float MAX_SCALE_FACTOR = 30.f;
private static final float MIN_SCALE_FACTOR = 0.3f;
private float mMaxScaleFactor = 30f;
private float mMinScaleFactor = 0.3f;

private MoveGestureDetector mMoveDetector;
protected float mFocusX = 0.f;
protected float mFocusY = 0.f;

private RotateGestureDetector mRotateDetector;
protected float mRotationDegree = 0.f;

private FlipDirection mFlipDirection = FlipDirection.NONE;
protected float mFlipX = 1;
protected float mFlipY = 1;

private boolean mIsMultiTouch = false;
private double mMoveDistance = 0f;
private PointF mTouchPoint = new PointF();
private static final int MAX_CLICK_DISTANCE = 4;
private static final int MAX_LONG_CLICK_DISTANCE = 16;


public FlexibleView(Context context) {
super(context);

this.init(context);
}

public FlexibleView(Context context, AttributeSet attrs) {
super(context, attrs);
this.init(context);
}

public FlexibleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.init(context);
}


private void init(Context context) {
this.mMaxScaleFactor = MAX_SCALE_FACTOR;
this.mMinScaleFactor = MIN_SCALE_FACTOR;
this.mScaleDetector = new ScaleGestureDetector(context, this.getScaleListener());
this.mMoveDetector = new MoveGestureDetector(context, this.getMoveListener());
this.mRotateDetector = new RotateGestureDetector(context, this.getRotateListener());
}

public void setMaxScaleFactor(float max) {
this.mMaxScaleFactor = max;
}

public void setMinScaleFactor(float min) {
this.mMinScaleFactor = min;
}


protected ScaleGestureDetector.SimpleOnScaleGestureListener getScaleListener() {
return new ScaleListener();
}

protected MoveGestureDetector.SimpleOnMoveGestureListener getMoveListener() {
return new MoveListener();
}


protected RotateGestureDetector.SimpleOnRotateGestureListener getRotateListener() {
return new RotateListener();
}



public void setFlip(FlipDirection direction) {
switch (direction) {
case HORIZONTAL:
this.mFlipX *= -1; break;
case VERTICAL:
this.mFlipY *= -1; break;
case NONE:
default:
this.mFlipX = 1;
this.mFlipY = 1;
break;
}

if(this.mFlipX > 1 && this.mFlipY > 1) {
this.mFlipDirection = FlipDirection.NONE;
} else if(this.mFlipX > 1 && this.mFlipY < 1) {
this.mFlipDirection = FlipDirection.VERTICAL;
} else if(this.mFlipX < 1 && this.mFlipY > 1) {
this.mFlipDirection = FlipDirection.HORIZONTAL;
}

this.invalidate();
}


public FlipDirection getFlipDirection() {
return this.mFlipDirection;
}

public float getFlipX() {
return this.mFlipX;
}


public float getFlipY() {
return this.mFlipY;
}


public float getScaleFactor() {
return this.mScaleFactor;
}


public void setScaleFactor(float scale) {
this.mScaleFactor = scale;
this.invalidate();
}

public void setFocus(float x, float y) {
this.mFocusX = x;
this.mFocusY = y;
this.invalidate();
}


public void moveFocus(float x, float y) {
this.setFocus(this.mFocusX + x, this.mFocusY + y);
}



@Override
public boolean performClick() {
if(this.mIsMultiTouch == true || this.mMoveDistance > MAX_CLICK_DISTANCE) { return false; }
return super.performClick();
}


@Override
public boolean performLongClick() {
if(this.mIsMultiTouch == true || this.mMoveDistance > MAX_LONG_CLICK_DISTANCE) { return false; }
return super.performLongClick();
}


private void getRowPoint(MotionEvent ev, int index, PointF point){
final int location[] = { 0, 0 };
getLocationOnScreen(location);

float x=ev.getX(index);
float y=ev.getY(index);

x *= getScaleX();
y *= getScaleY();

double angle = Math.toDegrees(Math.atan2(y, x));
angle += getRotation();

final float length = PointF.length(x, y);
x = (float)(length * Math.cos(Math.toRadians(angle))) + location[0];
y = (float)(length * Math.sin(Math.toRadians(angle))) + location[1];

point.set(x, y);
}



@Override
public boolean onTouchEvent(MotionEvent ev) {
if(this.isEnabled() == false) { return false; }

// compute transfrom
MotionEvent.PointerProperties[] prop = new MotionEvent.PointerProperties[ev.getPointerCount()];
MotionEvent.PointerCoords[] cords = new MotionEvent.PointerCoords[ev.getPointerCount()];

MotionEvent.PointerCoords firstCoords = new MotionEvent.PointerCoords();
ev.getPointerCoords(0, firstCoords);

for(int i = 0, n = ev.getPointerCount(); i < n; i++) {
MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
ev.getPointerProperties(i, properties);
prop[i] = properties;

MotionEvent.PointerCoords cod = new MotionEvent.PointerCoords();
ev.getPointerCoords(i, cod);

PointF rawPos = new PointF();
getRowPoint(ev,i, rawPos );
cod.x = (int)rawPos.x;
cod.y = (int)rawPos.y;
cords[i] = cod;
}


MotionEvent screenBaseMotionEvent = MotionEvent.obtain(ev.getDownTime(), ev.getEventTime(), ev.getAction(), ev.getPointerCount(), prop, cords, ev.getMetaState(), ev.getButtonState(), ev.getXPrecision(), ev.getYPrecision(), ev.getDeviceId(), ev.getEdgeFlags(), ev.getSource(), ev.getFlags());

this.mScaleDetector.onTouchEvent(screenBaseMotionEvent);
this.mRotateDetector.onTouchEvent(screenBaseMotionEvent);
this.mMoveDetector.onTouchEvent(screenBaseMotionEvent);

this.computeClickEvent(ev); // adjust click event
super.onTouchEvent(ev);

this.invalidate();
return true;
}



private void computeClickEvent(MotionEvent ev) {
// check if it is moved
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
this.mIsMultiTouch = (ev.getPointerCount() < 2 ? false : true);
this.mTouchPoint = new PointF(ev.getRawX(), ev.getRawY());
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
{
if(ev.getPointerCount() > 1) { this.mIsMultiTouch = true; break; }
this.mMoveDistance = getDistance(new PointF(ev.getRawX(), ev.getRawY()), this.mTouchPoint);
break;
}
}
}



private static double getDistance(PointF point1, PointF point2) {
return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2));
}




@Override
protected void onDraw(Canvas canvas) {
this.setTranslationX(this.mFocusX);
this.setTranslationY(this.mFocusY);
this.setScaleX(this.mScaleFactor * this.mFlipX);
this.setScaleY(this.mScaleFactor * this.mFlipY);
this.setRotation(this.mRotationDegree);

super.onDraw(canvas);
}


private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
float mergedScaleFactor = mScaleFactor * scaleFactor;

if(mergedScaleFactor > mMaxScaleFactor || mergedScaleFactor < mMinScaleFactor) { return false; }

mScaleFactor = mergedScaleFactor;
mScaleFactor = Math.max(mMinScaleFactor, mScaleFactor);
mScaleFactor = Math.min(mMaxScaleFactor, mScaleFactor);

return true;
}
}



private class MoveListener extends MoveGestureDetector.SimpleOnMoveGestureListener {
@Override
public boolean onMove(MoveGestureDetector detector) {
PointF delta = detector.getFocusDelta();
mFocusX += delta.x;
mFocusY += delta.y;

return true;
}
}



private class RotateListener extends RotateGestureDetector.SimpleOnRotateGestureListener {
@Override
public boolean onRotate(RotateGestureDetector detector) {
mRotationDegree -= detector.getRotationDegreesDelta();
return true;
}
}

}



[BaseGestureDetector]

package kr.co.crystalcube.translatable.gesture;

import android.content.Context;
import android.view.MotionEvent;

/**
* Created by Naver on 2015-12-07.
*/
public abstract class BaseGestureDetector {
protected final Context mContext;
protected boolean mGestureInProgress;

protected MotionEvent mPrevEvent;
protected MotionEvent mCurrEvent;

protected float mCurrPressure;
protected float mPrevPressure;
protected long mTimeDelta;


/**
* This value is the threshold ratio between the previous combined pressure
* and the current combined pressure. When pressure decreases rapidly
* between events the position values can often be imprecise, as it usually
* indicates that the user is in the process of lifting a pointer off of the
* device. This value was tuned experimentally.
*/
protected static final float PRESSURE_THRESHOLD = 0.67f;


public BaseGestureDetector(Context context) {
mContext = context;
}

/**
* All gesture detectors need to be called through this method to be able to
* detect gestures. This method delegates work to handler methods
* (handleStartProgressEvent, handleInProgressEvent) implemented in
* extending classes.
*
* @param event
* @return
*/
public boolean onTouchEvent(MotionEvent event){
final int actionCode = event.getAction() & MotionEvent.ACTION_MASK;
if (!mGestureInProgress) {
handleStartProgressEvent(actionCode, event);
} else {
handleInProgressEvent(actionCode, event);
}
return true;
}

/**
* Called when the current event occurred when NO gesture is in progress
* yet. The handling in this implementation may set the gesture in progress
* (via mGestureInProgress) or out of progress
* @param actionCode
* @param event
*/
protected abstract void handleStartProgressEvent(int actionCode, MotionEvent event);

/**
* Called when the current event occurred when a gesture IS in progress. The
* handling in this implementation may set the gesture out of progress (via
* mGestureInProgress).
* @param actionCode
* @param event
*/
protected abstract void handleInProgressEvent(int actionCode, MotionEvent event);


protected void updateStateByEvent(MotionEvent curr){
final MotionEvent prev = mPrevEvent;

// Reset mCurrEvent
if (mCurrEvent != null) {
mCurrEvent.recycle();
mCurrEvent = null;
}
mCurrEvent = MotionEvent.obtain(curr);


// Delta time
mTimeDelta = curr.getEventTime() - prev.getEventTime();

// Pressure
mCurrPressure = curr.getPressure(curr.getActionIndex());
mPrevPressure = prev.getPressure(prev.getActionIndex());
}

protected void resetState() {
if (mPrevEvent != null) {
mPrevEvent.recycle();
mPrevEvent = null;
}
if (mCurrEvent != null) {
mCurrEvent.recycle();
mCurrEvent = null;
}
mGestureInProgress = false;
}


/**
* Returns {@code true} if a gesture is currently in progress.
* @return {@code true} if a gesture is currently in progress, {@code false} otherwise.
*/
public boolean isInProgress() {
return mGestureInProgress;
}

/**
* Return the time difference in milliseconds between the previous accepted
* GestureDetector event and the current GestureDetector event.
*
* @return Time difference since the last move event in milliseconds.
*/
public long getTimeDelta() {
return mTimeDelta;
}

/**
* Return the event time of the current GestureDetector event being
* processed.
*
* @return Current GestureDetector event time in milliseconds.
*/
public long getEventTime() {
return mCurrEvent.getEventTime();
}

}



[MoveGestureDetector]

package kr.co.crystalcube.translatable.gesture;

import android.content.Context;
import android.graphics.PointF;
import android.view.MotionEvent;

/**
* Created by Naver on 2015-12-07.
*/
public class MoveGestureDetector extends BaseGestureDetector {

/**
* Listener which must be implemented which is used by MoveGestureDetector
* to perform callbacks to any implementing class which is registered to a
* MoveGestureDetector via the constructor.
*
* @see MoveGestureDetector.SimpleOnMoveGestureListener
*/
public interface OnMoveGestureListener {
public boolean onMove(MoveGestureDetector detector);
public boolean onMoveBegin(MoveGestureDetector detector);
public void onMoveEnd(MoveGestureDetector detector);
}

/**
* Helper class which may be extended and where the methods may be
* implemented. This way it is not necessary to implement all methods
* of OnMoveGestureListener.
*/
public static class SimpleOnMoveGestureListener implements OnMoveGestureListener {
public boolean onMove(MoveGestureDetector detector) {
return false;
}

public boolean onMoveBegin(MoveGestureDetector detector) {
return true;
}

public void onMoveEnd(MoveGestureDetector detector) {
// Do nothing, overridden implementation may be used
}
}

private static final PointF FOCUS_DELTA_ZERO = new PointF();

private final OnMoveGestureListener mListener;

private PointF mCurrFocusInternal;
private PointF mPrevFocusInternal;
private PointF mFocusExternal = new PointF();
private PointF mFocusDeltaExternal = new PointF();


public MoveGestureDetector(Context context, OnMoveGestureListener listener) {
super(context);
mListener = listener;
}

@Override
protected void handleStartProgressEvent(int actionCode, MotionEvent event){
switch (actionCode) {
case MotionEvent.ACTION_DOWN:
resetState(); // In case we missed an UP/CANCEL event

mPrevEvent = MotionEvent.obtain(event);
mTimeDelta = 0;

updateStateByEvent(event);
break;

case MotionEvent.ACTION_MOVE:
mGestureInProgress = mListener.onMoveBegin(this);
break;
}
}

@Override
protected void handleInProgressEvent(int actionCode, MotionEvent event){
switch (actionCode) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mListener.onMoveEnd(this);
resetState();
break;

case MotionEvent.ACTION_MOVE:
updateStateByEvent(event);

// Only accept the event if our relative pressure is within
// a certain limit. This can help filter shaky data as a
// finger is lifted.
if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
final boolean updatePrevious = mListener.onMove(this);
if (updatePrevious) {
mPrevEvent.recycle();
mPrevEvent = MotionEvent.obtain(event);
}
}
break;
}
}

public MotionEvent getEvent() {
return mPrevEvent;
}

protected void updateStateByEvent(MotionEvent curr) {
super.updateStateByEvent(curr);

final MotionEvent prev = mPrevEvent;

// Focus intenal
mCurrFocusInternal = determineFocalPoint(curr);
mPrevFocusInternal = determineFocalPoint(prev);

// Focus external
// - Prevent skipping of focus delta when a finger is added or removed
boolean mSkipNextMoveEvent = prev.getPointerCount() != curr.getPointerCount();
mFocusDeltaExternal = mSkipNextMoveEvent ? FOCUS_DELTA_ZERO : new PointF(mCurrFocusInternal.x - mPrevFocusInternal.x, mCurrFocusInternal.y - mPrevFocusInternal.y);

// - Don't directly use mFocusInternal (or skipping will occur). Add
// unskipped delta values to mFocusExternal instead.
mFocusExternal.x += mFocusDeltaExternal.x;
mFocusExternal.y += mFocusDeltaExternal.y;
}


private PointF determineFocalPoint(MotionEvent e){
// Number of fingers on screen
final int pCount = e.getPointerCount();
float x = 0f;
float y = 0f;

for(int i = 0; i < pCount; i++){
x += e.getX(i);
y += e.getY(i);
}

return new PointF(x/pCount, y/pCount);
}

public float getFocusX() {
return mFocusExternal.x;
}

public float getFocusY() {
return mFocusExternal.y;
}

public PointF getFocusDelta() {
return mFocusDeltaExternal;
}

}


[RotateGestureDetector]

package kr.co.crystalcube.translatable.gesture;

import android.content.Context;
import android.view.MotionEvent;

/**
* Created by Naver on 2015-12-07.
*/
public class RotateGestureDetector extends TwoFingerGestureDetector {

/**
* Listener which must be implemented which is used by RotateGestureDetector
* to perform callbacks to any implementing class which is registered to a
* RotateGestureDetector via the constructor.
*
* @see RotateGestureDetector.SimpleOnRotateGestureListener
*/
public interface OnRotateGestureListener {
public boolean onRotate(RotateGestureDetector detector);
public boolean onRotateBegin(RotateGestureDetector detector);
public void onRotateEnd(RotateGestureDetector detector);
}

/**
* Helper class which may be extended and where the methods may be
* implemented. This way it is not necessary to implement all methods
* of OnRotateGestureListener.
*/
public static class SimpleOnRotateGestureListener implements OnRotateGestureListener {
public boolean onRotate(RotateGestureDetector detector) {
return false;
}

public boolean onRotateBegin(RotateGestureDetector detector) {
return true;
}

public void onRotateEnd(RotateGestureDetector detector) {
// Do nothing, overridden implementation may be used
}
}


private final OnRotateGestureListener mListener;
private boolean mSloppyGesture;

public RotateGestureDetector(Context context, OnRotateGestureListener listener) {
super(context);
mListener = listener;
}

@Override
protected void handleStartProgressEvent(int actionCode, MotionEvent event){
switch (actionCode) {
case MotionEvent.ACTION_POINTER_DOWN:
// At least the second finger is on screen now

resetState(); // In case we missed an UP/CANCEL event
mPrevEvent = MotionEvent.obtain(event);
mTimeDelta = 0;

updateStateByEvent(event);

// See if we have a sloppy gesture
mSloppyGesture = isSloppyGesture(event);
if(!mSloppyGesture){
// No, start gesture now
mGestureInProgress = mListener.onRotateBegin(this);
}
break;

case MotionEvent.ACTION_MOVE:
if (!mSloppyGesture) {
break;
}

// See if we still have a sloppy gesture
mSloppyGesture = isSloppyGesture(event);
if(!mSloppyGesture){
// No, start normal gesture now
mGestureInProgress = mListener.onRotateBegin(this);
}

break;

case MotionEvent.ACTION_POINTER_UP:
if (!mSloppyGesture) {
break;
}

break;
}
}


@Override
protected void handleInProgressEvent(int actionCode, MotionEvent event){
switch (actionCode) {
case MotionEvent.ACTION_POINTER_UP:
// Gesture ended but
updateStateByEvent(event);

if (!mSloppyGesture) {
mListener.onRotateEnd(this);
}

resetState();
break;

case MotionEvent.ACTION_CANCEL:
if (!mSloppyGesture) {
mListener.onRotateEnd(this);
}

resetState();
break;

case MotionEvent.ACTION_MOVE:
updateStateByEvent(event);

// Only accept the event if our relative pressure is within
// a certain limit. This can help filter shaky data as a
// finger is lifted.
if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
final boolean updatePrevious = mListener.onRotate(this);
if (updatePrevious) {
mPrevEvent.recycle();
mPrevEvent = MotionEvent.obtain(event);
}
}
break;
}
}

@Override
protected void resetState() {
super.resetState();
mSloppyGesture = false;
}


/**
* Return the rotation difference from the previous rotate event to the current
* event.
*
* @return The current rotation //difference in degrees.
*/
public float getRotationDegreesDelta() {
double diffRadians = Math.atan2(mPrevFingerDiffY, mPrevFingerDiffX) - Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX);
return (float) (diffRadians * 180 / Math.PI);
}
}


[TwoFingerGestureDetector]

package kr.co.crystalcube.translatable.gesture;

import android.content.Context;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

/**
* Created by Naver on 2015-12-07.
*/
public abstract class TwoFingerGestureDetector extends BaseGestureDetector {

private final float mEdgeSlop;
private float mRightSlopEdge;
private float mBottomSlopEdge;

protected float mPrevFingerDiffX;
protected float mPrevFingerDiffY;
protected float mCurrFingerDiffX;
protected float mCurrFingerDiffY;

private float mCurrLen;
private float mPrevLen;

public TwoFingerGestureDetector(Context context) {
super(context);

ViewConfiguration config = ViewConfiguration.get(context);
mEdgeSlop = config.getScaledEdgeSlop();
}

@Override
protected abstract void handleStartProgressEvent(int actionCode, MotionEvent event);

@Override
protected abstract void handleInProgressEvent(int actionCode, MotionEvent event);

protected void updateStateByEvent(MotionEvent curr){
super.updateStateByEvent(curr);

final MotionEvent prev = mPrevEvent;

mCurrLen = -1;
mPrevLen = -1;

// Previous
if(prev.getPointerCount() >= 2) {
final float px0 = prev.getX(0);
final float py0 = prev.getY(0);
final float px1 = prev.getX(1);
final float py1 = prev.getY(1);
final float pvx = px1 - px0;
final float pvy = py1 - py0;
mPrevFingerDiffX = pvx;
mPrevFingerDiffY = pvy;
}

// Current
if(curr.getPointerCount() >= 2) {
final float cx0 = curr.getX(0);
final float cy0 = curr.getY(0);
final float cx1 = curr.getX(1);
final float cy1 = curr.getY(1);
final float cvx = cx1 - cx0;
final float cvy = cy1 - cy0;
mCurrFingerDiffX = cvx;
mCurrFingerDiffY = cvy;
}
}

/**
* Return the current distance between the two pointers forming the
* gesture in progress.
*
* @return Distance between pointers in pixels.
*/
public float getCurrentSpan() {
if (mCurrLen == -1) {
final float cvx = mCurrFingerDiffX;
final float cvy = mCurrFingerDiffY;
mCurrLen = (float)Math.sqrt((double)(cvx * cvx + cvy * cvy));
}
return mCurrLen;
}

/**
* Return the previous distance between the two pointers forming the
* gesture in progress.
*
* @return Previous distance between pointers in pixels.
*/
public float getPreviousSpan() {
if (mPrevLen == -1) {
final float pvx = mPrevFingerDiffX;
final float pvy = mPrevFingerDiffY;
mPrevLen = (float)Math.sqrt((double)(pvx*pvx + pvy*pvy));
}
return mPrevLen;
}

/**
* MotionEvent has no getRawX(int) method; simulate it pending future API approval.
* @param event
* @param pointerIndex
* @return
*/
protected static float getRawX(MotionEvent event, int pointerIndex) {
float offset = event.getX() - event.getRawX();
if(pointerIndex < event.getPointerCount()){
return event.getX(pointerIndex) + offset;
}
return 0f;
}

/**
* MotionEvent has no getRawY(int) method; simulate it pending future API approval.
* @param event
* @param pointerIndex
* @return
*/
protected static float getRawY(MotionEvent event, int pointerIndex) {
float offset = event.getY() - event.getRawY();
if(pointerIndex < event.getPointerCount()){
return event.getY(pointerIndex) + offset;
}
return 0f;
}

/**
* Check if we have a sloppy gesture. Sloppy gestures can happen if the edge
* of the user's hand is touching the screen, for example.
*
* @param event
* @return
*/
protected boolean isSloppyGesture(MotionEvent event){
// As orientation can change, query the metrics in touch down
DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
mRightSlopEdge = metrics.widthPixels - mEdgeSlop;
mBottomSlopEdge = metrics.heightPixels - mEdgeSlop;

final float edgeSlop = mEdgeSlop;
final float rightSlop = mRightSlopEdge;
final float bottomSlop = mBottomSlopEdge;

final float x0 = event.getRawX();
final float y0 = event.getRawY();
final float x1 = getRawX(event, 1);
final float y1 = getRawY(event, 1);

boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop
|| x0 > rightSlop || y0 > bottomSlop;
boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop
|| x1 > rightSlop || y1 > bottomSlop;

if (p0sloppy && p1sloppy) {
return true;
} else if (p0sloppy) {
return true;
} else if (p1sloppy) {
return true;
}
return false;
}

}