• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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