package com.github.mikephil.charting.listener; import android.annotation.SuppressLint; import android.graphics.Matrix; import android.graphics.PointF; import android.util.Log; import android.view.GestureDetector; import android.view.GestureDetector.SimpleOnGestureListener; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import com.github.mikephil.charting.charts.BarLineChartBase; import com.github.mikephil.charting.charts.HorizontalBarChart; import com.github.mikephil.charting.data.BarLineScatterCandleData; import com.github.mikephil.charting.data.BarLineScatterCandleDataSet; import com.github.mikephil.charting.data.DataSet; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.utils.Highlight; import com.github.mikephil.charting.utils.ViewPortHandler; /** * TouchListener for Bar-, Line-, Scatter- and CandleStickChart with handles all * touch interaction. Longpress == Zoom out. Double-Tap == Zoom in. * * @author Philipp Jahoda */ public class BarLineChartTouchListener<T extends BarLineChartBase<? extends BarLineScatterCandleData<? extends BarLineScatterCandleDataSet<? extends Entry>>>> extends SimpleOnGestureListener implements OnTouchListener { // private static final long REFRESH_MILLIS = 20; /** the original touch-matrix from the chart */ private Matrix mMatrix = new Matrix(); /** matrix for saving the original matrix state */ private Matrix mSavedMatrix = new Matrix(); /** point where the touch action started */ private PointF mTouchStartPoint = new PointF(); /** center between two pointers (fingers on the display) */ private PointF mTouchPointCenter = new PointF(); // states private static final int NONE = 0; private static final int DRAG = 1; private static final int X_ZOOM = 2; private static final int Y_ZOOM = 3; private static final int PINCH_ZOOM = 4; private static final int POST_ZOOM = 5; /** integer field that holds the current touch-state */ private int mTouchMode = NONE; private float mSavedXDist = 1f; private float mSavedYDist = 1f; private float mSavedDist = 1f; /** the last highlighted object */ private Highlight mLastHighlighted; private DataSet<?> mClosestDataSetToTouch; /** the chart the listener represents */ private T mChart; /** the gesturedetector used for detecting taps and longpresses, ... */ private GestureDetector mGestureDetector; public BarLineChartTouchListener(T chart, Matrix touchMatrix) { this.mChart = chart; this.mMatrix = touchMatrix; mGestureDetector = new GestureDetector(chart.getContext(), this); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { if (mTouchMode == NONE) { mGestureDetector.onTouchEvent(event); } if (!mChart.isDragEnabled() && (!mChart.isScaleXEnabled() && !mChart.isScaleYEnabled())) return true; // Handle touch events here... switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: saveTouchStart(event); break; case MotionEvent.ACTION_POINTER_DOWN: if (event.getPointerCount() >= 2) { mChart.disableScroll(); saveTouchStart(event); // get the distance between the pointers on the x-axis mSavedXDist = getXDist(event); // get the distance between the pointers on the y-axis mSavedYDist = getYDist(event); // get the total distance between the pointers mSavedDist = spacing(event); if (mSavedDist > 10f) { if (mChart.isPinchZoomEnabled()) { mTouchMode = PINCH_ZOOM; } else { if (mSavedXDist > mSavedYDist) mTouchMode = X_ZOOM; else mTouchMode = Y_ZOOM; } } // determine the touch-pointer center midPoint(mTouchPointCenter, event); } break; case MotionEvent.ACTION_MOVE: if (mTouchMode == DRAG) { mChart.disableScroll(); if (mChart.isDragEnabled()) performDrag(event); } else if (mTouchMode == X_ZOOM || mTouchMode == Y_ZOOM || mTouchMode == PINCH_ZOOM) { mChart.disableScroll(); if (mChart.isScaleXEnabled() || mChart.isScaleYEnabled()) performZoom(event); } else if (mTouchMode == NONE && Math.abs(distance(event.getX(), mTouchStartPoint.x, event.getY(), mTouchStartPoint.y)) > 5f) { if (mChart.hasNoDragOffset()) { if (!mChart.isFullyZoomedOut()) mTouchMode = DRAG; } else { mTouchMode = DRAG; } } break; case MotionEvent.ACTION_UP: mTouchMode = NONE; mChart.enableScroll(); break; case MotionEvent.ACTION_POINTER_UP: mTouchMode = POST_ZOOM; break; } // Perform the transformation, update the chart // if (needsRefresh()) mMatrix = mChart.getViewPortHandler().refresh(mMatrix, mChart, true); return true; // indicate event was handled } /** * ################ ################ ################ ################ */ /** BELOW CODE PERFORMS THE ACTUAL TOUCH ACTIONS */ /** * Saves the current Matrix state and the touch-start point. * * @param event */ private void saveTouchStart(MotionEvent event) { mSavedMatrix.set(mMatrix); mTouchStartPoint.set(event.getX(), event.getY()); mClosestDataSetToTouch = mChart.getDataSetByTouchPoint(event.getX(), event.getY()); } /** * Performs all necessary operations needed for dragging. * * @param event */ private void performDrag(MotionEvent event) { mMatrix.set(mSavedMatrix); OnChartGestureListener l = mChart.getOnChartGestureListener(); float dX, dY; // check if axis is inverted if (mChart.isAnyAxisInverted() && mClosestDataSetToTouch != null && mChart.getAxis(mClosestDataSetToTouch.getAxisDependency()).isInverted()) { // if there is an inverted horizontalbarchart if (mChart instanceof HorizontalBarChart) { dX = -(event.getX() - mTouchStartPoint.x); dY = event.getY() - mTouchStartPoint.y; } else { dX = event.getX() - mTouchStartPoint.x; dY = -(event.getY() - mTouchStartPoint.y); } } else { dX = event.getX() - mTouchStartPoint.x; dY = event.getY() - mTouchStartPoint.y; } mMatrix.postTranslate(dX, dY); if (l != null) l.onChartTranslate(event, dX, dY); } /** * Performs the all operations necessary for pinch and axis zoom. * * @param event */ private void performZoom(MotionEvent event) { if (event.getPointerCount() >= 2) { OnChartGestureListener l = mChart.getOnChartGestureListener(); // get the distance between the pointers of the touch // event float totalDist = spacing(event); if (totalDist > 10f) { // get the translation PointF t = getTrans(mTouchPointCenter.x, mTouchPointCenter.y); // take actions depending on the activated touch // mode if (mTouchMode == PINCH_ZOOM) { float scale = totalDist / mSavedDist; // total // scale float scaleX = (mChart.isScaleXEnabled()) ? scale : 1f; float scaleY = (mChart.isScaleYEnabled()) ? scale : 1f; mMatrix.set(mSavedMatrix); mMatrix.postScale(scaleX, scaleY, t.x, t.y); if (l != null) l.onChartScale(event, scaleX, scaleY); } else if (mTouchMode == X_ZOOM && mChart.isScaleXEnabled()) { float xDist = getXDist(event); float scaleX = xDist / mSavedXDist; // x-axis // scale mMatrix.set(mSavedMatrix); mMatrix.postScale(scaleX, 1f, t.x, t.y); if (l != null) l.onChartScale(event, scaleX, 1f); } else if (mTouchMode == Y_ZOOM && mChart.isScaleYEnabled()) { float yDist = getYDist(event); float scaleY = yDist / mSavedYDist; // y-axis // scale mMatrix.set(mSavedMatrix); // y-axis comes from top to bottom, revert y mMatrix.postScale(1f, scaleY, t.x, t.y); if (l != null) l.onChartScale(event, 1f, scaleY); } } } } /** * Perform a highlight operation. * * @param e */ private void performHighlight(MotionEvent e) { Highlight h = mChart.getHighlightByTouchPoint(e.getX(), e.getY()); if (h == null || h.equalTo(mLastHighlighted)) { mChart.highlightTouch(null); mLastHighlighted = null; } else { mLastHighlighted = h; mChart.highlightTouch(h); } } /** * Highlights upon dragging. * * @param e */ private void performHighlightDrag(MotionEvent e) { Highlight h = mChart.getHighlightByTouchPoint(e.getX(), e.getY()); if (h != null && !h.equalTo(mLastHighlighted)) { mLastHighlighted = h; mChart.highlightTouch(h); } } /** * ################ ################ ################ ################ */ /** DOING THE MATH BELOW ;-) */ /** * returns the distance between two points * * @param eventX * @param startX * @param eventY * @param startY * @return */ private static float distance(float eventX, float startX, float eventY, float startY) { float dx = eventX - startX; float dy = eventY - startY; return (float) Math.sqrt(dx * dx + dy * dy); } /** * Determines the center point between two pointer touch points. * * @param point * @param event */ private static void midPoint(PointF point, MotionEvent event) { float x = event.getX(0) + event.getX(1); float y = event.getY(0) + event.getY(1); point.set(x / 2f, y / 2f); } /** * returns the distance between two pointer touch points * * @param event * @return */ private static float spacing(MotionEvent event) { float x = event.getX(0) - event.getX(1); float y = event.getY(0) - event.getY(1); return (float) Math.sqrt(x * x + y * y); } /** * calculates the distance on the x-axis between two pointers (fingers on * the display) * * @param e * @return */ private static float getXDist(MotionEvent e) { float x = Math.abs(e.getX(0) - e.getX(1)); return x; } /** * calculates the distance on the y-axis between two pointers (fingers on * the display) * * @param e * @return */ private static float getYDist(MotionEvent e) { float y = Math.abs(e.getY(0) - e.getY(1)); return y; } /** * returns the correct translation depending on the provided x and y touch * points * * @param e * @return */ public PointF getTrans(float x, float y) { ViewPortHandler vph = mChart.getViewPortHandler(); float xTrans = x - vph.offsetLeft(); float yTrans = 0f; // check if axis is inverted if (mChart.isAnyAxisInverted() && mClosestDataSetToTouch != null && mChart.isInverted(mClosestDataSetToTouch.getAxisDependency())) { yTrans = -(y - vph.offsetTop()); } else { yTrans = -(mChart.getMeasuredHeight() - y - vph.offsetBottom()); } return new PointF(xTrans, yTrans); } /** * ################ ################ ################ ################ */ /** GETTERS AND GESTURE RECOGNITION BELOW */ /** * returns the matrix object the listener holds * * @return */ public Matrix getMatrix() { return mMatrix; } /** * returns the touch mode the listener is currently in * * @return */ public int getTouchMode() { return mTouchMode; } @Override public boolean onDoubleTap(MotionEvent e) { OnChartGestureListener l = mChart.getOnChartGestureListener(); if (l != null) { l.onChartDoubleTapped(e); return super.onDoubleTap(e); } // check if double-tap zooming is enabled if (mChart.isDoubleTapToZoomEnabled()) { PointF trans = getTrans(e.getX(), e.getY()); mChart.zoom(1.4f, 1.4f, trans.x, trans.y); if (mChart.isLogEnabled()) Log.i("BarlineChartTouch", "Double-Tap, Zooming In, x: " + trans.x + ", y: " + trans.y); } return super.onDoubleTap(e); } @Override public void onLongPress(MotionEvent e) { OnChartGestureListener l = mChart.getOnChartGestureListener(); if (l != null) { l.onChartLongPressed(e); } } @Override public boolean onSingleTapUp(MotionEvent e) { performHighlight(e); return super.onSingleTapUp(e); } @Override public boolean onSingleTapConfirmed(MotionEvent e) { OnChartGestureListener l = mChart.getOnChartGestureListener(); if (l != null) { l.onChartSingleTapped(e); } return super.onSingleTapConfirmed(e); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { OnChartGestureListener l = mChart.getOnChartGestureListener(); if (l != null) l.onChartFling(e1, e2, velocityX, velocityY); return super.onFling(e1, e2, velocityX, velocityY); } }