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