1package com.androidplot.xy; 2 3import android.content.Context; 4import android.graphics.PointF; 5import android.util.AttributeSet; 6import android.view.MotionEvent; 7import android.view.View; 8import android.view.View.OnTouchListener; 9 10public class XYPlotZoomPan extends XYPlot implements OnTouchListener { 11 private static final float MIN_DIST_2_FING = 5f; 12 13 // Definition of the touch states 14 private enum State 15 { 16 NONE, 17 ONE_FINGER_DRAG, 18 TWO_FINGERS_DRAG 19 } 20 21 private State mode = State.NONE; 22 private float minXLimit = Float.MAX_VALUE; 23 private float maxXLimit = Float.MAX_VALUE; 24 private float minYLimit = Float.MAX_VALUE; 25 private float maxYLimit = Float.MAX_VALUE; 26 private float lastMinX = Float.MAX_VALUE; 27 private float lastMaxX = Float.MAX_VALUE; 28 private float lastMinY = Float.MAX_VALUE; 29 private float lastMaxY = Float.MAX_VALUE; 30 private PointF firstFingerPos; 31 private float mDistX; 32 private boolean mZoomEnabled; //default is enabled 33 private boolean mZoomVertically; 34 private boolean mZoomHorizontally; 35 private boolean mCalledBySelf; 36 private boolean mZoomEnabledInit; 37 private boolean mZoomVerticallyInit; 38 private boolean mZoomHorizontallyInit; 39 40 public XYPlotZoomPan(Context context, String title, RenderMode mode) { 41 super(context, title, mode); 42 setZoomEnabled(true); //Default is ZoomEnabled if instantiated programmatically 43 } 44 45 public XYPlotZoomPan(final Context context, final AttributeSet attrs) { 46 super(context, attrs); 47 if(mZoomEnabled || !mZoomEnabledInit) { 48 setZoomEnabled(true); 49 } 50 if(!mZoomHorizontallyInit) { 51 mZoomHorizontally = true; 52 } 53 if(!mZoomVerticallyInit) { 54 mZoomVertically = true; 55 } 56 } 57 58 public XYPlotZoomPan(final Context context, final AttributeSet attrs, final int defStyle) { 59 super(context, attrs, defStyle); 60 if(mZoomEnabled || !mZoomEnabledInit) { 61 setZoomEnabled(true); 62 } 63 if(!mZoomHorizontallyInit) { 64 mZoomHorizontally = true; 65 } 66 if(!mZoomVerticallyInit) { 67 mZoomVertically = true; 68 } 69 } 70 71 public XYPlotZoomPan(final Context context, final String title) { 72 super(context, title); 73 } 74 75 @Override 76 public void setOnTouchListener(OnTouchListener l) { 77 if(l != this) { 78 mZoomEnabled = false; 79 } 80 super.setOnTouchListener(l); 81 } 82 83 public boolean getZoomVertically() { 84 return mZoomVertically; 85 } 86 87 public void setZoomVertically(boolean zoomVertically) { 88 mZoomVertically = zoomVertically; 89 mZoomVerticallyInit = true; 90 } 91 92 public boolean getZoomHorizontally() { 93 return mZoomHorizontally; 94 } 95 96 public void setZoomHorizontally(boolean zoomHorizontally) { 97 mZoomHorizontally = zoomHorizontally; 98 mZoomHorizontallyInit = true; 99 } 100 101 public void setZoomEnabled(boolean enabled) { 102 if(enabled) { 103 setOnTouchListener(this); 104 } else { 105 setOnTouchListener(null); 106 } 107 mZoomEnabled = enabled; 108 mZoomEnabledInit = true; 109 } 110 111 public boolean getZoomEnabled() { 112 return mZoomEnabled; 113 } 114 115 private float getMinXLimit() { 116 if(minXLimit == Float.MAX_VALUE) { 117 minXLimit = getCalculatedMinX().floatValue(); 118 lastMinX = minXLimit; 119 } 120 return minXLimit; 121 } 122 123 private float getMaxXLimit() { 124 if(maxXLimit == Float.MAX_VALUE) { 125 maxXLimit = getCalculatedMaxX().floatValue(); 126 lastMaxX = maxXLimit; 127 } 128 return maxXLimit; 129 } 130 131 private float getMinYLimit() { 132 if(minYLimit == Float.MAX_VALUE) { 133 minYLimit = getCalculatedMinY().floatValue(); 134 lastMinY = minYLimit; 135 } 136 return minYLimit; 137 } 138 139 private float getMaxYLimit() { 140 if(maxYLimit == Float.MAX_VALUE) { 141 maxYLimit = getCalculatedMaxY().floatValue(); 142 lastMaxY = maxYLimit; 143 } 144 return maxYLimit; 145 } 146 147 private float getLastMinX() { 148 if(lastMinX == Float.MAX_VALUE) { 149 lastMinX = getCalculatedMinX().floatValue(); 150 } 151 return lastMinX; 152 } 153 154 private float getLastMaxX() { 155 if(lastMaxX == Float.MAX_VALUE) { 156 lastMaxX = getCalculatedMaxX().floatValue(); 157 } 158 return lastMaxX; 159 } 160 161 private float getLastMinY() { 162 if(lastMinY == Float.MAX_VALUE) { 163 lastMinY = getCalculatedMinY().floatValue(); 164 } 165 return lastMinY; 166 } 167 168 private float getLastMaxY() { 169 if(lastMaxY == Float.MAX_VALUE) { 170 lastMaxY = getCalculatedMaxY().floatValue(); 171 } 172 return lastMaxY; 173 } 174 175 @Override 176 public synchronized void setDomainBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode, final Number upperBoundary, final BoundaryMode upperBoundaryMode) { 177 super.setDomainBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode); 178 if(mCalledBySelf) { 179 mCalledBySelf = false; 180 } else { 181 minXLimit = lowerBoundaryMode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinX().floatValue(); 182 maxXLimit = upperBoundaryMode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxX().floatValue(); 183 lastMinX = minXLimit; 184 lastMaxX = maxXLimit; 185 } 186 } 187 188 @Override 189 public synchronized void setRangeBoundaries(final Number lowerBoundary, final BoundaryMode lowerBoundaryMode, final Number upperBoundary, final BoundaryMode upperBoundaryMode) { 190 super.setRangeBoundaries(lowerBoundary, lowerBoundaryMode, upperBoundary, upperBoundaryMode); 191 if(mCalledBySelf) { 192 mCalledBySelf = false; 193 } else { 194 minYLimit = lowerBoundaryMode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinY().floatValue(); 195 maxYLimit = upperBoundaryMode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxY().floatValue(); 196 lastMinY = minYLimit; 197 lastMaxY = maxYLimit; 198 } 199 } 200 201 @Override 202 public synchronized void setDomainBoundaries(final Number lowerBoundary, final Number upperBoundary, final BoundaryMode mode) { 203 super.setDomainBoundaries(lowerBoundary, upperBoundary, mode); 204 if(mCalledBySelf) { 205 mCalledBySelf = false; 206 } else { 207 minXLimit = mode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinX().floatValue(); 208 maxXLimit = mode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxX().floatValue(); 209 lastMinX = minXLimit; 210 lastMaxX = maxXLimit; 211 } 212 } 213 214 @Override 215 public synchronized void setRangeBoundaries(final Number lowerBoundary, final Number upperBoundary, final BoundaryMode mode) { 216 super.setRangeBoundaries(lowerBoundary, upperBoundary, mode); 217 if(mCalledBySelf) { 218 mCalledBySelf = false; 219 } else { 220 minYLimit = mode == BoundaryMode.FIXED ? lowerBoundary.floatValue() : getCalculatedMinY().floatValue(); 221 maxYLimit = mode == BoundaryMode.FIXED ? upperBoundary.floatValue() : getCalculatedMaxY().floatValue(); 222 lastMinY = minYLimit; 223 lastMaxY = maxYLimit; 224 } 225 } 226 227 @Override 228 public boolean onTouch(final View view, final MotionEvent event) { 229 switch (event.getAction() & MotionEvent.ACTION_MASK) 230 { 231 case MotionEvent.ACTION_DOWN: // start gesture 232 firstFingerPos = new PointF(event.getX(), event.getY()); 233 mode = State.ONE_FINGER_DRAG; 234 break; 235 case MotionEvent.ACTION_POINTER_DOWN: // second finger 236 { 237 mDistX = getXDistance(event); 238 // the distance check is done to avoid false alarms 239 if(mDistX > MIN_DIST_2_FING || mDistX < -MIN_DIST_2_FING) { 240 mode = State.TWO_FINGERS_DRAG; 241 } 242 break; 243 } 244 case MotionEvent.ACTION_POINTER_UP: // end zoom 245 mode = State.NONE; 246 break; 247 case MotionEvent.ACTION_MOVE: 248 if(mode == State.ONE_FINGER_DRAG) { 249 pan(event); 250 } else if(mode == State.TWO_FINGERS_DRAG) { 251 zoom(event); 252 } 253 break; 254 } 255 return true; 256 } 257 258 private float getXDistance(final MotionEvent event) { 259 return event.getX(0) - event.getX(1); 260 } 261 262 private void pan(final MotionEvent motionEvent) { 263 final PointF oldFirstFinger = firstFingerPos; //save old position of finger 264 firstFingerPos = new PointF(motionEvent.getX(), motionEvent.getY()); //update finger position 265 PointF newX = new PointF(); 266 if(mZoomHorizontally) { 267 calculatePan(oldFirstFinger, newX, true); 268 mCalledBySelf = true; 269 super.setDomainBoundaries(newX.x, newX.y, BoundaryMode.FIXED); 270 lastMinX = newX.x; 271 lastMaxX = newX.y; 272 } 273 if(mZoomVertically) { 274 calculatePan(oldFirstFinger, newX, false); 275 mCalledBySelf = true; 276 super.setRangeBoundaries(newX.x, newX.y, BoundaryMode.FIXED); 277 lastMinY = newX.x; 278 lastMaxY = newX.y; 279 } 280 redraw(); 281 } 282 283 private void calculatePan(final PointF oldFirstFinger, PointF newX, final boolean horizontal) { 284 final float offset; 285 // multiply the absolute finger movement for a factor. 286 // the factor is dependent on the calculated min and max 287 if(horizontal) { 288 newX.x = getLastMinX(); 289 newX.y = getLastMaxX(); 290 offset = (oldFirstFinger.x - firstFingerPos.x) * ((newX.y - newX.x) / getWidth()); 291 } else { 292 newX.x = getLastMinY(); 293 newX.y = getLastMaxY(); 294 offset = -(oldFirstFinger.y - firstFingerPos.y) * ((newX.y - newX.x) / getHeight()); 295 } 296 // move the calculated offset 297 newX.x = newX.x + offset; 298 newX.y = newX.y + offset; 299 //get the distance between max and min 300 final float diff = newX.y - newX.x; 301 //check if we reached the limit of panning 302 if(horizontal) { 303 if(newX.x < getMinXLimit()) { 304 newX.x = getMinXLimit(); 305 newX.y = newX.x + diff; 306 } 307 if(newX.y > getMaxXLimit()) { 308 newX.y = getMaxXLimit(); 309 newX.x = newX.y - diff; 310 } 311 } else { 312 if(newX.x < getMinYLimit()) { 313 newX.x = getMinYLimit(); 314 newX.y = newX.x + diff; 315 } 316 if(newX.y > getMaxYLimit()) { 317 newX.y = getMaxYLimit(); 318 newX.x = newX.y - diff; 319 } 320 } 321 } 322 323 private void zoom(final MotionEvent motionEvent) { 324 final float oldDist = mDistX; 325 final float newDist = getXDistance(motionEvent); 326 // sign change! Fingers have crossed ;-) 327 if(oldDist > 0 && newDist < 0 || oldDist < 0 && newDist > 0) { 328 return; 329 } 330 mDistX = newDist; 331 float scale = (oldDist / mDistX); 332 // sanity check 333 if(Float.isInfinite(scale) || Float.isNaN(scale) || scale > -0.001 && scale < 0.001) { 334 return; 335 } 336 PointF newX = new PointF(); 337 if(mZoomHorizontally) { 338 calculateZoom(scale, newX, true); 339 mCalledBySelf = true; 340 super.setDomainBoundaries(newX.x, newX.y, BoundaryMode.FIXED); 341 lastMinX = newX.x; 342 lastMaxX = newX.y; 343 } 344 if(mZoomVertically) { 345 calculateZoom(scale, newX, false); 346 mCalledBySelf = true; 347 super.setRangeBoundaries(newX.x, newX.y, BoundaryMode.FIXED); 348 lastMinY = newX.x; 349 lastMaxY = newX.y; 350 } 351 redraw(); 352 } 353 354 private void calculateZoom(float scale, PointF newX, final boolean horizontal) { 355 final float calcMax; 356 final float span; 357 if(horizontal) { 358 calcMax = getLastMaxX(); 359 span = calcMax - getLastMinX(); 360 } else { 361 calcMax = getLastMaxY(); 362 span = calcMax - getLastMinY(); 363 } 364 final float midPoint = calcMax - (span / 2.0f); 365 final float offset = span * scale / 2.0f; 366 newX.x = midPoint - offset; 367 newX.y = midPoint + offset; 368 if(horizontal) { 369 if(newX.x < getMinXLimit()) { 370 newX.x = getMinXLimit(); 371 } 372 if(newX.y > getMaxXLimit()) { 373 newX.y = getMaxXLimit(); 374 } 375 } else { 376 if(newX.x < getMinYLimit()) { 377 newX.x = getMinYLimit(); 378 } 379 if(newX.y > getMaxYLimit()) { 380 newX.y = getMaxYLimit(); 381 } 382 } 383 } 384} 385