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