1 2 package com.github.mikephil.charting.renderer; 3 4 import android.graphics.Bitmap; 5 import android.graphics.Canvas; 6 import android.graphics.Color; 7 import android.graphics.Paint; 8 import android.graphics.Paint.Align; 9 import android.graphics.Paint.Style; 10 import android.graphics.Path; 11 import android.graphics.RectF; 12 import android.graphics.drawable.Drawable; 13 import android.os.Build; 14 import android.text.Layout; 15 import android.text.StaticLayout; 16 import android.text.TextPaint; 17 18 import com.github.mikephil.charting.animation.ChartAnimator; 19 import com.github.mikephil.charting.charts.LineChart; 20 import com.github.mikephil.charting.charts.PieChart; 21 import com.github.mikephil.charting.data.Entry; 22 import com.github.mikephil.charting.data.PieData; 23 import com.github.mikephil.charting.data.PieDataSet; 24 import com.github.mikephil.charting.data.PieEntry; 25 import com.github.mikephil.charting.formatter.IValueFormatter; 26 import com.github.mikephil.charting.highlight.Highlight; 27 import com.github.mikephil.charting.interfaces.datasets.IPieDataSet; 28 import com.github.mikephil.charting.utils.ColorTemplate; 29 import com.github.mikephil.charting.utils.MPPointF; 30 import com.github.mikephil.charting.utils.Utils; 31 import com.github.mikephil.charting.utils.ViewPortHandler; 32 33 import java.lang.ref.WeakReference; 34 import java.util.List; 35 36 public class PieChartRenderer extends DataRenderer { 37 38 protected PieChart mChart; 39 40 /** 41 * paint for the hole in the center of the pie chart and the transparent 42 * circle 43 */ 44 protected Paint mHolePaint; 45 protected Paint mTransparentCirclePaint; 46 protected Paint mValueLinePaint; 47 48 /** 49 * paint object for the text that can be displayed in the center of the 50 * chart 51 */ 52 private TextPaint mCenterTextPaint; 53 54 /** 55 * paint object used for drwing the slice-text 56 */ 57 private Paint mEntryLabelsPaint; 58 59 private StaticLayout mCenterTextLayout; 60 private CharSequence mCenterTextLastValue; 61 private RectF mCenterTextLastBounds = new RectF(); 62 private RectF[] mRectBuffer = {new RectF(), new RectF(), new RectF()}; 63 64 /** 65 * Bitmap for drawing the center hole 66 */ 67 protected WeakReference<Bitmap> mDrawBitmap; 68 69 protected Canvas mBitmapCanvas; 70 PieChartRenderer(PieChart chart, ChartAnimator animator, ViewPortHandler viewPortHandler)71 public PieChartRenderer(PieChart chart, ChartAnimator animator, 72 ViewPortHandler viewPortHandler) { 73 super(animator, viewPortHandler); 74 mChart = chart; 75 76 mHolePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 77 mHolePaint.setColor(Color.WHITE); 78 mHolePaint.setStyle(Style.FILL); 79 80 mTransparentCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 81 mTransparentCirclePaint.setColor(Color.WHITE); 82 mTransparentCirclePaint.setStyle(Style.FILL); 83 mTransparentCirclePaint.setAlpha(105); 84 85 mCenterTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 86 mCenterTextPaint.setColor(Color.BLACK); 87 mCenterTextPaint.setTextSize(Utils.convertDpToPixel(12f)); 88 89 mValuePaint.setTextSize(Utils.convertDpToPixel(13f)); 90 mValuePaint.setColor(Color.WHITE); 91 mValuePaint.setTextAlign(Align.CENTER); 92 93 mEntryLabelsPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 94 mEntryLabelsPaint.setColor(Color.WHITE); 95 mEntryLabelsPaint.setTextAlign(Align.CENTER); 96 mEntryLabelsPaint.setTextSize(Utils.convertDpToPixel(13f)); 97 98 mValueLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 99 mValueLinePaint.setStyle(Style.STROKE); 100 } 101 getPaintHole()102 public Paint getPaintHole() { 103 return mHolePaint; 104 } 105 getPaintTransparentCircle()106 public Paint getPaintTransparentCircle() { 107 return mTransparentCirclePaint; 108 } 109 getPaintCenterText()110 public TextPaint getPaintCenterText() { 111 return mCenterTextPaint; 112 } 113 getPaintEntryLabels()114 public Paint getPaintEntryLabels() { 115 return mEntryLabelsPaint; 116 } 117 118 @Override initBuffers()119 public void initBuffers() { 120 // TODO Auto-generated method stub 121 } 122 123 @Override drawData(Canvas c)124 public void drawData(Canvas c) { 125 126 int width = (int) mViewPortHandler.getChartWidth(); 127 int height = (int) mViewPortHandler.getChartHeight(); 128 129 Bitmap drawBitmap = mDrawBitmap == null ? null : mDrawBitmap.get(); 130 131 if (drawBitmap == null 132 || (drawBitmap.getWidth() != width) 133 || (drawBitmap.getHeight() != height)) { 134 135 if (width > 0 && height > 0) { 136 drawBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); 137 mDrawBitmap = new WeakReference<>(drawBitmap); 138 mBitmapCanvas = new Canvas(drawBitmap); 139 } else 140 return; 141 } 142 143 drawBitmap.eraseColor(Color.TRANSPARENT); 144 145 PieData pieData = mChart.getData(); 146 147 for (IPieDataSet set : pieData.getDataSets()) { 148 149 if (set.isVisible() && set.getEntryCount() > 0) 150 drawDataSet(c, set); 151 } 152 } 153 154 private Path mPathBuffer = new Path(); 155 private RectF mInnerRectBuffer = new RectF(); 156 calculateMinimumRadiusForSpacedSlice( MPPointF center, float radius, float angle, float arcStartPointX, float arcStartPointY, float startAngle, float sweepAngle)157 protected float calculateMinimumRadiusForSpacedSlice( 158 MPPointF center, 159 float radius, 160 float angle, 161 float arcStartPointX, 162 float arcStartPointY, 163 float startAngle, 164 float sweepAngle) { 165 final float angleMiddle = startAngle + sweepAngle / 2.f; 166 167 // Other point of the arc 168 float arcEndPointX = center.x + radius * (float) Math.cos((startAngle + sweepAngle) * Utils.FDEG2RAD); 169 float arcEndPointY = center.y + radius * (float) Math.sin((startAngle + sweepAngle) * Utils.FDEG2RAD); 170 171 // Middle point on the arc 172 float arcMidPointX = center.x + radius * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); 173 float arcMidPointY = center.y + radius * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); 174 175 // This is the base of the contained triangle 176 double basePointsDistance = Math.sqrt( 177 Math.pow(arcEndPointX - arcStartPointX, 2) + 178 Math.pow(arcEndPointY - arcStartPointY, 2)); 179 180 // After reducing space from both sides of the "slice", 181 // the angle of the contained triangle should stay the same. 182 // So let's find out the height of that triangle. 183 float containedTriangleHeight = (float) (basePointsDistance / 2.0 * 184 Math.tan((180.0 - angle) / 2.0 * Utils.DEG2RAD)); 185 186 // Now we subtract that from the radius 187 float spacedRadius = radius - containedTriangleHeight; 188 189 // And now subtract the height of the arc that's between the triangle and the outer circle 190 spacedRadius -= Math.sqrt( 191 Math.pow(arcMidPointX - (arcEndPointX + arcStartPointX) / 2.f, 2) + 192 Math.pow(arcMidPointY - (arcEndPointY + arcStartPointY) / 2.f, 2)); 193 194 return spacedRadius; 195 } 196 197 /** 198 * Calculates the sliceSpace to use based on visible values and their size compared to the set sliceSpace. 199 * 200 * @param dataSet 201 * @return 202 */ getSliceSpace(IPieDataSet dataSet)203 protected float getSliceSpace(IPieDataSet dataSet) { 204 205 if (!dataSet.isAutomaticallyDisableSliceSpacingEnabled()) 206 return dataSet.getSliceSpace(); 207 208 float spaceSizeRatio = dataSet.getSliceSpace() / mViewPortHandler.getSmallestContentExtension(); 209 float minValueRatio = dataSet.getYMin() / mChart.getData().getYValueSum() * 2; 210 211 float sliceSpace = spaceSizeRatio > minValueRatio ? 0f : dataSet.getSliceSpace(); 212 213 return sliceSpace; 214 } 215 drawDataSet(Canvas c, IPieDataSet dataSet)216 protected void drawDataSet(Canvas c, IPieDataSet dataSet) { 217 218 float angle = 0; 219 float rotationAngle = mChart.getRotationAngle(); 220 221 float phaseX = mAnimator.getPhaseX(); 222 float phaseY = mAnimator.getPhaseY(); 223 224 final RectF circleBox = mChart.getCircleBox(); 225 226 final int entryCount = dataSet.getEntryCount(); 227 final float[] drawAngles = mChart.getDrawAngles(); 228 final MPPointF center = mChart.getCenterCircleBox(); 229 final float radius = mChart.getRadius(); 230 final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled(); 231 final float userInnerRadius = drawInnerArc 232 ? radius * (mChart.getHoleRadius() / 100.f) 233 : 0.f; 234 final float roundedRadius = (radius - (radius * mChart.getHoleRadius() / 100f)) / 2f; 235 final RectF roundedCircleBox = new RectF(); 236 final boolean drawRoundedSlices = drawInnerArc && mChart.isDrawRoundedSlicesEnabled(); 237 238 int visibleAngleCount = 0; 239 for (int j = 0; j < entryCount; j++) { 240 // draw only if the value is greater than zero 241 if ((Math.abs(dataSet.getEntryForIndex(j).getY()) > Utils.FLOAT_EPSILON)) { 242 visibleAngleCount++; 243 } 244 } 245 246 final float sliceSpace = visibleAngleCount <= 1 ? 0.f : getSliceSpace(dataSet); 247 248 for (int j = 0; j < entryCount; j++) { 249 250 float sliceAngle = drawAngles[j]; 251 float innerRadius = userInnerRadius; 252 253 Entry e = dataSet.getEntryForIndex(j); 254 255 // draw only if the value is greater than zero 256 if (!(Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) { 257 angle += sliceAngle * phaseX; 258 continue; 259 } 260 261 // Don't draw if it's highlighted, unless the chart uses rounded slices 262 if (dataSet.isHighlightEnabled() && mChart.needsHighlight(j) && !drawRoundedSlices) { 263 angle += sliceAngle * phaseX; 264 continue; 265 } 266 267 final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f; 268 269 mRenderPaint.setColor(dataSet.getColor(j)); 270 271 final float sliceSpaceAngleOuter = visibleAngleCount == 1 ? 272 0.f : 273 sliceSpace / (Utils.FDEG2RAD * radius); 274 final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY; 275 float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; 276 if (sweepAngleOuter < 0.f) { 277 sweepAngleOuter = 0.f; 278 } 279 280 mPathBuffer.reset(); 281 282 if (drawRoundedSlices) { 283 float x = center.x + (radius - roundedRadius) * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD); 284 float y = center.y + (radius - roundedRadius) * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD); 285 roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius); 286 } 287 288 float arcStartPointX = center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD); 289 float arcStartPointY = center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD); 290 291 if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { 292 // Android is doing "mod 360" 293 mPathBuffer.addCircle(center.x, center.y, radius, Path.Direction.CW); 294 } else { 295 296 if (drawRoundedSlices) { 297 mPathBuffer.arcTo(roundedCircleBox, startAngleOuter + 180, -180); 298 } 299 300 mPathBuffer.arcTo( 301 circleBox, 302 startAngleOuter, 303 sweepAngleOuter 304 ); 305 } 306 307 // API < 21 does not receive floats in addArc, but a RectF 308 mInnerRectBuffer.set( 309 center.x - innerRadius, 310 center.y - innerRadius, 311 center.x + innerRadius, 312 center.y + innerRadius); 313 314 if (drawInnerArc && (innerRadius > 0.f || accountForSliceSpacing)) { 315 316 if (accountForSliceSpacing) { 317 float minSpacedRadius = 318 calculateMinimumRadiusForSpacedSlice( 319 center, radius, 320 sliceAngle * phaseY, 321 arcStartPointX, arcStartPointY, 322 startAngleOuter, 323 sweepAngleOuter); 324 325 if (minSpacedRadius < 0.f) 326 minSpacedRadius = -minSpacedRadius; 327 328 innerRadius = Math.max(innerRadius, minSpacedRadius); 329 } 330 331 final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ? 332 0.f : 333 sliceSpace / (Utils.FDEG2RAD * innerRadius); 334 final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY; 335 float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; 336 if (sweepAngleInner < 0.f) { 337 sweepAngleInner = 0.f; 338 } 339 final float endAngleInner = startAngleInner + sweepAngleInner; 340 341 if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { 342 // Android is doing "mod 360" 343 mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW); 344 } else { 345 346 if (drawRoundedSlices) { 347 float x = center.x + (radius - roundedRadius) * (float) Math.cos(endAngleInner * Utils.FDEG2RAD); 348 float y = center.y + (radius - roundedRadius) * (float) Math.sin(endAngleInner * Utils.FDEG2RAD); 349 roundedCircleBox.set(x - roundedRadius, y - roundedRadius, x + roundedRadius, y + roundedRadius); 350 mPathBuffer.arcTo(roundedCircleBox, endAngleInner, 180); 351 } else 352 mPathBuffer.lineTo( 353 center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD), 354 center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD)); 355 356 mPathBuffer.arcTo( 357 mInnerRectBuffer, 358 endAngleInner, 359 -sweepAngleInner 360 ); 361 } 362 } else { 363 364 if (sweepAngleOuter % 360f > Utils.FLOAT_EPSILON) { 365 if (accountForSliceSpacing) { 366 367 float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f; 368 369 float sliceSpaceOffset = 370 calculateMinimumRadiusForSpacedSlice( 371 center, 372 radius, 373 sliceAngle * phaseY, 374 arcStartPointX, 375 arcStartPointY, 376 startAngleOuter, 377 sweepAngleOuter); 378 379 float arcEndPointX = center.x + 380 sliceSpaceOffset * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); 381 float arcEndPointY = center.y + 382 sliceSpaceOffset * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); 383 384 mPathBuffer.lineTo( 385 arcEndPointX, 386 arcEndPointY); 387 388 } else { 389 mPathBuffer.lineTo( 390 center.x, 391 center.y); 392 } 393 } 394 395 } 396 397 mPathBuffer.close(); 398 399 mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint); 400 401 angle += sliceAngle * phaseX; 402 } 403 404 MPPointF.recycleInstance(center); 405 } 406 407 @Override drawValues(Canvas c)408 public void drawValues(Canvas c) { 409 410 MPPointF center = mChart.getCenterCircleBox(); 411 412 // get whole the radius 413 float radius = mChart.getRadius(); 414 float rotationAngle = mChart.getRotationAngle(); 415 float[] drawAngles = mChart.getDrawAngles(); 416 float[] absoluteAngles = mChart.getAbsoluteAngles(); 417 418 float phaseX = mAnimator.getPhaseX(); 419 float phaseY = mAnimator.getPhaseY(); 420 421 final float roundedRadius = (radius - (radius * mChart.getHoleRadius() / 100f)) / 2f; 422 final float holeRadiusPercent = mChart.getHoleRadius() / 100.f; 423 float labelRadiusOffset = radius / 10f * 3.6f; 424 425 if (mChart.isDrawHoleEnabled()) { 426 labelRadiusOffset = (radius - (radius * holeRadiusPercent)) / 2f; 427 428 if (!mChart.isDrawSlicesUnderHoleEnabled() && mChart.isDrawRoundedSlicesEnabled()) { 429 // Add curved circle slice and spacing to rotation angle, so that it sits nicely inside 430 rotationAngle += roundedRadius * 360 / (Math.PI * 2 * radius); 431 } 432 } 433 434 final float labelRadius = radius - labelRadiusOffset; 435 436 PieData data = mChart.getData(); 437 List<IPieDataSet> dataSets = data.getDataSets(); 438 439 float yValueSum = data.getYValueSum(); 440 441 boolean drawEntryLabels = mChart.isDrawEntryLabelsEnabled(); 442 443 float angle; 444 int xIndex = 0; 445 446 c.save(); 447 448 float offset = Utils.convertDpToPixel(5.f); 449 450 for (int i = 0; i < dataSets.size(); i++) { 451 452 IPieDataSet dataSet = dataSets.get(i); 453 454 final boolean drawValues = dataSet.isDrawValuesEnabled(); 455 456 if (!drawValues && !drawEntryLabels) 457 continue; 458 459 final PieDataSet.ValuePosition xValuePosition = dataSet.getXValuePosition(); 460 final PieDataSet.ValuePosition yValuePosition = dataSet.getYValuePosition(); 461 462 // apply the text-styling defined by the DataSet 463 applyValueTextStyle(dataSet); 464 465 float lineHeight = Utils.calcTextHeight(mValuePaint, "Q") 466 + Utils.convertDpToPixel(4f); 467 468 IValueFormatter formatter = dataSet.getValueFormatter(); 469 470 int entryCount = dataSet.getEntryCount(); 471 472 boolean isUseValueColorForLineEnabled = dataSet.isUseValueColorForLineEnabled(); 473 int valueLineColor = dataSet.getValueLineColor(); 474 475 mValueLinePaint.setStrokeWidth(Utils.convertDpToPixel(dataSet.getValueLineWidth())); 476 477 final float sliceSpace = getSliceSpace(dataSet); 478 479 MPPointF iconsOffset = MPPointF.getInstance(dataSet.getIconsOffset()); 480 iconsOffset.x = Utils.convertDpToPixel(iconsOffset.x); 481 iconsOffset.y = Utils.convertDpToPixel(iconsOffset.y); 482 483 for (int j = 0; j < entryCount; j++) { 484 485 PieEntry entry = dataSet.getEntryForIndex(j); 486 487 if (xIndex == 0) 488 angle = 0.f; 489 else 490 angle = absoluteAngles[xIndex - 1] * phaseX; 491 492 final float sliceAngle = drawAngles[xIndex]; 493 final float sliceSpaceMiddleAngle = sliceSpace / (Utils.FDEG2RAD * labelRadius); 494 495 // offset needed to center the drawn text in the slice 496 final float angleOffset = (sliceAngle - sliceSpaceMiddleAngle / 2.f) / 2.f; 497 498 angle = angle + angleOffset; 499 500 final float transformedAngle = rotationAngle + angle * phaseY; 501 502 float value = mChart.isUsePercentValuesEnabled() ? entry.getY() 503 / yValueSum * 100f : entry.getY(); 504 String entryLabel = entry.getLabel(); 505 506 final float sliceXBase = (float) Math.cos(transformedAngle * Utils.FDEG2RAD); 507 final float sliceYBase = (float) Math.sin(transformedAngle * Utils.FDEG2RAD); 508 509 final boolean drawXOutside = drawEntryLabels && 510 xValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE; 511 final boolean drawYOutside = drawValues && 512 yValuePosition == PieDataSet.ValuePosition.OUTSIDE_SLICE; 513 final boolean drawXInside = drawEntryLabels && 514 xValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE; 515 final boolean drawYInside = drawValues && 516 yValuePosition == PieDataSet.ValuePosition.INSIDE_SLICE; 517 518 if (drawXOutside || drawYOutside) { 519 520 final float valueLineLength1 = dataSet.getValueLinePart1Length(); 521 final float valueLineLength2 = dataSet.getValueLinePart2Length(); 522 final float valueLinePart1OffsetPercentage = dataSet.getValueLinePart1OffsetPercentage() / 100.f; 523 524 float pt2x, pt2y; 525 float labelPtx, labelPty; 526 527 float line1Radius; 528 529 if (mChart.isDrawHoleEnabled()) 530 line1Radius = (radius - (radius * holeRadiusPercent)) 531 * valueLinePart1OffsetPercentage 532 + (radius * holeRadiusPercent); 533 else 534 line1Radius = radius * valueLinePart1OffsetPercentage; 535 536 final float polyline2Width = dataSet.isValueLineVariableLength() 537 ? labelRadius * valueLineLength2 * (float) Math.abs(Math.sin( 538 transformedAngle * Utils.FDEG2RAD)) 539 : labelRadius * valueLineLength2; 540 541 final float pt0x = line1Radius * sliceXBase + center.x; 542 final float pt0y = line1Radius * sliceYBase + center.y; 543 544 final float pt1x = labelRadius * (1 + valueLineLength1) * sliceXBase + center.x; 545 final float pt1y = labelRadius * (1 + valueLineLength1) * sliceYBase + center.y; 546 547 if (transformedAngle % 360.0 >= 90.0 && transformedAngle % 360.0 <= 270.0) { 548 pt2x = pt1x - polyline2Width; 549 pt2y = pt1y; 550 551 mValuePaint.setTextAlign(Align.RIGHT); 552 553 if(drawXOutside) 554 mEntryLabelsPaint.setTextAlign(Align.RIGHT); 555 556 labelPtx = pt2x - offset; 557 labelPty = pt2y; 558 } else { 559 pt2x = pt1x + polyline2Width; 560 pt2y = pt1y; 561 mValuePaint.setTextAlign(Align.LEFT); 562 563 if(drawXOutside) 564 mEntryLabelsPaint.setTextAlign(Align.LEFT); 565 566 labelPtx = pt2x + offset; 567 labelPty = pt2y; 568 } 569 570 int lineColor = ColorTemplate.COLOR_NONE; 571 572 if (isUseValueColorForLineEnabled) 573 lineColor = dataSet.getColor(j); 574 else if (valueLineColor != ColorTemplate.COLOR_NONE) 575 lineColor = valueLineColor; 576 577 if (lineColor != ColorTemplate.COLOR_NONE) { 578 mValueLinePaint.setColor(lineColor); 579 c.drawLine(pt0x, pt0y, pt1x, pt1y, mValueLinePaint); 580 c.drawLine(pt1x, pt1y, pt2x, pt2y, mValueLinePaint); 581 } 582 583 // draw everything, depending on settings 584 if (drawXOutside && drawYOutside) { 585 586 drawValue(c, 587 formatter, 588 value, 589 entry, 590 0, 591 labelPtx, 592 labelPty, 593 dataSet.getValueTextColor(j)); 594 595 if (j < data.getEntryCount() && entryLabel != null) { 596 drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight); 597 } 598 599 } else if (drawXOutside) { 600 if (j < data.getEntryCount() && entryLabel != null) { 601 drawEntryLabel(c, entryLabel, labelPtx, labelPty + lineHeight / 2.f); 602 } 603 } else if (drawYOutside) { 604 605 drawValue(c, formatter, value, entry, 0, labelPtx, labelPty + lineHeight / 2.f, dataSet 606 .getValueTextColor(j)); 607 } 608 } 609 610 if (drawXInside || drawYInside) { 611 // calculate the text position 612 float x = labelRadius * sliceXBase + center.x; 613 float y = labelRadius * sliceYBase + center.y; 614 615 mValuePaint.setTextAlign(Align.CENTER); 616 617 // draw everything, depending on settings 618 if (drawXInside && drawYInside) { 619 620 drawValue(c, formatter, value, entry, 0, x, y, dataSet.getValueTextColor(j)); 621 622 if (j < data.getEntryCount() && entryLabel != null) { 623 drawEntryLabel(c, entryLabel, x, y + lineHeight); 624 } 625 626 } else if (drawXInside) { 627 if (j < data.getEntryCount() && entryLabel != null) { 628 drawEntryLabel(c, entryLabel, x, y + lineHeight / 2f); 629 } 630 } else if (drawYInside) { 631 632 drawValue(c, formatter, value, entry, 0, x, y + lineHeight / 2f, dataSet.getValueTextColor(j)); 633 } 634 } 635 636 if (entry.getIcon() != null && dataSet.isDrawIconsEnabled()) { 637 638 Drawable icon = entry.getIcon(); 639 640 float x = (labelRadius + iconsOffset.y) * sliceXBase + center.x; 641 float y = (labelRadius + iconsOffset.y) * sliceYBase + center.y; 642 y += iconsOffset.x; 643 644 Utils.drawImage( 645 c, 646 icon, 647 (int)x, 648 (int)y, 649 icon.getIntrinsicWidth(), 650 icon.getIntrinsicHeight()); 651 } 652 653 xIndex++; 654 } 655 656 MPPointF.recycleInstance(iconsOffset); 657 } 658 MPPointF.recycleInstance(center); 659 c.restore(); 660 } 661 662 /** 663 * Draws an entry label at the specified position. 664 * 665 * @param c 666 * @param label 667 * @param x 668 * @param y 669 */ drawEntryLabel(Canvas c, String label, float x, float y)670 protected void drawEntryLabel(Canvas c, String label, float x, float y) { 671 c.drawText(label, x, y, mEntryLabelsPaint); 672 } 673 674 @Override drawExtras(Canvas c)675 public void drawExtras(Canvas c) { 676 drawHole(c); 677 c.drawBitmap(mDrawBitmap.get(), 0, 0, null); 678 drawCenterText(c); 679 } 680 681 private Path mHoleCirclePath = new Path(); 682 683 /** 684 * draws the hole in the center of the chart and the transparent circle / 685 * hole 686 */ drawHole(Canvas c)687 protected void drawHole(Canvas c) { 688 689 if (mChart.isDrawHoleEnabled() && mBitmapCanvas != null) { 690 691 float radius = mChart.getRadius(); 692 float holeRadius = radius * (mChart.getHoleRadius() / 100); 693 MPPointF center = mChart.getCenterCircleBox(); 694 695 if (Color.alpha(mHolePaint.getColor()) > 0) { 696 // draw the hole-circle 697 mBitmapCanvas.drawCircle( 698 center.x, center.y, 699 holeRadius, mHolePaint); 700 } 701 702 // only draw the circle if it can be seen (not covered by the hole) 703 if (Color.alpha(mTransparentCirclePaint.getColor()) > 0 && 704 mChart.getTransparentCircleRadius() > mChart.getHoleRadius()) { 705 706 int alpha = mTransparentCirclePaint.getAlpha(); 707 float secondHoleRadius = radius * (mChart.getTransparentCircleRadius() / 100); 708 709 mTransparentCirclePaint.setAlpha((int) ((float) alpha * mAnimator.getPhaseX() * mAnimator.getPhaseY())); 710 711 // draw the transparent-circle 712 mHoleCirclePath.reset(); 713 mHoleCirclePath.addCircle(center.x, center.y, secondHoleRadius, Path.Direction.CW); 714 mHoleCirclePath.addCircle(center.x, center.y, holeRadius, Path.Direction.CCW); 715 mBitmapCanvas.drawPath(mHoleCirclePath, mTransparentCirclePaint); 716 717 // reset alpha 718 mTransparentCirclePaint.setAlpha(alpha); 719 } 720 MPPointF.recycleInstance(center); 721 } 722 } 723 724 protected Path mDrawCenterTextPathBuffer = new Path(); 725 /** 726 * draws the description text in the center of the pie chart makes most 727 * sense when center-hole is enabled 728 */ drawCenterText(Canvas c)729 protected void drawCenterText(Canvas c) { 730 731 CharSequence centerText = mChart.getCenterText(); 732 733 if (mChart.isDrawCenterTextEnabled() && centerText != null) { 734 735 MPPointF center = mChart.getCenterCircleBox(); 736 MPPointF offset = mChart.getCenterTextOffset(); 737 738 float x = center.x + offset.x; 739 float y = center.y + offset.y; 740 741 float innerRadius = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled() 742 ? mChart.getRadius() * (mChart.getHoleRadius() / 100f) 743 : mChart.getRadius(); 744 745 RectF holeRect = mRectBuffer[0]; 746 holeRect.left = x - innerRadius; 747 holeRect.top = y - innerRadius; 748 holeRect.right = x + innerRadius; 749 holeRect.bottom = y + innerRadius; 750 RectF boundingRect = mRectBuffer[1]; 751 boundingRect.set(holeRect); 752 753 float radiusPercent = mChart.getCenterTextRadiusPercent() / 100f; 754 if (radiusPercent > 0.0) { 755 boundingRect.inset( 756 (boundingRect.width() - boundingRect.width() * radiusPercent) / 2.f, 757 (boundingRect.height() - boundingRect.height() * radiusPercent) / 2.f 758 ); 759 } 760 761 if (!centerText.equals(mCenterTextLastValue) || !boundingRect.equals(mCenterTextLastBounds)) { 762 763 // Next time we won't recalculate StaticLayout... 764 mCenterTextLastBounds.set(boundingRect); 765 mCenterTextLastValue = centerText; 766 767 float width = mCenterTextLastBounds.width(); 768 769 // If width is 0, it will crash. Always have a minimum of 1 770 mCenterTextLayout = new StaticLayout(centerText, 0, centerText.length(), 771 mCenterTextPaint, 772 (int) Math.max(Math.ceil(width), 1.f), 773 Layout.Alignment.ALIGN_CENTER, 1.f, 0.f, false); 774 } 775 776 //float layoutWidth = Utils.getStaticLayoutMaxWidth(mCenterTextLayout); 777 float layoutHeight = mCenterTextLayout.getHeight(); 778 779 c.save(); 780 if (Build.VERSION.SDK_INT >= 18) { 781 Path path = mDrawCenterTextPathBuffer; 782 path.reset(); 783 path.addOval(holeRect, Path.Direction.CW); 784 c.clipPath(path); 785 } 786 787 c.translate(boundingRect.left, boundingRect.top + (boundingRect.height() - layoutHeight) / 2.f); 788 mCenterTextLayout.draw(c); 789 790 c.restore(); 791 792 MPPointF.recycleInstance(center); 793 MPPointF.recycleInstance(offset); 794 } 795 } 796 797 protected RectF mDrawHighlightedRectF = new RectF(); 798 @Override drawHighlighted(Canvas c, Highlight[] indices)799 public void drawHighlighted(Canvas c, Highlight[] indices) { 800 801 /* Skip entirely if using rounded circle slices, because it doesn't make sense to highlight 802 * in this way. 803 * TODO: add support for changing slice color with highlighting rather than only shifting the slice 804 */ 805 806 final boolean drawInnerArc = mChart.isDrawHoleEnabled() && !mChart.isDrawSlicesUnderHoleEnabled(); 807 if (drawInnerArc && mChart.isDrawRoundedSlicesEnabled()) 808 return; 809 810 float phaseX = mAnimator.getPhaseX(); 811 float phaseY = mAnimator.getPhaseY(); 812 813 float angle; 814 float rotationAngle = mChart.getRotationAngle(); 815 816 float[] drawAngles = mChart.getDrawAngles(); 817 float[] absoluteAngles = mChart.getAbsoluteAngles(); 818 final MPPointF center = mChart.getCenterCircleBox(); 819 final float radius = mChart.getRadius(); 820 final float userInnerRadius = drawInnerArc 821 ? radius * (mChart.getHoleRadius() / 100.f) 822 : 0.f; 823 824 final RectF highlightedCircleBox = mDrawHighlightedRectF; 825 highlightedCircleBox.set(0,0,0,0); 826 827 for (int i = 0; i < indices.length; i++) { 828 829 // get the index to highlight 830 int index = (int) indices[i].getX(); 831 832 if (index >= drawAngles.length) 833 continue; 834 835 IPieDataSet set = mChart.getData() 836 .getDataSetByIndex(indices[i].getDataSetIndex()); 837 838 if (set == null || !set.isHighlightEnabled()) 839 continue; 840 841 final int entryCount = set.getEntryCount(); 842 int visibleAngleCount = 0; 843 for (int j = 0; j < entryCount; j++) { 844 // draw only if the value is greater than zero 845 if ((Math.abs(set.getEntryForIndex(j).getY()) > Utils.FLOAT_EPSILON)) { 846 visibleAngleCount++; 847 } 848 } 849 850 if (index == 0) 851 angle = 0.f; 852 else 853 angle = absoluteAngles[index - 1] * phaseX; 854 855 final float sliceSpace = visibleAngleCount <= 1 ? 0.f : set.getSliceSpace(); 856 857 float sliceAngle = drawAngles[index]; 858 float innerRadius = userInnerRadius; 859 860 float shift = set.getSelectionShift(); 861 final float highlightedRadius = radius + shift; 862 highlightedCircleBox.set(mChart.getCircleBox()); 863 highlightedCircleBox.inset(-shift, -shift); 864 865 final boolean accountForSliceSpacing = sliceSpace > 0.f && sliceAngle <= 180.f; 866 867 Integer highlightColor = set.getHighlightColor(); 868 if (highlightColor == null) 869 highlightColor = set.getColor(index); 870 mRenderPaint.setColor(highlightColor); 871 872 final float sliceSpaceAngleOuter = visibleAngleCount == 1 ? 873 0.f : 874 sliceSpace / (Utils.FDEG2RAD * radius); 875 876 final float sliceSpaceAngleShifted = visibleAngleCount == 1 ? 877 0.f : 878 sliceSpace / (Utils.FDEG2RAD * highlightedRadius); 879 880 final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY; 881 float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; 882 if (sweepAngleOuter < 0.f) { 883 sweepAngleOuter = 0.f; 884 } 885 886 final float startAngleShifted = rotationAngle + (angle + sliceSpaceAngleShifted / 2.f) * phaseY; 887 float sweepAngleShifted = (sliceAngle - sliceSpaceAngleShifted) * phaseY; 888 if (sweepAngleShifted < 0.f) { 889 sweepAngleShifted = 0.f; 890 } 891 892 mPathBuffer.reset(); 893 894 if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { 895 // Android is doing "mod 360" 896 mPathBuffer.addCircle(center.x, center.y, highlightedRadius, Path.Direction.CW); 897 } else { 898 899 mPathBuffer.moveTo( 900 center.x + highlightedRadius * (float) Math.cos(startAngleShifted * Utils.FDEG2RAD), 901 center.y + highlightedRadius * (float) Math.sin(startAngleShifted * Utils.FDEG2RAD)); 902 903 mPathBuffer.arcTo( 904 highlightedCircleBox, 905 startAngleShifted, 906 sweepAngleShifted 907 ); 908 } 909 910 float sliceSpaceRadius = 0.f; 911 if (accountForSliceSpacing) { 912 sliceSpaceRadius = 913 calculateMinimumRadiusForSpacedSlice( 914 center, radius, 915 sliceAngle * phaseY, 916 center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD), 917 center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD), 918 startAngleOuter, 919 sweepAngleOuter); 920 } 921 922 // API < 21 does not receive floats in addArc, but a RectF 923 mInnerRectBuffer.set( 924 center.x - innerRadius, 925 center.y - innerRadius, 926 center.x + innerRadius, 927 center.y + innerRadius); 928 929 if (drawInnerArc && 930 (innerRadius > 0.f || accountForSliceSpacing)) { 931 932 if (accountForSliceSpacing) { 933 float minSpacedRadius = sliceSpaceRadius; 934 935 if (minSpacedRadius < 0.f) 936 minSpacedRadius = -minSpacedRadius; 937 938 innerRadius = Math.max(innerRadius, minSpacedRadius); 939 } 940 941 final float sliceSpaceAngleInner = visibleAngleCount == 1 || innerRadius == 0.f ? 942 0.f : 943 sliceSpace / (Utils.FDEG2RAD * innerRadius); 944 final float startAngleInner = rotationAngle + (angle + sliceSpaceAngleInner / 2.f) * phaseY; 945 float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY; 946 if (sweepAngleInner < 0.f) { 947 sweepAngleInner = 0.f; 948 } 949 final float endAngleInner = startAngleInner + sweepAngleInner; 950 951 if (sweepAngleOuter >= 360.f && sweepAngleOuter % 360f <= Utils.FLOAT_EPSILON) { 952 // Android is doing "mod 360" 953 mPathBuffer.addCircle(center.x, center.y, innerRadius, Path.Direction.CCW); 954 } else { 955 956 mPathBuffer.lineTo( 957 center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD), 958 center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD)); 959 960 mPathBuffer.arcTo( 961 mInnerRectBuffer, 962 endAngleInner, 963 -sweepAngleInner 964 ); 965 } 966 } else { 967 968 if (sweepAngleOuter % 360f > Utils.FLOAT_EPSILON) { 969 970 if (accountForSliceSpacing) { 971 final float angleMiddle = startAngleOuter + sweepAngleOuter / 2.f; 972 973 final float arcEndPointX = center.x + 974 sliceSpaceRadius * (float) Math.cos(angleMiddle * Utils.FDEG2RAD); 975 final float arcEndPointY = center.y + 976 sliceSpaceRadius * (float) Math.sin(angleMiddle * Utils.FDEG2RAD); 977 978 mPathBuffer.lineTo( 979 arcEndPointX, 980 arcEndPointY); 981 982 } else { 983 984 mPathBuffer.lineTo( 985 center.x, 986 center.y); 987 } 988 989 } 990 991 } 992 993 mPathBuffer.close(); 994 995 mBitmapCanvas.drawPath(mPathBuffer, mRenderPaint); 996 } 997 998 MPPointF.recycleInstance(center); 999 } 1000 1001 /** 1002 * This gives all pie-slices a rounded edge. 1003 * 1004 * @param c 1005 */ drawRoundedSlices(Canvas c)1006 protected void drawRoundedSlices(Canvas c) { 1007 1008 if (!mChart.isDrawRoundedSlicesEnabled()) 1009 return; 1010 1011 IPieDataSet dataSet = mChart.getData().getDataSet(); 1012 1013 if (!dataSet.isVisible()) 1014 return; 1015 1016 float phaseX = mAnimator.getPhaseX(); 1017 float phaseY = mAnimator.getPhaseY(); 1018 1019 MPPointF center = mChart.getCenterCircleBox(); 1020 float r = mChart.getRadius(); 1021 1022 // calculate the radius of the "slice-circle" 1023 float circleRadius = (r - (r * mChart.getHoleRadius() / 100f)) / 2f; 1024 1025 float[] drawAngles = mChart.getDrawAngles(); 1026 float angle = mChart.getRotationAngle(); 1027 1028 for (int j = 0; j < dataSet.getEntryCount(); j++) { 1029 1030 float sliceAngle = drawAngles[j]; 1031 1032 Entry e = dataSet.getEntryForIndex(j); 1033 1034 // draw only if the value is greater than zero 1035 if ((Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) { 1036 1037 float x = (float) ((r - circleRadius) 1038 * Math.cos(Math.toRadians((angle + sliceAngle) 1039 * phaseY)) + center.x); 1040 float y = (float) ((r - circleRadius) 1041 * Math.sin(Math.toRadians((angle + sliceAngle) 1042 * phaseY)) + center.y); 1043 1044 mRenderPaint.setColor(dataSet.getColor(j)); 1045 mBitmapCanvas.drawCircle(x, y, circleRadius, mRenderPaint); 1046 } 1047 1048 angle += sliceAngle * phaseX; 1049 } 1050 MPPointF.recycleInstance(center); 1051 } 1052 1053 /** 1054 * Releases the drawing bitmap. This should be called when {@link LineChart#onDetachedFromWindow()}. 1055 */ releaseBitmap()1056 public void releaseBitmap() { 1057 if (mBitmapCanvas != null) { 1058 mBitmapCanvas.setBitmap(null); 1059 mBitmapCanvas = null; 1060 } 1061 if (mDrawBitmap != null) { 1062 Bitmap drawBitmap = mDrawBitmap.get(); 1063 if (drawBitmap != null) { 1064 drawBitmap.recycle(); 1065 } 1066 mDrawBitmap.clear(); 1067 mDrawBitmap = null; 1068 } 1069 } 1070 } 1071