1 2 package com.github.mikephil.charting.utils; 3 4 import android.annotation.SuppressLint; 5 import android.content.Context; 6 import android.content.res.Resources; 7 import android.graphics.Canvas; 8 import android.graphics.Paint; 9 import android.graphics.Rect; 10 import android.graphics.drawable.BitmapDrawable; 11 import android.graphics.drawable.Drawable; 12 import android.os.Build; 13 import android.text.Layout; 14 import android.text.StaticLayout; 15 import android.text.TextPaint; 16 import android.util.DisplayMetrics; 17 import android.util.Log; 18 import android.util.SizeF; 19 import android.view.MotionEvent; 20 import android.view.VelocityTracker; 21 import android.view.View; 22 import android.view.ViewConfiguration; 23 24 import com.github.mikephil.charting.formatter.DefaultValueFormatter; 25 import com.github.mikephil.charting.formatter.IValueFormatter; 26 27 import java.util.List; 28 29 /** 30 * Utilities class that has some helper methods. Needs to be initialized by 31 * calling Utils.init(...) before usage. Inside the Chart.init() method, this is 32 * done, if the Utils are used before that, Utils.init(...) needs to be called 33 * manually. 34 * 35 * @author Philipp Jahoda 36 */ 37 public abstract class Utils { 38 39 private static DisplayMetrics mMetrics; 40 private static int mMinimumFlingVelocity = 50; 41 private static int mMaximumFlingVelocity = 8000; 42 public final static double DEG2RAD = (Math.PI / 180.0); 43 public final static float FDEG2RAD = ((float) Math.PI / 180.f); 44 45 @SuppressWarnings("unused") 46 public final static double DOUBLE_EPSILON = Double.longBitsToDouble(1); 47 48 @SuppressWarnings("unused") 49 public final static float FLOAT_EPSILON = Float.intBitsToFloat(1); 50 51 /** 52 * initialize method, called inside the Chart.init() method. 53 * 54 * @param context 55 */ 56 @SuppressWarnings("deprecation") init(Context context)57 public static void init(Context context) { 58 59 if (context == null) { 60 // noinspection deprecation 61 mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); 62 // noinspection deprecation 63 mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity(); 64 65 Log.e("MPChartLib-Utils" 66 , "Utils.init(...) PROVIDED CONTEXT OBJECT IS NULL"); 67 68 } else { 69 ViewConfiguration viewConfiguration = ViewConfiguration.get(context); 70 mMinimumFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity(); 71 mMaximumFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity(); 72 73 Resources res = context.getResources(); 74 mMetrics = res.getDisplayMetrics(); 75 } 76 } 77 78 /** 79 * initialize method, called inside the Chart.init() method. backwards 80 * compatibility - to not break existing code 81 * 82 * @param res 83 */ 84 @Deprecated init(Resources res)85 public static void init(Resources res) { 86 87 mMetrics = res.getDisplayMetrics(); 88 89 // noinspection deprecation 90 mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); 91 // noinspection deprecation 92 mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity(); 93 } 94 95 /** 96 * This method converts dp unit to equivalent pixels, depending on device 97 * density. NEEDS UTILS TO BE INITIALIZED BEFORE USAGE. 98 * 99 * @param dp A value in dp (density independent pixels) unit. Which we need 100 * to convert into pixels 101 * @return A float value to represent px equivalent to dp depending on 102 * device density 103 */ convertDpToPixel(float dp)104 public static float convertDpToPixel(float dp) { 105 106 if (mMetrics == null) { 107 108 Log.e("MPChartLib-Utils", 109 "Utils NOT INITIALIZED. You need to call Utils.init(...) at least once before" + 110 " calling Utils.convertDpToPixel(...). Otherwise conversion does not " + 111 "take place."); 112 return dp; 113 } 114 115 return dp * mMetrics.density; 116 } 117 118 /** 119 * This method converts device specific pixels to density independent 120 * pixels. NEEDS UTILS TO BE INITIALIZED BEFORE USAGE. 121 * 122 * @param px A value in px (pixels) unit. Which we need to convert into db 123 * @return A float value to represent dp equivalent to px value 124 */ convertPixelsToDp(float px)125 public static float convertPixelsToDp(float px) { 126 127 if (mMetrics == null) { 128 129 Log.e("MPChartLib-Utils", 130 "Utils NOT INITIALIZED. You need to call Utils.init(...) at least once before" + 131 " calling Utils.convertPixelsToDp(...). Otherwise conversion does not" + 132 " take place."); 133 return px; 134 } 135 136 return px / mMetrics.density; 137 } 138 139 /** 140 * calculates the approximate width of a text, depending on a demo text 141 * avoid repeated calls (e.g. inside drawing methods) 142 * 143 * @param paint 144 * @param demoText 145 * @return 146 */ calcTextWidth(Paint paint, String demoText)147 public static int calcTextWidth(Paint paint, String demoText) { 148 return (int) paint.measureText(demoText); 149 } 150 151 private static Rect mCalcTextHeightRect = new Rect(); 152 /** 153 * calculates the approximate height of a text, depending on a demo text 154 * avoid repeated calls (e.g. inside drawing methods) 155 * 156 * @param paint 157 * @param demoText 158 * @return 159 */ calcTextHeight(Paint paint, String demoText)160 public static int calcTextHeight(Paint paint, String demoText) { 161 162 Rect r = mCalcTextHeightRect; 163 r.set(0,0,0,0); 164 paint.getTextBounds(demoText, 0, demoText.length(), r); 165 return r.height(); 166 } 167 168 private static Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); 169 getLineHeight(Paint paint)170 public static float getLineHeight(Paint paint) { 171 return getLineHeight(paint, mFontMetrics); 172 } 173 getLineHeight(Paint paint, Paint.FontMetrics fontMetrics)174 public static float getLineHeight(Paint paint, Paint.FontMetrics fontMetrics){ 175 paint.getFontMetrics(fontMetrics); 176 return fontMetrics.descent - fontMetrics.ascent; 177 } 178 getLineSpacing(Paint paint)179 public static float getLineSpacing(Paint paint) { 180 return getLineSpacing(paint, mFontMetrics); 181 } 182 getLineSpacing(Paint paint, Paint.FontMetrics fontMetrics)183 public static float getLineSpacing(Paint paint, Paint.FontMetrics fontMetrics){ 184 paint.getFontMetrics(fontMetrics); 185 return fontMetrics.ascent - fontMetrics.top + fontMetrics.bottom; 186 } 187 188 /** 189 * Returns a recyclable FSize instance. 190 * calculates the approximate size of a text, depending on a demo text 191 * avoid repeated calls (e.g. inside drawing methods) 192 * 193 * @param paint 194 * @param demoText 195 * @return A Recyclable FSize instance 196 */ calcTextSize(Paint paint, String demoText)197 public static FSize calcTextSize(Paint paint, String demoText) { 198 199 FSize result = FSize.getInstance(0,0); 200 calcTextSize(paint, demoText, result); 201 return result; 202 } 203 204 private static Rect mCalcTextSizeRect = new Rect(); 205 /** 206 * calculates the approximate size of a text, depending on a demo text 207 * avoid repeated calls (e.g. inside drawing methods) 208 * 209 * @param paint 210 * @param demoText 211 * @param outputFSize An output variable, modified by the function. 212 */ calcTextSize(Paint paint, String demoText, FSize outputFSize)213 public static void calcTextSize(Paint paint, String demoText, FSize outputFSize) { 214 215 Rect r = mCalcTextSizeRect; 216 r.set(0,0,0,0); 217 paint.getTextBounds(demoText, 0, demoText.length(), r); 218 outputFSize.width = r.width(); 219 outputFSize.height = r.height(); 220 221 } 222 223 224 /** 225 * Math.pow(...) is very expensive, so avoid calling it and create it 226 * yourself. 227 */ 228 private static final int POW_10[] = { 229 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 230 }; 231 232 private static IValueFormatter mDefaultValueFormatter = generateDefaultValueFormatter(); 233 generateDefaultValueFormatter()234 private static IValueFormatter generateDefaultValueFormatter() { 235 final DefaultValueFormatter formatter = new DefaultValueFormatter(1); 236 return formatter; 237 } 238 239 /// - returns: The default value formatter used for all chart components that needs a default getDefaultValueFormatter()240 public static IValueFormatter getDefaultValueFormatter() 241 { 242 return mDefaultValueFormatter; 243 } 244 245 /** 246 * Formats the given number to the given number of decimals, and returns the 247 * number as a string, maximum 35 characters. If thousands are separated, the separating 248 * character is a dot ("."). 249 * 250 * @param number 251 * @param digitCount 252 * @param separateThousands set this to true to separate thousands values 253 * @return 254 */ formatNumber(float number, int digitCount, boolean separateThousands)255 public static String formatNumber(float number, int digitCount, boolean separateThousands) { 256 return formatNumber(number, digitCount, separateThousands, '.'); 257 } 258 259 /** 260 * Formats the given number to the given number of decimals, and returns the 261 * number as a string, maximum 35 characters. 262 * 263 * @param number 264 * @param digitCount 265 * @param separateThousands set this to true to separate thousands values 266 * @param separateChar a caracter to be paced between the "thousands" 267 * @return 268 */ formatNumber(float number, int digitCount, boolean separateThousands, char separateChar)269 public static String formatNumber(float number, int digitCount, boolean separateThousands, 270 char separateChar) { 271 272 char[] out = new char[35]; 273 274 boolean neg = false; 275 if (number == 0) { 276 return "0"; 277 } 278 279 boolean zero = false; 280 if (number < 1 && number > -1) { 281 zero = true; 282 } 283 284 if (number < 0) { 285 neg = true; 286 number = -number; 287 } 288 289 if (digitCount > POW_10.length) { 290 digitCount = POW_10.length - 1; 291 } 292 293 number *= POW_10[digitCount]; 294 long lval = Math.round(number); 295 int ind = out.length - 1; 296 int charCount = 0; 297 boolean decimalPointAdded = false; 298 299 while (lval != 0 || charCount < (digitCount + 1)) { 300 int digit = (int) (lval % 10); 301 lval = lval / 10; 302 out[ind--] = (char) (digit + '0'); 303 charCount++; 304 305 // add decimal point 306 if (charCount == digitCount) { 307 out[ind--] = ','; 308 charCount++; 309 decimalPointAdded = true; 310 311 // add thousand separators 312 } else if (separateThousands && lval != 0 && charCount > digitCount) { 313 314 if (decimalPointAdded) { 315 316 if ((charCount - digitCount) % 4 == 0) { 317 out[ind--] = separateChar; 318 charCount++; 319 } 320 321 } else { 322 323 if ((charCount - digitCount) % 4 == 3) { 324 out[ind--] = separateChar; 325 charCount++; 326 } 327 } 328 } 329 } 330 331 // if number around zero (between 1 and -1) 332 if (zero) { 333 out[ind--] = '0'; 334 charCount += 1; 335 } 336 337 // if the number is negative 338 if (neg) { 339 out[ind--] = '-'; 340 charCount += 1; 341 } 342 343 int start = out.length - charCount; 344 345 // use this instead of "new String(...)" because of issue < Android 4.0 346 return String.valueOf(out, start, out.length - start); 347 } 348 349 /** 350 * rounds the given number to the next significant number 351 * 352 * @param number 353 * @return 354 */ roundToNextSignificant(double number)355 public static float roundToNextSignificant(double number) { 356 if (Double.isInfinite(number) || 357 Double.isNaN(number) || 358 number == 0.0) 359 return 0; 360 361 final float d = (float) Math.ceil((float) Math.log10(number < 0 ? -number : number)); 362 final int pw = 1 - (int) d; 363 final float magnitude = (float) Math.pow(10, pw); 364 final long shifted = Math.round(number * magnitude); 365 return shifted / magnitude; 366 } 367 368 /** 369 * Returns the appropriate number of decimals to be used for the provided 370 * number. 371 * 372 * @param number 373 * @return 374 */ getDecimals(float number)375 public static int getDecimals(float number) { 376 377 float i = roundToNextSignificant(number); 378 379 if (Float.isInfinite(i)) 380 return 0; 381 382 return (int) Math.ceil(-Math.log10(i)) + 2; 383 } 384 385 /** 386 * Converts the provided Integer List to an int array. 387 * 388 * @param integers 389 * @return 390 */ convertIntegers(List<Integer> integers)391 public static int[] convertIntegers(List<Integer> integers) { 392 393 int[] ret = new int[integers.size()]; 394 395 copyIntegers(integers, ret); 396 397 return ret; 398 } 399 copyIntegers(List<Integer> from, int[] to)400 public static void copyIntegers(List<Integer> from, int[] to){ 401 int count = to.length < from.size() ? to.length : from.size(); 402 for(int i = 0 ; i < count ; i++){ 403 to[i] = from.get(i); 404 } 405 } 406 407 /** 408 * Converts the provided String List to a String array. 409 * 410 * @param strings 411 * @return 412 */ 413 public static String[] convertStrings(List<String> strings) { 414 415 String[] ret = new String[strings.size()]; 416 417 for (int i = 0; i < ret.length; i++) { 418 ret[i] = strings.get(i); 419 } 420 421 return ret; 422 } 423 424 public static void copyStrings(List<String> from, String[] to){ 425 int count = to.length < from.size() ? to.length : from.size(); 426 for(int i = 0 ; i < count ; i++){ 427 to[i] = from.get(i); 428 } 429 } 430 431 /** 432 * Replacement for the Math.nextUp(...) method that is only available in 433 * HONEYCOMB and higher. Dat's some seeeeek sheeet. 434 * 435 * @param d 436 * @return 437 */ 438 public static double nextUp(double d) { 439 if (d == Double.POSITIVE_INFINITY) 440 return d; 441 else { 442 d += 0.0d; 443 return Double.longBitsToDouble(Double.doubleToRawLongBits(d) + 444 ((d >= 0.0d) ? +1L : -1L)); 445 } 446 } 447 448 /** 449 * Returns a recyclable MPPointF instance. 450 * Calculates the position around a center point, depending on the distance 451 * from the center, and the angle of the position around the center. 452 * 453 * @param center 454 * @param dist 455 * @param angle in degrees, converted to radians internally 456 * @return 457 */ 458 public static MPPointF getPosition(MPPointF center, float dist, float angle) { 459 460 MPPointF p = MPPointF.getInstance(0,0); 461 getPosition(center, dist, angle, p); 462 return p; 463 } 464 465 public static void getPosition(MPPointF center, float dist, float angle, MPPointF outputPoint){ 466 outputPoint.x = (float) (center.x + dist * Math.cos(Math.toRadians(angle))); 467 outputPoint.y = (float) (center.y + dist * Math.sin(Math.toRadians(angle))); 468 } 469 470 public static void velocityTrackerPointerUpCleanUpIfNecessary(MotionEvent ev, 471 VelocityTracker tracker) { 472 473 // Check the dot product of current velocities. 474 // If the pointer that left was opposing another velocity vector, clear. 475 tracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); 476 final int upIndex = ev.getActionIndex(); 477 final int id1 = ev.getPointerId(upIndex); 478 final float x1 = tracker.getXVelocity(id1); 479 final float y1 = tracker.getYVelocity(id1); 480 for (int i = 0, count = ev.getPointerCount(); i < count; i++) { 481 if (i == upIndex) 482 continue; 483 484 final int id2 = ev.getPointerId(i); 485 final float x = x1 * tracker.getXVelocity(id2); 486 final float y = y1 * tracker.getYVelocity(id2); 487 488 final float dot = x + y; 489 if (dot < 0) { 490 tracker.clear(); 491 break; 492 } 493 } 494 } 495 496 /** 497 * Original method view.postInvalidateOnAnimation() only supportd in API >= 498 * 16, This is a replica of the code from ViewCompat. 499 * 500 * @param view 501 */ 502 @SuppressLint("NewApi") 503 public static void postInvalidateOnAnimation(View view) { 504 if (Build.VERSION.SDK_INT >= 16) 505 view.postInvalidateOnAnimation(); 506 else 507 view.postInvalidateDelayed(10); 508 } 509 510 public static int getMinimumFlingVelocity() { 511 return mMinimumFlingVelocity; 512 } 513 514 public static int getMaximumFlingVelocity() { 515 return mMaximumFlingVelocity; 516 } 517 518 /** 519 * returns an angle between 0.f < 360.f (not less than zero, less than 360) 520 */ 521 public static float getNormalizedAngle(float angle) { 522 while (angle < 0.f) 523 angle += 360.f; 524 525 return angle % 360.f; 526 } 527 528 private static Rect mDrawableBoundsCache = new Rect(); 529 530 public static void drawImage(Canvas canvas, 531 Drawable drawable, 532 int x, int y, 533 int width, int height) { 534 535 MPPointF drawOffset = MPPointF.getInstance(); 536 drawOffset.x = x - (width / 2); 537 drawOffset.y = y - (height / 2); 538 539 drawable.copyBounds(mDrawableBoundsCache); 540 drawable.setBounds( 541 mDrawableBoundsCache.left, 542 mDrawableBoundsCache.top, 543 mDrawableBoundsCache.left + width, 544 mDrawableBoundsCache.top + width); 545 546 int saveId = canvas.save(); 547 // translate to the correct position and draw 548 canvas.translate(drawOffset.x, drawOffset.y); 549 drawable.draw(canvas); 550 canvas.restoreToCount(saveId); 551 } 552 553 private static Rect mDrawTextRectBuffer = new Rect(); 554 private static Paint.FontMetrics mFontMetricsBuffer = new Paint.FontMetrics(); 555 556 public static void drawXAxisValue(Canvas c, String text, float x, float y, 557 Paint paint, 558 MPPointF anchor, float angleDegrees) { 559 560 float drawOffsetX = 0.f; 561 float drawOffsetY = 0.f; 562 563 final float lineHeight = paint.getFontMetrics(mFontMetricsBuffer); 564 paint.getTextBounds(text, 0, text.length(), mDrawTextRectBuffer); 565 566 // Android sometimes has pre-padding 567 drawOffsetX -= mDrawTextRectBuffer.left; 568 569 // Android does not snap the bounds to line boundaries, 570 // and draws from bottom to top. 571 // And we want to normalize it. 572 drawOffsetY += -mFontMetricsBuffer.ascent; 573 574 // To have a consistent point of reference, we always draw left-aligned 575 Paint.Align originalTextAlign = paint.getTextAlign(); 576 paint.setTextAlign(Paint.Align.LEFT); 577 578 if (angleDegrees != 0.f) { 579 580 // Move the text drawing rect in a way that it always rotates around its center 581 drawOffsetX -= mDrawTextRectBuffer.width() * 0.5f; 582 drawOffsetY -= lineHeight * 0.5f; 583 584 float translateX = x; 585 float translateY = y; 586 587 // Move the "outer" rect relative to the anchor, assuming its centered 588 if (anchor.x != 0.5f || anchor.y != 0.5f) { 589 final FSize rotatedSize = getSizeOfRotatedRectangleByDegrees( 590 mDrawTextRectBuffer.width(), 591 lineHeight, 592 angleDegrees); 593 594 translateX -= rotatedSize.width * (anchor.x - 0.5f); 595 translateY -= rotatedSize.height * (anchor.y - 0.5f); 596 FSize.recycleInstance(rotatedSize); 597 } 598 599 c.save(); 600 c.translate(translateX, translateY); 601 c.rotate(angleDegrees); 602 603 c.drawText(text, drawOffsetX, drawOffsetY, paint); 604 605 c.restore(); 606 } else { 607 if (anchor.x != 0.f || anchor.y != 0.f) { 608 609 drawOffsetX -= mDrawTextRectBuffer.width() * anchor.x; 610 drawOffsetY -= lineHeight * anchor.y; 611 } 612 613 drawOffsetX += x; 614 drawOffsetY += y; 615 616 c.drawText(text, drawOffsetX, drawOffsetY, paint); 617 } 618 619 paint.setTextAlign(originalTextAlign); 620 } 621 622 public static void drawMultilineText(Canvas c, StaticLayout textLayout, 623 float x, float y, 624 TextPaint paint, 625 MPPointF anchor, float angleDegrees) { 626 627 float drawOffsetX = 0.f; 628 float drawOffsetY = 0.f; 629 float drawWidth; 630 float drawHeight; 631 632 final float lineHeight = paint.getFontMetrics(mFontMetricsBuffer); 633 634 drawWidth = textLayout.getWidth(); 635 drawHeight = textLayout.getLineCount() * lineHeight; 636 637 // Android sometimes has pre-padding 638 drawOffsetX -= mDrawTextRectBuffer.left; 639 640 // Android does not snap the bounds to line boundaries, 641 // and draws from bottom to top. 642 // And we want to normalize it. 643 drawOffsetY += drawHeight; 644 645 // To have a consistent point of reference, we always draw left-aligned 646 Paint.Align originalTextAlign = paint.getTextAlign(); 647 paint.setTextAlign(Paint.Align.LEFT); 648 649 if (angleDegrees != 0.f) { 650 651 // Move the text drawing rect in a way that it always rotates around its center 652 drawOffsetX -= drawWidth * 0.5f; 653 drawOffsetY -= drawHeight * 0.5f; 654 655 float translateX = x; 656 float translateY = y; 657 658 // Move the "outer" rect relative to the anchor, assuming its centered 659 if (anchor.x != 0.5f || anchor.y != 0.5f) { 660 final FSize rotatedSize = getSizeOfRotatedRectangleByDegrees( 661 drawWidth, 662 drawHeight, 663 angleDegrees); 664 665 translateX -= rotatedSize.width * (anchor.x - 0.5f); 666 translateY -= rotatedSize.height * (anchor.y - 0.5f); 667 FSize.recycleInstance(rotatedSize); 668 } 669 670 c.save(); 671 c.translate(translateX, translateY); 672 c.rotate(angleDegrees); 673 674 c.translate(drawOffsetX, drawOffsetY); 675 textLayout.draw(c); 676 677 c.restore(); 678 } else { 679 if (anchor.x != 0.f || anchor.y != 0.f) { 680 681 drawOffsetX -= drawWidth * anchor.x; 682 drawOffsetY -= drawHeight * anchor.y; 683 } 684 685 drawOffsetX += x; 686 drawOffsetY += y; 687 688 c.save(); 689 690 c.translate(drawOffsetX, drawOffsetY); 691 textLayout.draw(c); 692 693 c.restore(); 694 } 695 696 paint.setTextAlign(originalTextAlign); 697 } 698 699 public static void drawMultilineText(Canvas c, String text, 700 float x, float y, 701 TextPaint paint, 702 FSize constrainedToSize, 703 MPPointF anchor, float angleDegrees) { 704 705 StaticLayout textLayout = new StaticLayout( 706 text, 0, text.length(), 707 paint, 708 (int) Math.max(Math.ceil(constrainedToSize.width), 1.f), 709 Layout.Alignment.ALIGN_NORMAL, 1.f, 0.f, false); 710 711 712 drawMultilineText(c, textLayout, x, y, paint, anchor, angleDegrees); 713 } 714 715 /** 716 * Returns a recyclable FSize instance. 717 * Represents size of a rotated rectangle by degrees. 718 * 719 * @param rectangleSize 720 * @param degrees 721 * @return A Recyclable FSize instance 722 */ 723 public static FSize getSizeOfRotatedRectangleByDegrees(FSize rectangleSize, float degrees) { 724 final float radians = degrees * FDEG2RAD; 725 return getSizeOfRotatedRectangleByRadians(rectangleSize.width, rectangleSize.height, 726 radians); 727 } 728 729 /** 730 * Returns a recyclable FSize instance. 731 * Represents size of a rotated rectangle by radians. 732 * 733 * @param rectangleSize 734 * @param radians 735 * @return A Recyclable FSize instance 736 */ 737 public static FSize getSizeOfRotatedRectangleByRadians(FSize rectangleSize, float radians) { 738 return getSizeOfRotatedRectangleByRadians(rectangleSize.width, rectangleSize.height, 739 radians); 740 } 741 742 /** 743 * Returns a recyclable FSize instance. 744 * Represents size of a rotated rectangle by degrees. 745 * 746 * @param rectangleWidth 747 * @param rectangleHeight 748 * @param degrees 749 * @return A Recyclable FSize instance 750 */ 751 public static FSize getSizeOfRotatedRectangleByDegrees(float rectangleWidth, float 752 rectangleHeight, float degrees) { 753 final float radians = degrees * FDEG2RAD; 754 return getSizeOfRotatedRectangleByRadians(rectangleWidth, rectangleHeight, radians); 755 } 756 757 /** 758 * Returns a recyclable FSize instance. 759 * Represents size of a rotated rectangle by radians. 760 * 761 * @param rectangleWidth 762 * @param rectangleHeight 763 * @param radians 764 * @return A Recyclable FSize instance 765 */ 766 public static FSize getSizeOfRotatedRectangleByRadians(float rectangleWidth, float 767 rectangleHeight, float radians) { 768 return FSize.getInstance( 769 Math.abs(rectangleWidth * (float) Math.cos(radians)) + Math.abs(rectangleHeight * 770 (float) Math.sin(radians)), 771 Math.abs(rectangleWidth * (float) Math.sin(radians)) + Math.abs(rectangleHeight * 772 (float) Math.cos(radians)) 773 ); 774 } 775 776 public static int getSDKInt() { 777 return android.os.Build.VERSION.SDK_INT; 778 } 779 } 780