package com.github.mikephil.charting.listener; import android.annotation.SuppressLint; import android.graphics.PointF; import android.view.MotionEvent; import android.view.View; import android.view.animation.AnimationUtils; import com.github.mikephil.charting.charts.PieChart; import com.github.mikephil.charting.charts.PieRadarChartBase; import com.github.mikephil.charting.charts.RadarChart; import com.github.mikephil.charting.highlight.Highlight; import com.github.mikephil.charting.utils.SelectionDetail; import com.github.mikephil.charting.utils.Utils; import java.util.ArrayList; import java.util.List; /** * Touchlistener for the PieChart. * * @author Philipp Jahoda */ public class PieRadarChartTouchListener extends ChartTouchListener<PieRadarChartBase<?>> { private PointF mTouchStartPoint = new PointF(); /** * the angle where the dragging started */ private float mStartAngle = 0f; private ArrayList<AngularVelocitySample> _velocitySamples = new ArrayList<AngularVelocitySample>(); private long mDecelerationLastTime = 0; private float mDecelerationAngularVelocity = 0.f; public PieRadarChartTouchListener(PieRadarChartBase<?> chart) { super(chart); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent event) { if (mGestureDetector.onTouchEvent(event)) return true; // if rotation by touch is enabled if (mChart.isRotationEnabled()) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startAction(event); stopDeceleration(); resetVelocity(); if (mChart.isDragDecelerationEnabled()) sampleVelocity(x, y); setGestureStartAngle(x, y); mTouchStartPoint.x = x; mTouchStartPoint.y = y; break; case MotionEvent.ACTION_MOVE: if (mChart.isDragDecelerationEnabled()) sampleVelocity(x, y); if (mTouchMode == NONE && distance(x, mTouchStartPoint.x, y, mTouchStartPoint.y) > Utils.convertDpToPixel(8f)) { mLastGesture = ChartGesture.ROTATE; mTouchMode = ROTATE; mChart.disableScroll(); } else if (mTouchMode == ROTATE) { updateGestureRotation(x, y); mChart.invalidate(); } endAction(event); break; case MotionEvent.ACTION_UP: if (mChart.isDragDecelerationEnabled()) { stopDeceleration(); sampleVelocity(x, y); mDecelerationAngularVelocity = calculateVelocity(); if (mDecelerationAngularVelocity != 0.f) { mDecelerationLastTime = AnimationUtils.currentAnimationTimeMillis(); Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by Google } } mChart.enableScroll(); mTouchMode = NONE; endAction(event); break; } } return true; } @Override public void onLongPress(MotionEvent me) { mLastGesture = ChartGesture.LONG_PRESS; OnChartGestureListener l = mChart.getOnChartGestureListener(); if (l != null) { l.onChartLongPressed(me); } } @Override public boolean onSingleTapConfirmed(MotionEvent e) { return true; } @Override public boolean onSingleTapUp(MotionEvent e) { mLastGesture = ChartGesture.SINGLE_TAP; OnChartGestureListener l = mChart.getOnChartGestureListener(); if (l != null) { l.onChartSingleTapped(e); } float distance = mChart.distanceToCenter(e.getX(), e.getY()); // check if a slice was touched if (distance > mChart.getRadius()) { // if no slice was touched, highlight nothing if (mLastHighlighted == null) mChart.highlightValues(null); // no listener callback else mChart.highlightTouch(null); // listener callback mLastHighlighted = null; } else { float angle = mChart.getAngleForPoint(e.getX(), e.getY()); if (mChart instanceof PieChart) { angle /= mChart.getAnimator().getPhaseY(); } int index = mChart.getIndexForAngle(angle); // check if the index could be found if (index < 0) { mChart.highlightValues(null); mLastHighlighted = null; } else { List<SelectionDetail> valsAtIndex = mChart.getSelectionDetailsAtIndex(index); int dataSetIndex = 0; // get the dataset that is closest to the selection (PieChart // only // has one DataSet) if (mChart instanceof RadarChart) { dataSetIndex = Utils.getClosestDataSetIndex(valsAtIndex, distance / ((RadarChart) mChart).getFactor(), null); } if (dataSetIndex < 0) { mChart.highlightValues(null); mLastHighlighted = null; } else { Highlight h = new Highlight(index, dataSetIndex); if (h.equalTo(mLastHighlighted)) { mChart.highlightTouch(null); mLastHighlighted = null; } else { mChart.highlightTouch(h); mLastHighlighted = h; } } } } return true; } private void resetVelocity() { _velocitySamples.clear(); } private void sampleVelocity(float touchLocationX, float touchLocationY) { long currentTime = AnimationUtils.currentAnimationTimeMillis(); _velocitySamples.add(new AngularVelocitySample(currentTime, mChart.getAngleForPoint(touchLocationX, touchLocationY))); // Remove samples older than our sample time - 1 seconds for (int i = 0, count = _velocitySamples.size(); i < count - 2; i++) { if (currentTime - _velocitySamples.get(i).time > 1000) { _velocitySamples.remove(0); i--; count--; } else { break; } } } private float calculateVelocity() { if (_velocitySamples.isEmpty()) return 0.f; AngularVelocitySample firstSample = _velocitySamples.get(0); AngularVelocitySample lastSample = _velocitySamples.get(_velocitySamples.size() - 1); // Look for a sample that's closest to the latest sample, but not the same, so we can deduce the direction AngularVelocitySample beforeLastSample = firstSample; for (int i = _velocitySamples.size() - 1; i >= 0; i--) { beforeLastSample = _velocitySamples.get(i); if (beforeLastSample.angle != lastSample.angle) { break; } } // Calculate the sampling time float timeDelta = (lastSample.time - firstSample.time) / 1000.f; if (timeDelta == 0.f) { timeDelta = 0.1f; } // Calculate clockwise/ccw by choosing two values that should be closest to each other, // so if the angles are two far from each other we know they are inverted "for sure" boolean clockwise = lastSample.angle >= beforeLastSample.angle; if (Math.abs(lastSample.angle - beforeLastSample.angle) > 270.0) { clockwise = !clockwise; } // Now if the "gesture" is over a too big of an angle - then we know the angles are inverted, and we need to move them closer to each other from both sides of the 360.0 wrapping point if (lastSample.angle - firstSample.angle > 180.0) { firstSample.angle += 360.0; } else if (firstSample.angle - lastSample.angle > 180.0) { lastSample.angle += 360.0; } // The velocity float velocity = Math.abs((lastSample.angle - firstSample.angle) / timeDelta); // Direction? if (!clockwise) { velocity = -velocity; } return velocity; } /** * sets the starting angle of the rotation, this is only used by the touch * listener, x and y is the touch position * * @param x * @param y */ public void setGestureStartAngle(float x, float y) { mStartAngle = mChart.getAngleForPoint(x, y) - mChart.getRawRotationAngle(); } /** * updates the view rotation depending on the given touch position, also * takes the starting angle into consideration * * @param x * @param y */ public void updateGestureRotation(float x, float y) { mChart.setRotationAngle(mChart.getAngleForPoint(x, y) - mStartAngle); } /** * Sets the deceleration-angular-velocity to 0f */ public void stopDeceleration() { mDecelerationAngularVelocity = 0.f; } public void computeScroll() { if (mDecelerationAngularVelocity == 0.f) return; // There's no deceleration in progress final long currentTime = AnimationUtils.currentAnimationTimeMillis(); mDecelerationAngularVelocity *= mChart.getDragDecelerationFrictionCoef(); final float timeInterval = (float) (currentTime - mDecelerationLastTime) / 1000.f; mChart.setRotationAngle(mChart.getRotationAngle() + mDecelerationAngularVelocity * timeInterval); mDecelerationLastTime = currentTime; if (Math.abs(mDecelerationAngularVelocity) >= 0.001) Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by Google else stopDeceleration(); } private class AngularVelocitySample { public long time; public float angle; public AngularVelocitySample(long time, float angle) { this.time = time; this.angle = angle; } } }