1 package com.github.mikephil.charting.listener; 2 3 import android.annotation.SuppressLint; 4 import android.graphics.Matrix; 5 import android.graphics.PointF; 6 import android.util.Log; 7 import android.view.MotionEvent; 8 import android.view.VelocityTracker; 9 import android.view.View; 10 import android.view.animation.AnimationUtils; 11 12 import com.github.mikephil.charting.charts.BarLineChartBase; 13 import com.github.mikephil.charting.charts.HorizontalBarChart; 14 import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData; 15 import com.github.mikephil.charting.data.Entry; 16 import com.github.mikephil.charting.highlight.Highlight; 17 import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; 18 import com.github.mikephil.charting.interfaces.datasets.IDataSet; 19 import com.github.mikephil.charting.utils.MPPointF; 20 import com.github.mikephil.charting.utils.Utils; 21 import com.github.mikephil.charting.utils.ViewPortHandler; 22 23 /** 24 * TouchListener for Bar-, Line-, Scatter- and CandleStickChart with handles all 25 * touch interaction. Longpress == Zoom out. Double-Tap == Zoom in. 26 * 27 * @author Philipp Jahoda 28 */ 29 public class BarLineChartTouchListener extends ChartTouchListener<BarLineChartBase<? extends BarLineScatterCandleBubbleData<? 30 extends IBarLineScatterCandleBubbleDataSet<? extends Entry>>>> { 31 32 /** 33 * the original touch-matrix from the chart 34 */ 35 private Matrix mMatrix = new Matrix(); 36 37 /** 38 * matrix for saving the original matrix state 39 */ 40 private Matrix mSavedMatrix = new Matrix(); 41 42 /** 43 * point where the touch action started 44 */ 45 private MPPointF mTouchStartPoint = MPPointF.getInstance(0,0); 46 47 /** 48 * center between two pointers (fingers on the display) 49 */ 50 private MPPointF mTouchPointCenter = MPPointF.getInstance(0,0); 51 52 private float mSavedXDist = 1f; 53 private float mSavedYDist = 1f; 54 private float mSavedDist = 1f; 55 56 private IDataSet mClosestDataSetToTouch; 57 58 /** 59 * used for tracking velocity of dragging 60 */ 61 private VelocityTracker mVelocityTracker; 62 63 private long mDecelerationLastTime = 0; 64 private MPPointF mDecelerationCurrentPoint = MPPointF.getInstance(0,0); 65 private MPPointF mDecelerationVelocity = MPPointF.getInstance(0,0); 66 67 /** 68 * the distance of movement that will be counted as a drag 69 */ 70 private float mDragTriggerDist; 71 72 /** 73 * the minimum distance between the pointers that will trigger a zoom gesture 74 */ 75 private float mMinScalePointerDistance; 76 77 /** 78 * Constructor with initialization parameters. 79 * 80 * @param chart instance of the chart 81 * @param touchMatrix the touch-matrix of the chart 82 * @param dragTriggerDistance the minimum movement distance that will be interpreted as a "drag" gesture in dp (3dp equals 83 * to about 9 pixels on a 5.5" FHD screen) 84 */ BarLineChartTouchListener(BarLineChartBase<? extends BarLineScatterCandleBubbleData<? extends IBarLineScatterCandleBubbleDataSet<? extends Entry>>> chart, Matrix touchMatrix, float dragTriggerDistance)85 public BarLineChartTouchListener(BarLineChartBase<? extends BarLineScatterCandleBubbleData<? extends 86 IBarLineScatterCandleBubbleDataSet<? extends Entry>>> chart, Matrix touchMatrix, float dragTriggerDistance) { 87 super(chart); 88 this.mMatrix = touchMatrix; 89 90 this.mDragTriggerDist = Utils.convertDpToPixel(dragTriggerDistance); 91 92 this.mMinScalePointerDistance = Utils.convertDpToPixel(3.5f); 93 } 94 95 @SuppressLint("ClickableViewAccessibility") 96 @Override onTouch(View v, MotionEvent event)97 public boolean onTouch(View v, MotionEvent event) { 98 99 if (mVelocityTracker == null) { 100 mVelocityTracker = VelocityTracker.obtain(); 101 } 102 mVelocityTracker.addMovement(event); 103 104 if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) { 105 if (mVelocityTracker != null) { 106 mVelocityTracker.recycle(); 107 mVelocityTracker = null; 108 } 109 } 110 111 if (mTouchMode == NONE) { 112 mGestureDetector.onTouchEvent(event); 113 } 114 115 if (!mChart.isDragEnabled() && (!mChart.isScaleXEnabled() && !mChart.isScaleYEnabled())) 116 return true; 117 118 // Handle touch events here... 119 switch (event.getAction() & MotionEvent.ACTION_MASK) { 120 121 case MotionEvent.ACTION_DOWN: 122 123 startAction(event); 124 125 stopDeceleration(); 126 127 saveTouchStart(event); 128 129 break; 130 131 case MotionEvent.ACTION_POINTER_DOWN: 132 133 if (event.getPointerCount() >= 2) { 134 135 mChart.disableScroll(); 136 137 saveTouchStart(event); 138 139 // get the distance between the pointers on the x-axis 140 mSavedXDist = getXDist(event); 141 142 // get the distance between the pointers on the y-axis 143 mSavedYDist = getYDist(event); 144 145 // get the total distance between the pointers 146 mSavedDist = spacing(event); 147 148 if (mSavedDist > 10f) { 149 150 if (mChart.isPinchZoomEnabled()) { 151 mTouchMode = PINCH_ZOOM; 152 } else { 153 if (mChart.isScaleXEnabled() != mChart.isScaleYEnabled()) { 154 mTouchMode = mChart.isScaleXEnabled() ? X_ZOOM : Y_ZOOM; 155 } else { 156 mTouchMode = mSavedXDist > mSavedYDist ? X_ZOOM : Y_ZOOM; 157 } 158 } 159 } 160 161 // determine the touch-pointer center 162 midPoint(mTouchPointCenter, event); 163 } 164 break; 165 166 case MotionEvent.ACTION_MOVE: 167 168 if (mTouchMode == DRAG) { 169 170 mChart.disableScroll(); 171 172 float x = mChart.isDragXEnabled() ? event.getX() - mTouchStartPoint.x : 0.f; 173 float y = mChart.isDragYEnabled() ? event.getY() - mTouchStartPoint.y : 0.f; 174 175 performDrag(event, x, y); 176 177 } else if (mTouchMode == X_ZOOM || mTouchMode == Y_ZOOM || mTouchMode == PINCH_ZOOM) { 178 179 mChart.disableScroll(); 180 181 if (mChart.isScaleXEnabled() || mChart.isScaleYEnabled()) 182 performZoom(event); 183 184 } else if (mTouchMode == NONE 185 && Math.abs(distance(event.getX(), mTouchStartPoint.x, event.getY(), 186 mTouchStartPoint.y)) > mDragTriggerDist) { 187 188 if (mChart.isDragEnabled()) { 189 190 boolean shouldPan = !mChart.isFullyZoomedOut() || 191 !mChart.hasNoDragOffset(); 192 193 if (shouldPan) { 194 195 float distanceX = Math.abs(event.getX() - mTouchStartPoint.x); 196 float distanceY = Math.abs(event.getY() - mTouchStartPoint.y); 197 198 // Disable dragging in a direction that's disallowed 199 if ((mChart.isDragXEnabled() || distanceY >= distanceX) && 200 (mChart.isDragYEnabled() || distanceY <= distanceX)) { 201 202 mLastGesture = ChartGesture.DRAG; 203 mTouchMode = DRAG; 204 } 205 206 } else { 207 208 if (mChart.isHighlightPerDragEnabled()) { 209 mLastGesture = ChartGesture.DRAG; 210 211 if (mChart.isHighlightPerDragEnabled()) 212 performHighlightDrag(event); 213 } 214 } 215 216 } 217 218 } 219 break; 220 221 case MotionEvent.ACTION_UP: 222 223 final VelocityTracker velocityTracker = mVelocityTracker; 224 final int pointerId = event.getPointerId(0); 225 velocityTracker.computeCurrentVelocity(1000, Utils.getMaximumFlingVelocity()); 226 final float velocityY = velocityTracker.getYVelocity(pointerId); 227 final float velocityX = velocityTracker.getXVelocity(pointerId); 228 229 if (Math.abs(velocityX) > Utils.getMinimumFlingVelocity() || 230 Math.abs(velocityY) > Utils.getMinimumFlingVelocity()) { 231 232 if (mTouchMode == DRAG && mChart.isDragDecelerationEnabled()) { 233 234 stopDeceleration(); 235 236 mDecelerationLastTime = AnimationUtils.currentAnimationTimeMillis(); 237 238 mDecelerationCurrentPoint.x = event.getX(); 239 mDecelerationCurrentPoint.y = event.getY(); 240 241 mDecelerationVelocity.x = velocityX; 242 mDecelerationVelocity.y = velocityY; 243 244 Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by 245 // Google 246 } 247 } 248 249 if (mTouchMode == X_ZOOM || 250 mTouchMode == Y_ZOOM || 251 mTouchMode == PINCH_ZOOM || 252 mTouchMode == POST_ZOOM) { 253 254 // Range might have changed, which means that Y-axis labels 255 // could have changed in size, affecting Y-axis size. 256 // So we need to recalculate offsets. 257 mChart.calculateOffsets(); 258 mChart.postInvalidate(); 259 } 260 261 mTouchMode = NONE; 262 mChart.enableScroll(); 263 264 if (mVelocityTracker != null) { 265 mVelocityTracker.recycle(); 266 mVelocityTracker = null; 267 } 268 269 endAction(event); 270 271 break; 272 case MotionEvent.ACTION_POINTER_UP: 273 Utils.velocityTrackerPointerUpCleanUpIfNecessary(event, mVelocityTracker); 274 275 mTouchMode = POST_ZOOM; 276 break; 277 278 case MotionEvent.ACTION_CANCEL: 279 280 mTouchMode = NONE; 281 endAction(event); 282 break; 283 } 284 285 // perform the transformation, update the chart 286 mMatrix = mChart.getViewPortHandler().refresh(mMatrix, mChart, true); 287 288 return true; // indicate event was handled 289 } 290 291 /** 292 * ################ ################ ################ ################ 293 */ 294 /** BELOW CODE PERFORMS THE ACTUAL TOUCH ACTIONS */ 295 296 /** 297 * Saves the current Matrix state and the touch-start point. 298 * 299 * @param event 300 */ saveTouchStart(MotionEvent event)301 private void saveTouchStart(MotionEvent event) { 302 303 mSavedMatrix.set(mMatrix); 304 mTouchStartPoint.x = event.getX(); 305 mTouchStartPoint.y = event.getY(); 306 307 mClosestDataSetToTouch = mChart.getDataSetByTouchPoint(event.getX(), event.getY()); 308 } 309 310 /** 311 * Performs all necessary operations needed for dragging. 312 * 313 * @param event 314 */ performDrag(MotionEvent event, float distanceX, float distanceY)315 private void performDrag(MotionEvent event, float distanceX, float distanceY) { 316 317 mLastGesture = ChartGesture.DRAG; 318 319 mMatrix.set(mSavedMatrix); 320 321 OnChartGestureListener l = mChart.getOnChartGestureListener(); 322 323 // check if axis is inverted 324 if (inverted()) { 325 326 // if there is an inverted horizontalbarchart 327 if (mChart instanceof HorizontalBarChart) { 328 distanceX = -distanceX; 329 } else { 330 distanceY = -distanceY; 331 } 332 } 333 334 mMatrix.postTranslate(distanceX, distanceY); 335 336 if (l != null) 337 l.onChartTranslate(event, distanceX, distanceY); 338 } 339 340 /** 341 * Performs the all operations necessary for pinch and axis zoom. 342 * 343 * @param event 344 */ performZoom(MotionEvent event)345 private void performZoom(MotionEvent event) { 346 347 if (event.getPointerCount() >= 2) { // two finger zoom 348 349 OnChartGestureListener l = mChart.getOnChartGestureListener(); 350 351 // get the distance between the pointers of the touch event 352 float totalDist = spacing(event); 353 354 if (totalDist > mMinScalePointerDistance) { 355 356 // get the translation 357 MPPointF t = getTrans(mTouchPointCenter.x, mTouchPointCenter.y); 358 ViewPortHandler h = mChart.getViewPortHandler(); 359 360 // take actions depending on the activated touch mode 361 if (mTouchMode == PINCH_ZOOM) { 362 363 mLastGesture = ChartGesture.PINCH_ZOOM; 364 365 float scale = totalDist / mSavedDist; // total scale 366 367 boolean isZoomingOut = (scale < 1); 368 369 boolean canZoomMoreX = isZoomingOut ? 370 h.canZoomOutMoreX() : 371 h.canZoomInMoreX(); 372 373 boolean canZoomMoreY = isZoomingOut ? 374 h.canZoomOutMoreY() : 375 h.canZoomInMoreY(); 376 377 float scaleX = (mChart.isScaleXEnabled()) ? scale : 1f; 378 float scaleY = (mChart.isScaleYEnabled()) ? scale : 1f; 379 380 if (canZoomMoreY || canZoomMoreX) { 381 382 mMatrix.set(mSavedMatrix); 383 mMatrix.postScale(scaleX, scaleY, t.x, t.y); 384 385 if (l != null) 386 l.onChartScale(event, scaleX, scaleY); 387 } 388 389 } else if (mTouchMode == X_ZOOM && mChart.isScaleXEnabled()) { 390 391 mLastGesture = ChartGesture.X_ZOOM; 392 393 float xDist = getXDist(event); 394 float scaleX = xDist / mSavedXDist; // x-axis scale 395 396 boolean isZoomingOut = (scaleX < 1); 397 boolean canZoomMoreX = isZoomingOut ? 398 h.canZoomOutMoreX() : 399 h.canZoomInMoreX(); 400 401 if (canZoomMoreX) { 402 403 mMatrix.set(mSavedMatrix); 404 mMatrix.postScale(scaleX, 1f, t.x, t.y); 405 406 if (l != null) 407 l.onChartScale(event, scaleX, 1f); 408 } 409 410 } else if (mTouchMode == Y_ZOOM && mChart.isScaleYEnabled()) { 411 412 mLastGesture = ChartGesture.Y_ZOOM; 413 414 float yDist = getYDist(event); 415 float scaleY = yDist / mSavedYDist; // y-axis scale 416 417 boolean isZoomingOut = (scaleY < 1); 418 boolean canZoomMoreY = isZoomingOut ? 419 h.canZoomOutMoreY() : 420 h.canZoomInMoreY(); 421 422 if (canZoomMoreY) { 423 424 mMatrix.set(mSavedMatrix); 425 mMatrix.postScale(1f, scaleY, t.x, t.y); 426 427 if (l != null) 428 l.onChartScale(event, 1f, scaleY); 429 } 430 } 431 432 MPPointF.recycleInstance(t); 433 } 434 } 435 } 436 437 /** 438 * Highlights upon dragging, generates callbacks for the selection-listener. 439 * 440 * @param e 441 */ performHighlightDrag(MotionEvent e)442 private void performHighlightDrag(MotionEvent e) { 443 444 Highlight h = mChart.getHighlightByTouchPoint(e.getX(), e.getY()); 445 446 if (h != null && !h.equalTo(mLastHighlighted)) { 447 mLastHighlighted = h; 448 mChart.highlightValue(h, true); 449 } 450 } 451 452 /** 453 * ################ ################ ################ ################ 454 */ 455 /** DOING THE MATH BELOW ;-) */ 456 457 458 /** 459 * Determines the center point between two pointer touch points. 460 * 461 * @param point 462 * @param event 463 */ midPoint(MPPointF point, MotionEvent event)464 private static void midPoint(MPPointF point, MotionEvent event) { 465 float x = event.getX(0) + event.getX(1); 466 float y = event.getY(0) + event.getY(1); 467 point.x = (x / 2f); 468 point.y = (y / 2f); 469 } 470 471 /** 472 * returns the distance between two pointer touch points 473 * 474 * @param event 475 * @return 476 */ spacing(MotionEvent event)477 private static float spacing(MotionEvent event) { 478 float x = event.getX(0) - event.getX(1); 479 float y = event.getY(0) - event.getY(1); 480 return (float) Math.sqrt(x * x + y * y); 481 } 482 483 /** 484 * calculates the distance on the x-axis between two pointers (fingers on 485 * the display) 486 * 487 * @param e 488 * @return 489 */ getXDist(MotionEvent e)490 private static float getXDist(MotionEvent e) { 491 float x = Math.abs(e.getX(0) - e.getX(1)); 492 return x; 493 } 494 495 /** 496 * calculates the distance on the y-axis between two pointers (fingers on 497 * the display) 498 * 499 * @param e 500 * @return 501 */ getYDist(MotionEvent e)502 private static float getYDist(MotionEvent e) { 503 float y = Math.abs(e.getY(0) - e.getY(1)); 504 return y; 505 } 506 507 /** 508 * Returns a recyclable MPPointF instance. 509 * returns the correct translation depending on the provided x and y touch 510 * points 511 * 512 * @param x 513 * @param y 514 * @return 515 */ getTrans(float x, float y)516 public MPPointF getTrans(float x, float y) { 517 518 ViewPortHandler vph = mChart.getViewPortHandler(); 519 520 float xTrans = x - vph.offsetLeft(); 521 float yTrans = 0f; 522 523 // check if axis is inverted 524 if (inverted()) { 525 yTrans = -(y - vph.offsetTop()); 526 } else { 527 yTrans = -(mChart.getMeasuredHeight() - y - vph.offsetBottom()); 528 } 529 530 return MPPointF.getInstance(xTrans, yTrans); 531 } 532 533 /** 534 * Returns true if the current touch situation should be interpreted as inverted, false if not. 535 * 536 * @return 537 */ inverted()538 private boolean inverted() { 539 return (mClosestDataSetToTouch == null && mChart.isAnyAxisInverted()) || (mClosestDataSetToTouch != null 540 && mChart.isInverted(mClosestDataSetToTouch.getAxisDependency())); 541 } 542 543 /** 544 * ################ ################ ################ ################ 545 */ 546 /** GETTERS AND GESTURE RECOGNITION BELOW */ 547 548 /** 549 * returns the matrix object the listener holds 550 * 551 * @return 552 */ getMatrix()553 public Matrix getMatrix() { 554 return mMatrix; 555 } 556 557 /** 558 * Sets the minimum distance that will be interpreted as a "drag" by the chart in dp. 559 * Default: 3dp 560 * 561 * @param dragTriggerDistance 562 */ setDragTriggerDist(float dragTriggerDistance)563 public void setDragTriggerDist(float dragTriggerDistance) { 564 this.mDragTriggerDist = Utils.convertDpToPixel(dragTriggerDistance); 565 } 566 567 @Override onDoubleTap(MotionEvent e)568 public boolean onDoubleTap(MotionEvent e) { 569 570 mLastGesture = ChartGesture.DOUBLE_TAP; 571 572 OnChartGestureListener l = mChart.getOnChartGestureListener(); 573 574 if (l != null) { 575 l.onChartDoubleTapped(e); 576 } 577 578 // check if double-tap zooming is enabled 579 if (mChart.isDoubleTapToZoomEnabled() && mChart.getData().getEntryCount() > 0) { 580 581 MPPointF trans = getTrans(e.getX(), e.getY()); 582 583 float scaleX = mChart.isScaleXEnabled() ? 1.4f : 1f; 584 float scaleY = mChart.isScaleYEnabled() ? 1.4f : 1f; 585 586 mChart.zoom(scaleX, scaleY, trans.x, trans.y); 587 588 if (mChart.isLogEnabled()) 589 Log.i("BarlineChartTouch", "Double-Tap, Zooming In, x: " + trans.x + ", y: " 590 + trans.y); 591 592 if (l != null) { 593 l.onChartScale(e, scaleX, scaleY); 594 } 595 596 MPPointF.recycleInstance(trans); 597 } 598 599 return super.onDoubleTap(e); 600 } 601 602 @Override onLongPress(MotionEvent e)603 public void onLongPress(MotionEvent e) { 604 605 mLastGesture = ChartGesture.LONG_PRESS; 606 607 OnChartGestureListener l = mChart.getOnChartGestureListener(); 608 609 if (l != null) { 610 611 l.onChartLongPressed(e); 612 } 613 } 614 615 @Override onSingleTapUp(MotionEvent e)616 public boolean onSingleTapUp(MotionEvent e) { 617 618 mLastGesture = ChartGesture.SINGLE_TAP; 619 620 OnChartGestureListener l = mChart.getOnChartGestureListener(); 621 622 if (l != null) { 623 l.onChartSingleTapped(e); 624 } 625 626 if (!mChart.isHighlightPerTapEnabled()) { 627 return false; 628 } 629 630 Highlight h = mChart.getHighlightByTouchPoint(e.getX(), e.getY()); 631 performHighlight(h, e); 632 633 return super.onSingleTapUp(e); 634 } 635 636 @Override onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)637 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 638 639 mLastGesture = ChartGesture.FLING; 640 641 OnChartGestureListener l = mChart.getOnChartGestureListener(); 642 643 if (l != null) { 644 l.onChartFling(e1, e2, velocityX, velocityY); 645 } 646 647 return super.onFling(e1, e2, velocityX, velocityY); 648 } 649 stopDeceleration()650 public void stopDeceleration() { 651 mDecelerationVelocity.x = 0; 652 mDecelerationVelocity.y = 0; 653 } 654 computeScroll()655 public void computeScroll() { 656 657 if (mDecelerationVelocity.x == 0.f && mDecelerationVelocity.y == 0.f) 658 return; // There's no deceleration in progress 659 660 final long currentTime = AnimationUtils.currentAnimationTimeMillis(); 661 662 mDecelerationVelocity.x *= mChart.getDragDecelerationFrictionCoef(); 663 mDecelerationVelocity.y *= mChart.getDragDecelerationFrictionCoef(); 664 665 final float timeInterval = (float) (currentTime - mDecelerationLastTime) / 1000.f; 666 667 float distanceX = mDecelerationVelocity.x * timeInterval; 668 float distanceY = mDecelerationVelocity.y * timeInterval; 669 670 mDecelerationCurrentPoint.x += distanceX; 671 mDecelerationCurrentPoint.y += distanceY; 672 673 MotionEvent event = MotionEvent.obtain(currentTime, currentTime, MotionEvent.ACTION_MOVE, mDecelerationCurrentPoint.x, 674 mDecelerationCurrentPoint.y, 0); 675 676 float dragDistanceX = mChart.isDragXEnabled() ? mDecelerationCurrentPoint.x - mTouchStartPoint.x : 0.f; 677 float dragDistanceY = mChart.isDragYEnabled() ? mDecelerationCurrentPoint.y - mTouchStartPoint.y : 0.f; 678 679 performDrag(event, dragDistanceX, dragDistanceY); 680 681 event.recycle(); 682 mMatrix = mChart.getViewPortHandler().refresh(mMatrix, mChart, false); 683 684 mDecelerationLastTime = currentTime; 685 686 if (Math.abs(mDecelerationVelocity.x) >= 0.01 || Math.abs(mDecelerationVelocity.y) >= 0.01) 687 Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by Google 688 else { 689 // Range might have changed, which means that Y-axis labels 690 // could have changed in size, affecting Y-axis size. 691 // So we need to recalculate offsets. 692 mChart.calculateOffsets(); 693 mChart.postInvalidate(); 694 695 stopDeceleration(); 696 } 697 } 698 } 699