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