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