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