| 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(); |
| } |
| } |
| } |
| } |