| |
| package com.github.mikephil.charting.renderer; |
| |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.Paint; |
| import android.graphics.Paint.Align; |
| import android.graphics.Paint.Style; |
| import android.graphics.Path; |
| import android.graphics.RectF; |
| import android.os.Build; |
| import android.text.Layout; |
| import android.text.StaticLayout; |
| import android.text.TextPaint; |
| |
| import com.github.mikephil.charting.animation.ChartAnimator; |
| import com.github.mikephil.charting.charts.LineChart; |
| import com.github.mikephil.charting.charts.PieChart; |
| import com.github.mikephil.charting.data.Entry; |
| import com.github.mikephil.charting.data.PieData; |
| import com.github.mikephil.charting.data.PieDataSet; |
| import com.github.mikephil.charting.data.PieEntry; |
| import com.github.mikephil.charting.formatter.IValueFormatter; |
| import com.github.mikephil.charting.highlight.Highlight; |
| import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; |
| import com.github.mikephil.charting.utils.ColorTemplate; |
| import com.github.mikephil.charting.utils.MPPointF; |
| import com.github.mikephil.charting.utils.Utils; |
| import com.github.mikephil.charting.utils.ViewPortHandler; |
| |
| import java.lang.ref.WeakReference; |
| import java.util.List; |
| |
| public class PieChartRenderer extends DataRenderer { |
| |
| protected PieChart mChart; |
| |
| /** |
| * paint for the hole in the center of the pie chart and the transparent |
| * circle |
| */ |
| protected Paint mHolePaint; |
| protected Paint mTransparentCirclePaint; |
| protected Paint mValueLinePaint; |
| |
| /** |
| * paint object for the text that can be displayed in the center of the |
| * chart |
| */ |
| private TextPaint mCenterTextPaint; |
| |
| /** |
| * paint object used for drwing the slice-text |
| */ |
| private Paint mEntryLabelsPaint; |
| |
| private StaticLayout mCenterTextLayout; |
| private CharSequence mCenterTextLastValue; |
| private RectF mCenterTextLastBounds = new RectF(); |
| private RectF[] mRectBuffer = {new RectF(), new RectF(), new RectF()}; |
| |
| /** |
| * Bitmap for drawing the center hole |
| */ |
| protected WeakReference<Bitmap> mDrawBitmap; |
| |
| protected Canvas mBitmapCanvas; |
| |
| public PieChartRenderer(PieChart chart, ChartAnimator animator, |
| ViewPortHandler viewPortHandler) { |
| super(animator, viewPortHandler); |
| mChart = chart; |
| |
| mHolePaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mHolePaint.setColor(Color.WHITE); |
| mHolePaint.setStyle(Style.FILL); |
| |
| mTransparentCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mTransparentCirclePaint.setColor(Color.WHITE); |
| mTransparentCirclePaint.setStyle(Style.FILL); |
| mTransparentCirclePaint.setAlpha(105); |
| |
| mCenterTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); |
| mCenterTextPaint.setColor(Color.BLACK); |
| mCenterTextPaint.setTextSize(Utils.convertDpToPixel(12f)); |
| |
| mValuePaint.setTextSize(Utils.convertDpToPixel(13f)); |
| mValuePaint.setColor(Color.WHITE); |
| mValuePaint.setTextAlign(Align.CENTER); |
| |
| mEntryLabelsPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mEntryLabelsPaint.setColor(Color.WHITE); |
| mEntryLabelsPaint.setTextAlign(Align.CENTER); |
| mEntryLabelsPaint.setTextSize(Utils.convertDpToPixel(13f)); |
| |
| mValueLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| mValueLinePaint.setStyle(Style.STROKE); |
| } |
| |
| public Paint getPaintHole() { |
| return mHolePaint; |
| } |
| |
| public Paint getPaintTransparentCircle() { |
| return mTransparentCirclePaint; |
| } |
| |
| public TextPaint getPaintCenterText() { |
| return mCenterTextPaint; |
| } |
| |
| public Paint getPaintEntryLabels() { |
| return mEntryLabelsPaint; |
| } |
| |
| @Override |
| public void initBuffers() { |
| // TODO Auto-generated method stub |
| } |
| |
| @Override |
| public void drawData(Canvas c) { |
| |
| int width = (int) mViewPortHandler.getChartWidth(); |
| int height = (int) mViewPortHandler.getChartHeight(); |
| |
| if (mDrawBitmap == null |
| || (mDrawBitmap.get().getWidth() != width) |
| || (mDrawBitmap.get().getHeight() != height)) { |
| |
| if (width > 0 && height > 0) { |
| |
| mDrawBitmap = new WeakReference<Bitmap>(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444)); |
| mBitmapCanvas = new Canvas(mDrawBitmap.get()); |
| } else |
| return; |
| } |
| |
| mDrawBitmap.get().eraseColor(Color.TRANSPARENT); |
| |
| PieData pieData = mChart.getData(); |
| |
| for (IPieDataSet set : pieData.getDataSets()) { |
| |
| if (set.isVisible() && set.getEntryCount() > 0) |
| drawDataSet(c, set); |
| } |
| } |
| |
| private Path mPathBuffer = new Path(); |
| private RectF mInnerRectBuffer = new RectF(); |
| |
| protected float calculateMinimumRadiusForSpacedSlice( |
| MPPointF center, |
| float radius, |
| float angle, |
| float arcStartPointX, |
| float arcStartPointY, |
| float startAngle, |
| float sweepAngle) { |
| final float angleMiddle = startAngle + sweepAngle / 2.f; |
| |
| // Other point of the arc |
| float arcEndPointX = center.x + radius * (float) Math.cos((startAngle + sweepAngle) * Utils.FDEG2RAD); |
| float arcEndPointY = center.y + radius * (float) Math.sin((startAngle + sweepAngle) * Utils.FDEG2RAD); |
| |
| // Middle point on the arc |
| float arcMidPointX = center.x + radius * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); |
| float arcMidPointY = center.y + radius * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); |
| |
| // This is the base of the contained triangle |
| double basePointsDistance = Math.sqrt( |
| Math.pow(arcEndPointX - arcStartPointX, 2) + |
| Math.pow(arcEndPointY - arcStartPointY, 2)); |
| |
| // After reducing space from both sides of the "slice", |
| // the angle of the contained triangle should stay the same. |
| // So let's find out the height of that triangle. |
| float containedTriangleHeight = (float) (basePointsDistance / 2.0 * |
| Math.tan((180.0 - angle) / 2.0 * Utils.DEG2RAD)); |
| |
| // Now we subtract that from the radius |
| float spacedRadius = radius - containedTriangleHeight; |
| |
| // And now subtract the height of the arc that's between the triangle and the outer circle |
| spacedRadius -= Math.sqrt( |
| Math.pow(arcMidPointX - (arcEndPointX + arcStartPointX) / 2.f, 2) + |
| Math.pow(arcMidPointY - (arcEndPointY + arcStartPointY) / 2.f, 2)); |
| |
| return spacedRadius; |
| } |
| |
| /** |
| * Calculates the sliceSpace to use based on visible values and their size compared to the set sliceSpace. |
| * |
| * @param dataSet |
| * @return |
| */ |
| protected float getSliceSpace(IPieDataSet dataSet) { |
| |
| if (!dataSet.isAutomaticallyDisableSliceSpacing()) |
| return dataSet.getSliceSpace(); |
| |
| float spaceSizeRatio = dataSet.getSliceSpace() / mViewPortHandler.getSmallestContentExtension(); |
| float minValueRatio = dataSet.getYMin() / mChart.getData().getYValueSum() * 2; |
| |
| float sliceSpace = spaceSizeRatio > minValueRatio ? 0f : dataSet.getSliceSpace(); |
| |
| return sliceSpace; |
| } |
| |
| protected void drawDataSet(Canvas c, IPieDataSet dataSet) { |
| |
| float angle = 0; |
| float rotationAngle = mChart.getRotationAngle(); |
| |
| float phaseX = mAnimator.getPhaseX(); |
| float phaseY = mAnimator.getPhaseY(); |
| |
| final RectF circleBox = mChart.getCircleBox(); |
| |
| final int entryCount = dataSet.getEntryCount(); |
| final float[] drawAngles = mChart.getDrawAngles(); |
| final MPPointF center = mChart.getCenterCircleBox(); |
| final float radius = mChart.getRadius(); |
| final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled(); |
| final float userInnerRadius = drawInnerArc |
| ? radius * (mChart.getHoleRadius() / 100.f) |
| : 0.f; |
| |
| int visibleAngleCount = 0; |
| for (int j = 0; j < entryCount; j++) { |
| // draw only if the value is greater than zero |
| if ((Math.abs(dataSet.getEntryForIndex(j).getY()) > Utils.FLOAT_EPSILON)) { |
| visibleAngleCount++; |
| } |
| } |
| |
| final float sliceSpace = visibleAngleCount <= 1 ? 0.f : getSliceSpace(dataSet); |
| |
| for (int j = 0; j < entryCount; j++) { |
| |
| float sliceAngle = drawAngles[j]; |
| float innerRadius = userInnerRadius; |
| |
| Entry e = dataSet.getEntryForIndex(j); |
| |
| // draw only if the value is greater than zero |
| if ((Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) { |
| |
| if (!mChart.needsHighlight(j)) { |
| |
| final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f; |
| |
| mRenderPaint.setColor(dataSet.getColor(j)); |
| |
| final float sliceSpaceAngleOuter = visibleAngleCount == 1 ? |
| 0.f : |
| sliceSpace / (Utils.FDEG2RAD * radius); |
| final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY; |
| float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; |
| if (sweepAngleOuter < 0.f) { |
| sweepAngleOuter = 0.f; |
| } |
| |
| mPathBuffer.reset(); |
| |
| float arcStartPointX = center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD); |
| float arcStartPointY = center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD); |
| |
| if (sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { |
| // Android is doing "mod 360" |
| mPathBuffer.addCircle(center.x, center.y, radius, Path.Direction.CW); |
| } else { |
| |
| mPathBuffer.moveTo(arcStartPointX, arcStartPointY); |
| |
| mPathBuffer.arcTo( |
| circleBox, |
| startAngleOuter, |
| sweepAngleOuter |
| ); |
| } |
| |
| // API < 21 does not receive floats in addArc, but a RectF |
| mInnerRectBuffer.set( |
| center.x - innerRadius, |
| center.y - innerRadius, |
| center.x + innerRadius, |
| center.y + innerRadius); |
| |
| if (drawInnerArc && |
| (innerRadius > 0.f || accountForSliceSpacing)) { |
| |
| if (accountForSliceSpacing) { |
| float minSpacedRadius = |
| calculateMinimumRadiusForSpacedSlice( |
| center, radius, |
| sliceAngle * phaseY, |
| arcStartPointX, arcStartPointY, |
| startAngleOuter, |
| sweepAngleOuter); |
| |
| if (minSpacedRadius < 0.f) |
| minSpacedRadius = -minSpacedRadius; |
| |
| innerRadius = Math.max(innerRadius, minSpacedRadius); |
| } |
| |
| final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ? |
| 0.f : |
| sliceSpace / (Utils.FDEG2RAD * innerRadius); |
| final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY; |
| float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; |
| if (sweepAngleInner < 0.f) { |
| sweepAngleInner = 0.f; |
| } |
| final float endAngleInner = startAngleInner + sweepAngleInner; |
| |
| if (sweepAngleOuter % 360f == 0.f) { |
| // Android is doing "mod 360" |
| mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW); |
| } else { |
| |
| mPathBuffer.lineTo( |
| center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD), |
| center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD)); |
| |
| mPathBuffer.arcTo( |
| mInnerRectBuffer, |
| endAngleInner, |
| -sweepAngleInner |
| ); |
| } |
| } else { |
| |
| if (sweepAngleOuter % 360f != 0.f) { |
| if (accountForSliceSpacing) { |
| |
| float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f; |
| |
| float sliceSpaceOffset = |
| calculateMinimumRadiusForSpacedSlice( |
| center, |
| radius, |
| sliceAngle * phaseY, |
| arcStartPointX, |
| arcStartPointY, |
| startAngleOuter, |
| sweepAngleOuter); |
| |
| float arcEndPointX = center.x + |
| sliceSpaceOffset * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); |
| float arcEndPointY = center.y + |
| sliceSpaceOffset * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); |
| |
| mPathBuffer.lineTo( |
| arcEndPointX, |
| arcEndPointY); |
| |
| } else { |
| mPathBuffer.lineTo( |
| center.x, |
| center.y); |
| } |
| } |
| |
| } |
| |
| mPathBuffer.close(); |
| |
| mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint); |
| } |
| } |
| |
| angle += sliceAngle * phaseX; |
| } |
| |
| MPPointF.recycleInstance(center); |
| } |
| |
| @Override |
| public void drawValues(Canvas c) { |
| |
| MPPointF center = mChart.getCenterCircleBox(); |
| |
| // get whole the radius |
| float radius = mChart.getRadius(); |
| float rotationAngle = mChart.getRotationAngle(); |
| float[] drawAngles = mChart.getDrawAngles(); |
| float[] absoluteAngles = mChart.getAbsoluteAngles(); |
| |
| float phaseX = mAnimator.getPhaseX(); |
| float phaseY = mAnimator.getPhaseY(); |
| |
| final float holeRadiusPercent = mChart.getHoleRadius() / 100.f; |
| float labelRadiusOffset = radius / 10f * 3.6f; |
| |
| if (mChart.isDrawHoleEnabled()) { |
| labelRadiusOffset = (radius - (radius * holeRadiusPercent)) / 2f; |
| } |
| |
| final float labelRadius = radius - labelRadiusOffset; |
| |
| PieData data = mChart.getData(); |
| List<IPieDataSet> dataSets = data.getDataSets(); |
| |
| float yValueSum = data.getYValueSum(); |
| |
| boolean drawEntryLabels = mChart.isDrawEntryLabelsEnabled(); |
| |
| float angle; |
| int xIndex = 0; |
| |
| c.save(); |
| |
| float offset = Utils.convertDpToPixel(5.f); |
| |
| for (int i = 0; i < dataSets.size(); i++) { |
| |
| IPieDataSet dataSet = dataSets.get(i); |
| |
| final boolean drawValues = dataSet.isDrawValuesEnabled(); |
| |
| if (!drawValues && !drawEntryLabels) |
| continue; |
| |
| final PieDataSet.ValuePosition xValuePosition = dataSet.getXValuePosition(); |
| final PieDataSet.ValuePosition yValuePosition = dataSet.getYValuePosition(); |
| |
| // apply the text-styling defined by the DataSet |
| applyValueTextStyle(dataSet); |
| |
| float lineHeight = Utils.calcTextHeight(mValuePaint, "Q") |
| + Utils.convertDpToPixel(4f); |
| |
| IValueFormatter formatter = dataSet.getValueFormatter(); |
| |
| int entryCount = dataSet.getEntryCount(); |
| |
| mValueLinePaint.setColor(dataSet.getValueLineColor()); |
| mValueLinePaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getValueLineWidth())); |
| |
| final float sliceSpace = getSliceSpace(dataSet); |
| |
| for (int j = 0; j < entryCount; j++) { |
| |
| PieEntry entry = dataSet.getEntryForIndex(j); |
| |
| if (xIndex == 0) |
| angle = 0.f; |
| else |
| angle = absoluteAngles[xIndex - 1] * phaseX; |
| |
| final float sliceAngle = drawAngles[xIndex]; |
| final float sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius); |
| |
| // offset needed to center the drawn text in the slice |
| final float angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2.f) / 2.f; |
| |
| angle = angle + angleOffset; |
| |
| final float transformedAngle = rotationAngle + angle * phaseY; |
| |
| float value = mChart.isUsePercentValuesEnabled() ? entry.getY() |
| / yValueSum * 100f : entry.getY(); |
| |
| final float sliceXBase = (float) Math.cos(transformedAngle * Utils.FDEG2RAD); |
| final float sliceYBase = (float) Math.sin(transformedAngle * Utils.FDEG2RAD); |
| |
| final boolean drawXOutside = drawEntryLabels && |
| xValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE; |
| final boolean drawYOutside = drawValues && |
| yValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE; |
| final boolean drawXInside = drawEntryLabels && |
| xValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE; |
| final boolean drawYInside = drawValues && |
| yValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE; |
| |
| if (drawXOutside || drawYOutside) { |
| |
| final float valueLineLength1 = dataSet.getValueLinePart1Length(); |
| final float valueLineLength2 = dataSet.getValueLinePart2Length(); |
| final float valueLinePart1OffsetPercentage = dataSet.getValueLinePart1OffsetPercentage() / 100.f; |
| |
| float pt2x, pt2y; |
| float labelPtx, labelPty; |
| |
| float line1Radius; |
| |
| if (mChart.isDrawHoleEnabled()) |
| line1Radius = (radius - (radius * holeRadiusPercent)) |
| * valueLinePart1OffsetPercentage |
| + (radius * holeRadiusPercent); |
| else |
| line1Radius = radius * valueLinePart1OffsetPercentage; |
| |
| final float polyline2Width = dataSet.isValueLineVariableLength() |
| ? labelRadius * valueLineLength2 * (float) Math.abs(Math.sin( |
| transformedAngle * Utils.FDEG2RAD)) |
| : labelRadius * valueLineLength2; |
| |
| final float pt0x = line1Radius * sliceXBase + center.x; |
| final float pt0y = line1Radius * sliceYBase + center.y; |
| |
| final float pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x; |
| final float pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y; |
| |
| if (transformedAngle % 360.0 >= 90.0 && transformedAngle % 360.0 <= 270.0) { |
| pt2x = pt1x - polyline2Width; |
| pt2y = pt1y; |
| |
| mValuePaint.setTextAlign(Align.RIGHT); |
| |
| if(drawXOutside) |
| mEntryLabelsPaint.setTextAlign(Align.RIGHT); |
| |
| labelPtx = pt2x - offset; |
| labelPty = pt2y; |
| } else { |
| pt2x = pt1x + polyline2Width; |
| pt2y = pt1y; |
| mValuePaint.setTextAlign(Align.LEFT); |
| |
| if(drawXOutside) |
| mEntryLabelsPaint.setTextAlign(Align.LEFT); |
| |
| labelPtx = pt2x + offset; |
| labelPty = pt2y; |
| } |
| |
| if (dataSet.getValueLineColor() != ColorTemplate.COLOR_NONE) { |
| c.drawLine(pt0x, pt0y, pt1x, pt1y, mValueLinePaint); |
| c.drawLine(pt1x, pt1y, pt2x, pt2y, mValueLinePaint); |
| } |
| |
| // draw everything, depending on settings |
| if (drawXOutside && drawYOutside) { |
| |
| drawValue(c, |
| formatter, |
| value, |
| entry, |
| 0, |
| labelPtx, |
| labelPty, |
| dataSet.getValueTextColor(j)); |
| |
| if (j < data.getEntryCount() && entry.getLabel() != null) { |
| drawEntryLabel(c, entry.getLabel(), labelPtx, labelPty + lineHeight); |
| } |
| |
| } else if (drawXOutside) { |
| if (j < data.getEntryCount() && entry.getLabel() != null) { |
| drawEntryLabel(c, entry.getLabel(), labelPtx, labelPty + lineHeight / 2.f); |
| } |
| } else if (drawYOutside) { |
| |
| drawValue(c, formatter, value, entry, 0, labelPtx, labelPty + lineHeight / 2.f, dataSet |
| .getValueTextColor(j)); |
| } |
| } |
| |
| if (drawXInside || drawYInside) { |
| // calculate the text position |
| float x = labelRadius * sliceXBase + center.x; |
| float y = labelRadius * sliceYBase + center.y; |
| |
| mValuePaint.setTextAlign(Align.CENTER); |
| |
| // draw everything, depending on settings |
| if (drawXInside && drawYInside) { |
| |
| drawValue(c, formatter, value, entry, 0, x, y, dataSet.getValueTextColor(j)); |
| |
| if (j < data.getEntryCount() && entry.getLabel() != null) { |
| drawEntryLabel(c, entry.getLabel(), x, y + lineHeight); |
| } |
| |
| } else if (drawXInside) { |
| if (j < data.getEntryCount() && entry.getLabel() != null) { |
| drawEntryLabel(c, entry.getLabel(), x, y + lineHeight / 2f); |
| } |
| } else if (drawYInside) { |
| |
| drawValue(c, formatter, value, entry, 0, x, y + lineHeight / 2f, dataSet.getValueTextColor(j)); |
| } |
| } |
| |
| xIndex++; |
| } |
| } |
| MPPointF.recycleInstance(center); |
| c.restore(); |
| } |
| |
| /** |
| * Draws an entry label at the specified position. |
| * |
| * @param c |
| * @param label |
| * @param x |
| * @param y |
| */ |
| protected void drawEntryLabel(Canvas c, String label, float x, float y) { |
| c.drawText(label, x, y, mEntryLabelsPaint); |
| } |
| |
| @Override |
| public void drawExtras(Canvas c) { |
| // drawCircles(c); |
| drawHole(c); |
| c.drawBitmap(mDrawBitmap.get(), 0, 0, null); |
| drawCenterText(c); |
| } |
| |
| private Path mHoleCirclePath = new Path(); |
| |
| /** |
| * draws the hole in the center of the chart and the transparent circle / |
| * hole |
| */ |
| protected void drawHole(Canvas c) { |
| |
| if (mChart.isDrawHoleEnabled() && mBitmapCanvas != null) { |
| |
| float radius = mChart.getRadius(); |
| float holeRadius = radius * (mChart.getHoleRadius() / 100); |
| MPPointF center = mChart.getCenterCircleBox(); |
| |
| if (Color.alpha(mHolePaint.getColor()) > 0) { |
| // draw the hole-circle |
| mBitmapCanvas.drawCircle( |
| center.x, center.y, |
| holeRadius, mHolePaint); |
| } |
| |
| // only draw the circle if it can be seen (not covered by the hole) |
| if (Color.alpha(mTransparentCirclePaint.getColor()) > 0 && |
| mChart.getTransparentCircleRadius() > mChart.getHoleRadius()) { |
| |
| int alpha = mTransparentCirclePaint.getAlpha(); |
| float secondHoleRadius = radius * (mChart.getTransparentCircleRadius() / 100); |
| |
| mTransparentCirclePaint.setAlpha((int) ((float) alpha * mAnimator.getPhaseX() * mAnimator.getPhaseY())); |
| |
| // draw the transparent-circle |
| mHoleCirclePath.reset(); |
| mHoleCirclePath.addCircle(center.x, center.y, secondHoleRadius, Path.Direction.CW); |
| mHoleCirclePath.addCircle(center.x, center.y, holeRadius, Path.Direction.CCW); |
| mBitmapCanvas.drawPath(mHoleCirclePath, mTransparentCirclePaint); |
| |
| // reset alpha |
| mTransparentCirclePaint.setAlpha(alpha); |
| } |
| MPPointF.recycleInstance(center); |
| } |
| } |
| |
| protected Path mDrawCenterTextPathBuffer = new Path(); |
| /** |
| * draws the description text in the center of the pie chart makes most |
| * sense when center-hole is enabled |
| */ |
| protected void drawCenterText(Canvas c) { |
| |
| CharSequence centerText = mChart.getCenterText(); |
| |
| if (mChart.isDrawCenterTextEnabled() && centerText != null) { |
| |
| MPPointF center = mChart.getCenterCircleBox(); |
| MPPointF offset = mChart.getCenterTextOffset(); |
| |
| float x = center.x + offset.x; |
| float y = center.y + offset.y; |
| |
| float innerRadius = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled() |
| ? mChart.getRadius() * (mChart.getHoleRadius() / 100f) |
| : mChart.getRadius(); |
| |
| RectF holeRect = mRectBuffer[0]; |
| holeRect.left = x - innerRadius; |
| holeRect.top = y - innerRadius; |
| holeRect.right = x + innerRadius; |
| holeRect.bottom = y + innerRadius; |
| RectF boundingRect = mRectBuffer[1]; |
| boundingRect.set(holeRect); |
| |
| float radiusPercent = mChart.getCenterTextRadiusPercent() / 100f; |
| if (radiusPercent > 0.0) { |
| boundingRect.inset( |
| (boundingRect.width() - boundingRect.width() * radiusPercent) / 2.f, |
| (boundingRect.height() - boundingRect.height() * radiusPercent) / 2.f |
| ); |
| } |
| |
| if (!centerText.equals(mCenterTextLastValue) || !boundingRect.equals(mCenterTextLastBounds)) { |
| |
| // Next time we won't recalculate StaticLayout... |
| mCenterTextLastBounds.set(boundingRect); |
| mCenterTextLastValue = centerText; |
| |
| float width = mCenterTextLastBounds.width(); |
| |
| // If width is 0, it will crash. Always have a minimum of 1 |
| mCenterTextLayout = new StaticLayout(centerText, 0, centerText.length(), |
| mCenterTextPaint, |
| (int) Math.max(Math.ceil(width), 1.f), |
| Layout.Alignment.ALIGN_CENTER, 1.f, 0.f, false); |
| } |
| |
| //float layoutWidth = Utils.getStaticLayoutMaxWidth(mCenterTextLayout); |
| float layoutHeight = mCenterTextLayout.getHeight(); |
| |
| c.save(); |
| if (Build.VERSION.SDK_INT >= 18) { |
| Path path = mDrawCenterTextPathBuffer; |
| path.reset(); |
| path.addOval(holeRect, Path.Direction.CW); |
| c.clipPath(path); |
| } |
| |
| c.translate(boundingRect.left, boundingRect.top + (boundingRect.height() - layoutHeight) / 2.f); |
| mCenterTextLayout.draw(c); |
| |
| c.restore(); |
| |
| MPPointF.recycleInstance(center); |
| MPPointF.recycleInstance(offset); |
| } |
| } |
| |
| protected RectF mDrawHighlightedRectF = new RectF(); |
| @Override |
| public void drawHighlighted(Canvas c, Highlight[] indices) { |
| |
| float phaseX = mAnimator.getPhaseX(); |
| float phaseY = mAnimator.getPhaseY(); |
| |
| float angle; |
| float rotationAngle = mChart.getRotationAngle(); |
| |
| float[] drawAngles = mChart.getDrawAngles(); |
| float[] absoluteAngles = mChart.getAbsoluteAngles(); |
| final MPPointF center = mChart.getCenterCircleBox(); |
| final float radius = mChart.getRadius(); |
| final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled(); |
| final float userInnerRadius = drawInnerArc |
| ? radius * (mChart.getHoleRadius() / 100.f) |
| : 0.f; |
| |
| final RectF highlightedCircleBox = mDrawHighlightedRectF; |
| highlightedCircleBox.set(0,0,0,0); |
| |
| for (int i = 0; i < indices.length; i++) { |
| |
| // get the index to highlight |
| int index = (int) indices[i].getX(); |
| |
| if (index >= drawAngles.length) |
| continue; |
| |
| IPieDataSet set = mChart.getData() |
| .getDataSetByIndex(indices[i] |
| .getDataSetIndex()); |
| |
| if (set == null || !set.isHighlightEnabled()) |
| continue; |
| |
| final int entryCount = set.getEntryCount(); |
| int visibleAngleCount = 0; |
| for (int j = 0; j < entryCount; j++) { |
| // draw only if the value is greater than zero |
| if ((Math.abs(set.getEntryForIndex(j).getY()) > Utils.FLOAT_EPSILON)) { |
| visibleAngleCount++; |
| } |
| } |
| |
| if (index == 0) |
| angle = 0.f; |
| else |
| angle = absoluteAngles[index - 1] * phaseX; |
| |
| final float sliceSpace = visibleAngleCount <= 1 ? 0.f : set.getSliceSpace(); |
| |
| float sliceAngle = drawAngles[index]; |
| float innerRadius = userInnerRadius; |
| |
| float shift = set.getSelectionShift(); |
| final float highlightedRadius = radius + shift; |
| highlightedCircleBox.set(mChart.getCircleBox()); |
| highlightedCircleBox.inset(-shift, -shift); |
| |
| final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f; |
| |
| mRenderPaint.setColor(set.getColor(index)); |
| |
| final float sliceSpaceAngleOuter = visibleAngleCount == 1 ? |
| 0.f : |
| sliceSpace / (Utils.FDEG2RAD * radius); |
| |
| final float sliceSpaceAngleShifted = visibleAngleCount == 1 ? |
| 0.f : |
| sliceSpace / (Utils.FDEG2RAD * highlightedRadius); |
| |
| final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY; |
| float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; |
| if (sweepAngleOuter < 0.f) { |
| sweepAngleOuter = 0.f; |
| } |
| |
| final float startAngleShifted = rotationAngle + (angle + sliceSpaceAngleShifted / 2.f) * phaseY; |
| float sweepAngleShifted = (sliceAngle - sliceSpaceAngleShifted) * phaseY; |
| if (sweepAngleShifted < 0.f) { |
| sweepAngleShifted = 0.f; |
| } |
| |
| mPathBuffer.reset(); |
| |
| if (sweepAngleOuter % 360f == 0.f) { |
| // Android is doing "mod 360" |
| mPathBuffer.addCircle(center.x, center.y, highlightedRadius, Path.Direction.CW); |
| } else { |
| |
| mPathBuffer.moveTo( |
| center.x + highlightedRadius * (float) Math.cos(startAngleShifted * Utils.FDEG2RAD), |
| center.y + highlightedRadius * (float) Math.sin(startAngleShifted * Utils.FDEG2RAD)); |
| |
| mPathBuffer.arcTo( |
| highlightedCircleBox, |
| startAngleShifted, |
| sweepAngleShifted |
| ); |
| } |
| |
| float sliceSpaceRadius = 0.f; |
| if (accountForSliceSpacing) { |
| sliceSpaceRadius = |
| calculateMinimumRadiusForSpacedSlice( |
| center, radius, |
| sliceAngle * phaseY, |
| center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD), |
| center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD), |
| startAngleOuter, |
| sweepAngleOuter); |
| } |
| |
| // API < 21 does not receive floats in addArc, but a RectF |
| mInnerRectBuffer.set( |
| center.x - innerRadius, |
| center.y - innerRadius, |
| center.x + innerRadius, |
| center.y + innerRadius); |
| |
| if (drawInnerArc && |
| (innerRadius > 0.f || accountForSliceSpacing)) { |
| |
| if (accountForSliceSpacing) { |
| float minSpacedRadius = sliceSpaceRadius; |
| |
| if (minSpacedRadius < 0.f) |
| minSpacedRadius = -minSpacedRadius; |
| |
| innerRadius = Math.max(innerRadius, minSpacedRadius); |
| } |
| |
| final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ? |
| 0.f : |
| sliceSpace / (Utils.FDEG2RAD * innerRadius); |
| final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY; |
| float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; |
| if (sweepAngleInner < 0.f) { |
| sweepAngleInner = 0.f; |
| } |
| final float endAngleInner = startAngleInner + sweepAngleInner; |
| |
| if (sweepAngleOuter % 360f == 0.f) { |
| // Android is doing "mod 360" |
| mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW); |
| } else { |
| |
| mPathBuffer.lineTo( |
| center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD), |
| center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD)); |
| |
| mPathBuffer.arcTo( |
| mInnerRectBuffer, |
| endAngleInner, |
| -sweepAngleInner |
| ); |
| } |
| } else { |
| |
| if (sweepAngleOuter % 360f != 0.f) { |
| |
| if (accountForSliceSpacing) { |
| final float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f; |
| |
| final float arcEndPointX = center.x + |
| sliceSpaceRadius * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); |
| final float arcEndPointY = center.y + |
| sliceSpaceRadius * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); |
| |
| mPathBuffer.lineTo( |
| arcEndPointX, |
| arcEndPointY); |
| |
| } else { |
| |
| mPathBuffer.lineTo( |
| center.x, |
| center.y); |
| } |
| |
| } |
| |
| } |
| |
| mPathBuffer.close(); |
| |
| mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint); |
| } |
| |
| MPPointF.recycleInstance(center); |
| } |
| |
| /** |
| * This gives all pie-slices a rounded edge. |
| * |
| * @param c |
| */ |
| protected void drawRoundedSlices(Canvas c) { |
| |
| if (!mChart.isDrawRoundedSlicesEnabled()) |
| return; |
| |
| IPieDataSet dataSet = mChart.getData().getDataSet(); |
| |
| if (!dataSet.isVisible()) |
| return; |
| |
| float phaseX = mAnimator.getPhaseX(); |
| float phaseY = mAnimator.getPhaseY(); |
| |
| MPPointF center = mChart.getCenterCircleBox(); |
| float r = mChart.getRadius(); |
| |
| // calculate the radius of the "slice-circle" |
| float circleRadius = (r - (r * mChart.getHoleRadius() / 100f)) / 2f; |
| |
| float[] drawAngles = mChart.getDrawAngles(); |
| float angle = mChart.getRotationAngle(); |
| |
| for (int j = 0; j < dataSet.getEntryCount(); j++) { |
| |
| float sliceAngle = drawAngles[j]; |
| |
| Entry e = dataSet.getEntryForIndex(j); |
| |
| // draw only if the value is greater than zero |
| if ((Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) { |
| |
| float x = (float) ((r - circleRadius) |
| * Math.cos(Math.toRadians((angle + sliceAngle) |
| * phaseY)) + center.x); |
| float y = (float) ((r - circleRadius) |
| * Math.sin(Math.toRadians((angle + sliceAngle) |
| * phaseY)) + center.y); |
| |
| mRenderPaint.setColor(dataSet.getColor(j)); |
| mBitmapCanvas.drawCircle(x, y, circleRadius, mRenderPaint); |
| } |
| |
| angle += sliceAngle * phaseX; |
| } |
| MPPointF.recycleInstance(center); |
| } |
| |
| /** |
| * Releases the drawing bitmap. This should be called when {@link LineChart#onDetachedFromWindow()}. |
| */ |
| public void releaseBitmap() { |
| if (mBitmapCanvas != null) { |
| mBitmapCanvas.setBitmap(null); |
| mBitmapCanvas = null; |
| } |
| if (mDrawBitmap != null) { |
| mDrawBitmap.get().recycle(); |
| mDrawBitmap.clear(); |
| mDrawBitmap = null; |
| } |
| } |
| } |