1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.widget; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.graphics.Canvas; 22 import android.graphics.Color; 23 import android.graphics.Paint; 24 import android.graphics.Paint.Style; 25 import android.graphics.Point; 26 import android.graphics.Rect; 27 import android.graphics.drawable.Drawable; 28 import android.text.DynamicLayout; 29 import android.text.Layout.Alignment; 30 import android.text.SpannableStringBuilder; 31 import android.text.TextPaint; 32 import android.util.AttributeSet; 33 import android.util.MathUtils; 34 import android.view.MotionEvent; 35 import android.view.View; 36 37 import com.android.settings.R; 38 import com.google.common.base.Preconditions; 39 40 /** 41 * Sweep across a {@link ChartView} at a specific {@link ChartAxis} value, which 42 * a user can drag. 43 */ 44 public class ChartSweepView extends View { 45 46 private static final boolean DRAW_OUTLINE = false; 47 48 // TODO: clean up all the various padding/offset/margins 49 50 private Drawable mSweep; 51 private Rect mSweepPadding = new Rect(); 52 53 /** Offset of content inside this view. */ 54 private Rect mContentOffset = new Rect(); 55 /** Offset of {@link #mSweep} inside this view. */ 56 private Point mSweepOffset = new Point(); 57 58 private Rect mMargins = new Rect(); 59 private float mNeighborMargin; 60 61 private int mFollowAxis; 62 63 private int mLabelSize; 64 private int mLabelTemplateRes; 65 private int mLabelColor; 66 67 private SpannableStringBuilder mLabelTemplate; 68 private DynamicLayout mLabelLayout; 69 70 private ChartAxis mAxis; 71 private long mValue; 72 private long mLabelValue; 73 74 private long mValidAfter; 75 private long mValidBefore; 76 private ChartSweepView mValidAfterDynamic; 77 private ChartSweepView mValidBeforeDynamic; 78 79 private float mLabelOffset; 80 81 private Paint mOutlinePaint = new Paint(); 82 83 public static final int HORIZONTAL = 0; 84 public static final int VERTICAL = 1; 85 86 private int mTouchMode = MODE_NONE; 87 88 private static final int MODE_NONE = 0; 89 private static final int MODE_DRAG = 1; 90 private static final int MODE_LABEL = 2; 91 92 private long mDragInterval = 1; 93 94 public interface OnSweepListener { 95 public void onSweep(ChartSweepView sweep, boolean sweepDone); 96 public void requestEdit(ChartSweepView sweep); 97 } 98 99 private OnSweepListener mListener; 100 101 private float mTrackingStart; 102 private MotionEvent mTracking; 103 104 private ChartSweepView[] mNeighbors = new ChartSweepView[0]; 105 106 public ChartSweepView(Context context) { 107 this(context, null); 108 } 109 110 public ChartSweepView(Context context, AttributeSet attrs) { 111 this(context, attrs, 0); 112 } 113 114 public ChartSweepView(Context context, AttributeSet attrs, int defStyle) { 115 super(context, attrs, defStyle); 116 117 final TypedArray a = context.obtainStyledAttributes( 118 attrs, R.styleable.ChartSweepView, defStyle, 0); 119 120 setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable)); 121 setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1)); 122 setNeighborMargin(a.getDimensionPixelSize(R.styleable.ChartSweepView_neighborMargin, 0)); 123 124 setLabelSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0)); 125 setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0)); 126 setLabelColor(a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE)); 127 128 // TODO: moved focused state directly into assets 129 setBackgroundResource(R.drawable.data_usage_sweep_background); 130 131 mOutlinePaint.setColor(Color.RED); 132 mOutlinePaint.setStrokeWidth(1f); 133 mOutlinePaint.setStyle(Style.STROKE); 134 135 a.recycle(); 136 137 setClickable(true); 138 setFocusable(true); 139 setOnClickListener(mClickListener); 140 141 setWillNotDraw(false); 142 } 143 144 private OnClickListener mClickListener = new OnClickListener() { 145 public void onClick(View v) { 146 dispatchRequestEdit(); 147 } 148 }; 149 150 void init(ChartAxis axis) { 151 mAxis = Preconditions.checkNotNull(axis, "missing axis"); 152 } 153 154 public void setNeighbors(ChartSweepView... neighbors) { 155 mNeighbors = neighbors; 156 } 157 158 public int getFollowAxis() { 159 return mFollowAxis; 160 } 161 162 public Rect getMargins() { 163 return mMargins; 164 } 165 166 public void setDragInterval(long dragInterval) { 167 mDragInterval = dragInterval; 168 } 169 170 /** 171 * Return the number of pixels that the "target" area is inset from the 172 * {@link View} edge, along the current {@link #setFollowAxis(int)}. 173 */ 174 private float getTargetInset() { 175 if (mFollowAxis == VERTICAL) { 176 final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top 177 - mSweepPadding.bottom; 178 return mSweepPadding.top + (targetHeight / 2) + mSweepOffset.y; 179 } else { 180 final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left 181 - mSweepPadding.right; 182 return mSweepPadding.left + (targetWidth / 2) + mSweepOffset.x; 183 } 184 } 185 186 public void addOnSweepListener(OnSweepListener listener) { 187 mListener = listener; 188 } 189 190 private void dispatchOnSweep(boolean sweepDone) { 191 if (mListener != null) { 192 mListener.onSweep(this, sweepDone); 193 } 194 } 195 196 private void dispatchRequestEdit() { 197 if (mListener != null) { 198 mListener.requestEdit(this); 199 } 200 } 201 202 @Override 203 public void setEnabled(boolean enabled) { 204 super.setEnabled(enabled); 205 setFocusable(enabled); 206 requestLayout(); 207 } 208 209 public void setSweepDrawable(Drawable sweep) { 210 if (mSweep != null) { 211 mSweep.setCallback(null); 212 unscheduleDrawable(mSweep); 213 } 214 215 if (sweep != null) { 216 sweep.setCallback(this); 217 if (sweep.isStateful()) { 218 sweep.setState(getDrawableState()); 219 } 220 sweep.setVisible(getVisibility() == VISIBLE, false); 221 mSweep = sweep; 222 sweep.getPadding(mSweepPadding); 223 } else { 224 mSweep = null; 225 } 226 227 invalidate(); 228 } 229 230 public void setFollowAxis(int followAxis) { 231 mFollowAxis = followAxis; 232 } 233 234 public void setLabelSize(int size) { 235 mLabelSize = size; 236 invalidateLabelTemplate(); 237 } 238 239 public void setLabelTemplate(int resId) { 240 mLabelTemplateRes = resId; 241 invalidateLabelTemplate(); 242 } 243 244 public void setLabelColor(int color) { 245 mLabelColor = color; 246 invalidateLabelTemplate(); 247 } 248 249 private void invalidateLabelTemplate() { 250 if (mLabelTemplateRes != 0) { 251 final CharSequence template = getResources().getText(mLabelTemplateRes); 252 253 final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG); 254 paint.density = getResources().getDisplayMetrics().density; 255 paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale); 256 paint.setColor(mLabelColor); 257 paint.setShadowLayer(4 * paint.density, 0, 0, Color.BLACK); 258 259 mLabelTemplate = new SpannableStringBuilder(template); 260 mLabelLayout = new DynamicLayout( 261 mLabelTemplate, paint, mLabelSize, Alignment.ALIGN_RIGHT, 1f, 0f, false); 262 invalidateLabel(); 263 264 } else { 265 mLabelTemplate = null; 266 mLabelLayout = null; 267 } 268 269 invalidate(); 270 requestLayout(); 271 } 272 273 private void invalidateLabel() { 274 if (mLabelTemplate != null && mAxis != null) { 275 mLabelValue = mAxis.buildLabel(getResources(), mLabelTemplate, mValue); 276 setContentDescription(mLabelTemplate); 277 invalidateLabelOffset(); 278 invalidate(); 279 } else { 280 mLabelValue = mValue; 281 } 282 } 283 284 /** 285 * When overlapping with neighbor, split difference and push label. 286 */ 287 public void invalidateLabelOffset() { 288 float margin; 289 float labelOffset = 0; 290 if (mFollowAxis == VERTICAL) { 291 if (mValidAfterDynamic != null) { 292 margin = getLabelTop(mValidAfterDynamic) - getLabelBottom(this); 293 if (margin < 0) { 294 labelOffset = margin / 2; 295 } 296 } else if (mValidBeforeDynamic != null) { 297 margin = getLabelTop(this) - getLabelBottom(mValidBeforeDynamic); 298 if (margin < 0) { 299 labelOffset = -margin / 2; 300 } 301 } 302 } else { 303 // TODO: implement horizontal labels 304 } 305 306 // when offsetting label, neighbor probably needs to offset too 307 if (labelOffset != mLabelOffset) { 308 mLabelOffset = labelOffset; 309 invalidate(); 310 if (mValidAfterDynamic != null) mValidAfterDynamic.invalidateLabelOffset(); 311 if (mValidBeforeDynamic != null) mValidBeforeDynamic.invalidateLabelOffset(); 312 } 313 } 314 315 @Override 316 public void jumpDrawablesToCurrentState() { 317 super.jumpDrawablesToCurrentState(); 318 if (mSweep != null) { 319 mSweep.jumpToCurrentState(); 320 } 321 } 322 323 @Override 324 public void setVisibility(int visibility) { 325 super.setVisibility(visibility); 326 if (mSweep != null) { 327 mSweep.setVisible(visibility == VISIBLE, false); 328 } 329 } 330 331 @Override 332 protected boolean verifyDrawable(Drawable who) { 333 return who == mSweep || super.verifyDrawable(who); 334 } 335 336 public ChartAxis getAxis() { 337 return mAxis; 338 } 339 340 public void setValue(long value) { 341 mValue = value; 342 invalidateLabel(); 343 } 344 345 public long getValue() { 346 return mValue; 347 } 348 349 public long getLabelValue() { 350 return mLabelValue; 351 } 352 353 public float getPoint() { 354 if (isEnabled()) { 355 return mAxis.convertToPoint(mValue); 356 } else { 357 // when disabled, show along top edge 358 return 0; 359 } 360 } 361 362 /** 363 * Set valid range this sweep can move within, in {@link #mAxis} values. The 364 * most restrictive combination of all valid ranges is used. 365 */ 366 public void setValidRange(long validAfter, long validBefore) { 367 mValidAfter = validAfter; 368 mValidBefore = validBefore; 369 } 370 371 public void setNeighborMargin(float neighborMargin) { 372 mNeighborMargin = neighborMargin; 373 } 374 375 /** 376 * Set valid range this sweep can move within, defined by the given 377 * {@link ChartSweepView}. The most restrictive combination of all valid 378 * ranges is used. 379 */ 380 public void setValidRangeDynamic(ChartSweepView validAfter, ChartSweepView validBefore) { 381 mValidAfterDynamic = validAfter; 382 mValidBeforeDynamic = validBefore; 383 } 384 385 /** 386 * Test if given {@link MotionEvent} is closer to another 387 * {@link ChartSweepView} compared to ourselves. 388 */ 389 public boolean isTouchCloserTo(MotionEvent eventInParent, ChartSweepView another) { 390 final float selfDist = getTouchDistanceFromTarget(eventInParent); 391 final float anotherDist = another.getTouchDistanceFromTarget(eventInParent); 392 return anotherDist < selfDist; 393 } 394 395 private float getTouchDistanceFromTarget(MotionEvent eventInParent) { 396 if (mFollowAxis == HORIZONTAL) { 397 return Math.abs(eventInParent.getX() - (getX() + getTargetInset())); 398 } else { 399 return Math.abs(eventInParent.getY() - (getY() + getTargetInset())); 400 } 401 } 402 403 @Override 404 public boolean onTouchEvent(MotionEvent event) { 405 if (!isEnabled()) return false; 406 407 final View parent = (View) getParent(); 408 switch (event.getAction()) { 409 case MotionEvent.ACTION_DOWN: { 410 411 // only start tracking when in sweet spot 412 final boolean acceptDrag; 413 final boolean acceptLabel; 414 if (mFollowAxis == VERTICAL) { 415 acceptDrag = event.getX() > getWidth() - (mSweepPadding.right * 8); 416 acceptLabel = mLabelLayout != null ? event.getX() < mLabelLayout.getWidth() 417 : false; 418 } else { 419 acceptDrag = event.getY() > getHeight() - (mSweepPadding.bottom * 8); 420 acceptLabel = mLabelLayout != null ? event.getY() < mLabelLayout.getHeight() 421 : false; 422 } 423 424 final MotionEvent eventInParent = event.copy(); 425 eventInParent.offsetLocation(getLeft(), getTop()); 426 427 // ignore event when closer to a neighbor 428 for (ChartSweepView neighbor : mNeighbors) { 429 if (isTouchCloserTo(eventInParent, neighbor)) { 430 return false; 431 } 432 } 433 434 if (acceptDrag) { 435 if (mFollowAxis == VERTICAL) { 436 mTrackingStart = getTop() - mMargins.top; 437 } else { 438 mTrackingStart = getLeft() - mMargins.left; 439 } 440 mTracking = event.copy(); 441 mTouchMode = MODE_DRAG; 442 443 // starting drag should activate entire chart 444 if (!parent.isActivated()) { 445 parent.setActivated(true); 446 } 447 448 return true; 449 } else if (acceptLabel) { 450 mTouchMode = MODE_LABEL; 451 return true; 452 } else { 453 mTouchMode = MODE_NONE; 454 return false; 455 } 456 } 457 case MotionEvent.ACTION_MOVE: { 458 if (mTouchMode == MODE_LABEL) { 459 return true; 460 } 461 462 getParent().requestDisallowInterceptTouchEvent(true); 463 464 // content area of parent 465 final Rect parentContent = getParentContentRect(); 466 final Rect clampRect = computeClampRect(parentContent); 467 if (clampRect.isEmpty()) return true; 468 469 long value; 470 if (mFollowAxis == VERTICAL) { 471 final float currentTargetY = getTop() - mMargins.top; 472 final float requestedTargetY = mTrackingStart 473 + (event.getRawY() - mTracking.getRawY()); 474 final float clampedTargetY = MathUtils.constrain( 475 requestedTargetY, clampRect.top, clampRect.bottom); 476 setTranslationY(clampedTargetY - currentTargetY); 477 478 value = mAxis.convertToValue(clampedTargetY - parentContent.top); 479 } else { 480 final float currentTargetX = getLeft() - mMargins.left; 481 final float requestedTargetX = mTrackingStart 482 + (event.getRawX() - mTracking.getRawX()); 483 final float clampedTargetX = MathUtils.constrain( 484 requestedTargetX, clampRect.left, clampRect.right); 485 setTranslationX(clampedTargetX - currentTargetX); 486 487 value = mAxis.convertToValue(clampedTargetX - parentContent.left); 488 } 489 490 // round value from drag to nearest increment 491 value -= value % mDragInterval; 492 setValue(value); 493 494 dispatchOnSweep(false); 495 return true; 496 } 497 case MotionEvent.ACTION_UP: { 498 if (mTouchMode == MODE_LABEL) { 499 performClick(); 500 } else if (mTouchMode == MODE_DRAG) { 501 mTrackingStart = 0; 502 mTracking = null; 503 mValue = mLabelValue; 504 dispatchOnSweep(true); 505 setTranslationX(0); 506 setTranslationY(0); 507 requestLayout(); 508 } 509 510 mTouchMode = MODE_NONE; 511 return true; 512 } 513 default: { 514 return false; 515 } 516 } 517 } 518 519 /** 520 * Update {@link #mValue} based on current position, including any 521 * {@link #onTouchEvent(MotionEvent)} in progress. Typically used when 522 * {@link ChartAxis} changes during sweep adjustment. 523 */ 524 public void updateValueFromPosition() { 525 final Rect parentContent = getParentContentRect(); 526 if (mFollowAxis == VERTICAL) { 527 final float effectiveY = getY() - mMargins.top - parentContent.top; 528 setValue(mAxis.convertToValue(effectiveY)); 529 } else { 530 final float effectiveX = getX() - mMargins.left - parentContent.left; 531 setValue(mAxis.convertToValue(effectiveX)); 532 } 533 } 534 535 public int shouldAdjustAxis() { 536 return mAxis.shouldAdjustAxis(getValue()); 537 } 538 539 private Rect getParentContentRect() { 540 final View parent = (View) getParent(); 541 return new Rect(parent.getPaddingLeft(), parent.getPaddingTop(), 542 parent.getWidth() - parent.getPaddingRight(), 543 parent.getHeight() - parent.getPaddingBottom()); 544 } 545 546 @Override 547 public void addOnLayoutChangeListener(OnLayoutChangeListener listener) { 548 // ignored to keep LayoutTransition from animating us 549 } 550 551 @Override 552 public void removeOnLayoutChangeListener(OnLayoutChangeListener listener) { 553 // ignored to keep LayoutTransition from animating us 554 } 555 556 private long getValidAfterDynamic() { 557 final ChartSweepView dynamic = mValidAfterDynamic; 558 return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MIN_VALUE; 559 } 560 561 private long getValidBeforeDynamic() { 562 final ChartSweepView dynamic = mValidBeforeDynamic; 563 return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MAX_VALUE; 564 } 565 566 /** 567 * Compute {@link Rect} in {@link #getParent()} coordinates that we should 568 * be clamped inside of, usually from {@link #setValidRange(long, long)} 569 * style rules. 570 */ 571 private Rect computeClampRect(Rect parentContent) { 572 // create two rectangles, and pick most restrictive combination 573 final Rect rect = buildClampRect(parentContent, mValidAfter, mValidBefore, 0f); 574 final Rect dynamicRect = buildClampRect( 575 parentContent, getValidAfterDynamic(), getValidBeforeDynamic(), mNeighborMargin); 576 577 if (!rect.intersect(dynamicRect)) { 578 rect.setEmpty(); 579 } 580 return rect; 581 } 582 583 private Rect buildClampRect( 584 Rect parentContent, long afterValue, long beforeValue, float margin) { 585 if (mAxis instanceof InvertedChartAxis) { 586 long temp = beforeValue; 587 beforeValue = afterValue; 588 afterValue = temp; 589 } 590 591 final boolean afterValid = afterValue != Long.MIN_VALUE && afterValue != Long.MAX_VALUE; 592 final boolean beforeValid = beforeValue != Long.MIN_VALUE && beforeValue != Long.MAX_VALUE; 593 594 final float afterPoint = mAxis.convertToPoint(afterValue) + margin; 595 final float beforePoint = mAxis.convertToPoint(beforeValue) - margin; 596 597 final Rect clampRect = new Rect(parentContent); 598 if (mFollowAxis == VERTICAL) { 599 if (beforeValid) clampRect.bottom = clampRect.top + (int) beforePoint; 600 if (afterValid) clampRect.top += afterPoint; 601 } else { 602 if (beforeValid) clampRect.right = clampRect.left + (int) beforePoint; 603 if (afterValid) clampRect.left += afterPoint; 604 } 605 return clampRect; 606 } 607 608 @Override 609 protected void drawableStateChanged() { 610 super.drawableStateChanged(); 611 if (mSweep.isStateful()) { 612 mSweep.setState(getDrawableState()); 613 } 614 } 615 616 @Override 617 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 618 619 // TODO: handle vertical labels 620 if (isEnabled() && mLabelLayout != null) { 621 final int sweepHeight = mSweep.getIntrinsicHeight(); 622 final int templateHeight = mLabelLayout.getHeight(); 623 624 mSweepOffset.x = 0; 625 mSweepOffset.y = 0; 626 mSweepOffset.y = (int) ((templateHeight / 2) - getTargetInset()); 627 setMeasuredDimension(mSweep.getIntrinsicWidth(), Math.max(sweepHeight, templateHeight)); 628 629 } else { 630 mSweepOffset.x = 0; 631 mSweepOffset.y = 0; 632 setMeasuredDimension(mSweep.getIntrinsicWidth(), mSweep.getIntrinsicHeight()); 633 } 634 635 if (mFollowAxis == VERTICAL) { 636 final int targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top 637 - mSweepPadding.bottom; 638 mMargins.top = -(mSweepPadding.top + (targetHeight / 2)); 639 mMargins.bottom = 0; 640 mMargins.left = -mSweepPadding.left; 641 mMargins.right = mSweepPadding.right; 642 } else { 643 final int targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left 644 - mSweepPadding.right; 645 mMargins.left = -(mSweepPadding.left + (targetWidth / 2)); 646 mMargins.right = 0; 647 mMargins.top = -mSweepPadding.top; 648 mMargins.bottom = mSweepPadding.bottom; 649 } 650 651 mContentOffset.set(0, 0, 0, 0); 652 653 // make touch target area larger 654 final int widthBefore = getMeasuredWidth(); 655 final int heightBefore = getMeasuredHeight(); 656 if (mFollowAxis == HORIZONTAL) { 657 final int widthAfter = widthBefore * 3; 658 setMeasuredDimension(widthAfter, heightBefore); 659 mContentOffset.left = (widthAfter - widthBefore) / 2; 660 661 final int offset = mSweepPadding.bottom * 2; 662 mContentOffset.bottom -= offset; 663 mMargins.bottom += offset; 664 } else { 665 final int heightAfter = heightBefore * 2; 666 setMeasuredDimension(widthBefore, heightAfter); 667 mContentOffset.offset(0, (heightAfter - heightBefore) / 2); 668 669 final int offset = mSweepPadding.right * 2; 670 mContentOffset.right -= offset; 671 mMargins.right += offset; 672 } 673 674 mSweepOffset.offset(mContentOffset.left, mContentOffset.top); 675 mMargins.offset(-mSweepOffset.x, -mSweepOffset.y); 676 } 677 678 @Override 679 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 680 super.onLayout(changed, left, top, right, bottom); 681 invalidateLabelOffset(); 682 } 683 684 @Override 685 protected void onDraw(Canvas canvas) { 686 super.onDraw(canvas); 687 688 final int width = getWidth(); 689 final int height = getHeight(); 690 691 final int labelSize; 692 if (isEnabled() && mLabelLayout != null) { 693 final int count = canvas.save(); 694 { 695 canvas.translate(mContentOffset.left, mContentOffset.top + mLabelOffset); 696 mLabelLayout.draw(canvas); 697 } 698 canvas.restoreToCount(count); 699 labelSize = mLabelSize; 700 } else { 701 labelSize = 0; 702 } 703 704 if (mFollowAxis == VERTICAL) { 705 mSweep.setBounds(labelSize, mSweepOffset.y, width + mContentOffset.right, 706 mSweepOffset.y + mSweep.getIntrinsicHeight()); 707 } else { 708 mSweep.setBounds(mSweepOffset.x, labelSize, mSweepOffset.x + mSweep.getIntrinsicWidth(), 709 height + mContentOffset.bottom); 710 } 711 712 mSweep.draw(canvas); 713 714 if (DRAW_OUTLINE) { 715 mOutlinePaint.setColor(Color.RED); 716 canvas.drawRect(0, 0, width, height, mOutlinePaint); 717 } 718 } 719 720 public static float getLabelTop(ChartSweepView view) { 721 return view.getY() + view.mContentOffset.top; 722 } 723 724 public static float getLabelBottom(ChartSweepView view) { 725 return getLabelTop(view) + view.mLabelLayout.getHeight(); 726 } 727 } 728