package com.androidplot.xy;
import android.content.Context;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
public class XYPlotZoomPan extends XYPlot implements OnTouchListener {
private static final float MIN_DIST_2_FING = 5f;
// Definition of the touch states
private enum State
{
NONE,
ONE_FINGER_DRAG,
TWO_FINGERS_DRAG
}
private State mode = State.NONE;
private float minXLimit = Float.MAX_VALUE;
private float maxXLimit = Float.MAX_VALUE;
private float minYLimit = Float.MAX_VALUE;
private float maxYLimit = Float.MAX_VALUE;
private float lastMinX = Float.MAX_VALUE;
private float lastMaxX = Float.MAX_VALUE;
private float lastMinY = Float.MAX_VALUE;
private float lastMaxY = Float.MAX_VALUE;
private PointF firstFingerPos;
private float mDistX;
private boolean mZoomEnabled; //default is enabled
private boolean mZoomVertically;
private boolean mZoomHorizontally;
private boolean mCalledBySelf;
private boolean mZoomEnabledInit;
private boolean mZoomVerticallyInit;
private boolean mZoomHorizontallyInit;
public XYPlotZoomPan(Context context, String title, RenderMode mode) {
super(context, title, mode);
setZoomEnabled(true); //Default is ZoomEnabled if instantiated programmatically
}
public XYPlotZoomPan(final Context context, final AttributeSet attrs) {
super(context, attrs);
if(mZoomEnabled || !mZoomEnabledInit) {
setZoomEnabled(true);
}
if(!mZoomHorizontallyInit) {
mZoomHorizontally = true;
}
if(!mZoomVerticallyInit) {
mZoomVertically = true;
}
}
public XYPlotZoomPan(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
if(mZoomEnabled || !mZoomEnabledInit) {
setZoomEnabled(true);
}
if(!mZoomHorizontallyInit) {
mZoomHorizontally = true;
}
if(!mZoomVerticallyInit) {
mZoomVertically = true;
}
}
public XYPlotZoomPan(final Context context, final String title) {
super(context, title);
}
@Override
public void setOnTouchListener(OnTouchListener l) {
if(l != this) {
mZoomEnabled = false;
}
super.setOnTouchListener(l);
}
public boolean getZoomVertically() {
return mZoomVertically;
}
public void setZoomVertically(boolean zoomVertically) {
mZoomVertically = zoomVertically;
mZoomVerticallyInit = true;
}
public boolean getZoomHorizontally() {
return mZoomHorizontally;
}
public void setZoomHorizontally(boolean zoomHorizontally) {
mZoomHorizontally = zoomHorizontally;
mZoomHorizontallyInit = true;
}
public void setZoomEnabled(boolean enabled) {
if(enabled) {
setOnTouchListener(this);
} else {
setOnTouchListener(null);
}
mZoomEnabled = enabled;
mZoomEnabledInit = true;
}
public boolean getZoomEnabled() {
return mZoomEnabled;
}
private float getMinXLimit() {
if(minXLimit == Float.MAX_VALUE) {
minXLimit = getCalculatedMinX().floatValue();
lastMinX = minXLimit;
}
return minXLimit;
}
private float getMaxXLimit() {
if(maxXLimit == Float.MAX_VALUE) {
maxXLimit = getCalculatedMaxX().floatValue();
lastMaxX = maxXLimit;
}
return maxXLimit;
}
private float getMinYLimit() {
if(minYLimit == Float.MAX_VALUE) {
minYLimit = getCalculatedMinY().floatValue();
lastMinY = minYLimit;
}
return minYLimit;
}
private float getMaxYLimit() {
if(maxYLimit == Float.MAX_VALUE) {
maxYLimit = getCalculatedMaxY().floatValue();
lastMaxY = maxYLimit;
}
return maxYLimit;
}
private float getLastMinX() {
if(lastMinX == Float.MAX_VALUE) {
lastMinX = getCalculatedMinX().floatValue();
}
return lastMinX;
}
private float getLastMaxX() {
if(lastMaxX == Float.MAX_VALUE) {
lastMaxX = getCalculatedMaxX().floatValue();
}
return lastMaxX;
}
private float getLastMinY() {
if(lastMinY == Float.MAX_VALUE) {
lastMinY = getCalculatedMinY().floatValue();
}
return lastMinY;
}
private float getLastMaxY() {
if(lastMaxY == Float.MAX_VALUE) {
lastMaxY = getCalculatedMaxY().floatValue();
}
return lastMaxY;
}
@Override
public synchronized void setDomainBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode, final Number upperBoundary, final BoundaryMode upperBoundaryMode) {
super.setDomainBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode);
if(mCalledBySelf) {
mCalledBySelf = false;
} else {
minXLimit = lowerBoundaryMode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinX().floatValue();
maxXLimit = upperBoundaryMode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxX().floatValue();
lastMinX = minXLimit;
lastMaxX = maxXLimit;
}
}
@Override
public synchronized void setRangeBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode, final Number upperBoundary, final BoundaryMode upperBoundaryMode) {
super.setRangeBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode);
if(mCalledBySelf) {
mCalledBySelf = false;
} else {
minYLimit = lowerBoundaryMode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinY().floatValue();
maxYLimit = upperBoundaryMode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxY().floatValue();
lastMinY = minYLimit;
lastMaxY = maxYLimit;
}
}
@Override
public synchronized void setDomainBoundaries(final Number lowerBoundary, final Number upperBoundary, final BoundaryMode mode) {
super.setDomainBoundaries(lowerBoundary, upperBoundary, mode);
if(mCalledBySelf) {
mCalledBySelf = false;
} else {
minXLimit = mode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinX().floatValue();
maxXLimit = mode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxX().floatValue();
lastMinX = minXLimit;
lastMaxX = maxXLimit;
}
}
@Override
public synchronized void setRangeBoundaries(final Number lowerBoundary, final Number upperBoundary, final BoundaryMode mode) {
super.setRangeBoundaries(lowerBoundary, upperBoundary, mode);
if(mCalledBySelf) {
mCalledBySelf = false;
} else {
minYLimit = mode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinY().floatValue();
maxYLimit = mode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxY().floatValue();
lastMinY = minYLimit;
lastMaxY = maxYLimit;
}
}
@Override
public boolean onTouch(final View view, final MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN: // start gesture
firstFingerPos = new PointF(event.getX(), event.getY());
mode = State.ONE_FINGER_DRAG;
break;
case MotionEvent.ACTION_POINTER_DOWN: // second finger
{
mDistX = getXDistance(event);
// the distance check is done to avoid false alarms
if(mDistX > MIN_DIST_2_FING || mDistX < -MIN_DIST_2_FING) {
mode = State.TWO_FINGERS_DRAG;
}
break;
}
case MotionEvent.ACTION_POINTER_UP: // end zoom
mode = State.NONE;
break;
case MotionEvent.ACTION_MOVE:
if(mode == State.ONE_FINGER_DRAG) {
pan(event);
} else if(mode == State.TWO_FINGERS_DRAG) {
zoom(event);
}
break;
}
return true;
}
private float getXDistance(final MotionEvent event) {
return event.getX(0) - event.getX(1);
}
private void pan(final MotionEvent motionEvent) {
final PointF oldFirstFinger = firstFingerPos; //save old position of finger
firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position
PointF newX = new PointF();
if(mZoomHorizontally) {
calculatePan(oldFirstFinger, newX, true);
mCalledBySelf = true;
super.setDomainBoundaries(newX.x, newX.y, BoundaryMode.FIXED);
lastMinX = newX.x;
lastMaxX = newX.y;
}
if(mZoomVertically) {
calculatePan(oldFirstFinger, newX, false);
mCalledBySelf = true;
super.setRangeBoundaries(newX.x, newX.y, BoundaryMode.FIXED);
lastMinY = newX.x;
lastMaxY = newX.y;
}
redraw();
}
private void calculatePan(final PointF oldFirstFinger, PointF newX, final boolean horizontal) {
final float offset;
// multiply the absolute finger movement for a factor.
// the factor is dependent on the calculated min and max
if(horizontal) {
newX.x = getLastMinX();
newX.y = getLastMaxX();
offset = (oldFirstFinger.x - firstFingerPos.x) * ((newX.y - newX.x) / getWidth());
} else {
newX.x = getLastMinY();
newX.y = getLastMaxY();
offset = -(oldFirstFinger.y - firstFingerPos.y) * ((newX.y - newX.x) / getHeight());
}
// move the calculated offset
newX.x = newX.x + offset;
newX.y = newX.y + offset;
//get the distance between max and min
final float diff = newX.y - newX.x;
//check if we reached the limit of panning
if(horizontal) {
if(newX.x < getMinXLimit()) {
newX.x = getMinXLimit();
newX.y = newX.x + diff;
}
if(newX.y > getMaxXLimit()) {
newX.y = getMaxXLimit();
newX.x = newX.y - diff;
}
} else {
if(newX.x < getMinYLimit()) {
newX.x = getMinYLimit();
newX.y = newX.x + diff;
}
if(newX.y > getMaxYLimit()) {
newX.y = getMaxYLimit();
newX.x = newX.y - diff;
}
}
}
private void zoom(final MotionEvent motionEvent) {
final float oldDist = mDistX;
final float newDist = getXDistance(motionEvent);
// sign change! Fingers have crossed ;-)
if(oldDist > 0 && newDist < 0 || oldDist < 0 && newDist > 0) {
return;
}
mDistX = newDist;
float scale = (oldDist / mDistX);
// sanity check
if(Float.isInfinite(scale) || Float.isNaN(scale) || scale > -0.001 && scale < 0.001) {
return;
}
PointF newX = new PointF();
if(mZoomHorizontally) {
calculateZoom(scale, newX, true);
mCalledBySelf = true;
super.setDomainBoundaries(newX.x, newX.y, BoundaryMode.FIXED);
lastMinX = newX.x;
lastMaxX = newX.y;
}
if(mZoomVertically) {
calculateZoom(scale, newX, false);
mCalledBySelf = true;
super.setRangeBoundaries(newX.x, newX.y, BoundaryMode.FIXED);
lastMinY = newX.x;
lastMaxY = newX.y;
}
redraw();
}
private void calculateZoom(float scale, PointF newX, final boolean horizontal) {
final float calcMax;
final float span;
if(horizontal) {
calcMax = getLastMaxX();
span = calcMax - getLastMinX();
} else {
calcMax = getLastMaxY();
span = calcMax - getLastMinY();
}
final float midPoint = calcMax - (span / 2.0f);
final float offset = span * scale / 2.0f;
newX.x = midPoint - offset;
newX.y = midPoint + offset;
if(horizontal) {
if(newX.x < getMinXLimit()) {
newX.x = getMinXLimit();
}
if(newX.y > getMaxXLimit()) {
newX.y = getMaxXLimit();
}
} else {
if(newX.x < getMinYLimit()) {
newX.x = getMinYLimit();
}
if(newX.y > getMaxYLimit()) {
newX.y = getMaxYLimit();
}
}
}
}