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
17package com.android.settings.widget;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.Paint.Style;
25import android.graphics.Point;
26import android.graphics.Rect;
27import android.graphics.drawable.Drawable;
28import android.text.DynamicLayout;
29import android.text.Layout;
30import android.text.Layout.Alignment;
31import android.text.SpannableStringBuilder;
32import android.text.TextPaint;
33import android.util.AttributeSet;
34import android.util.MathUtils;
35import android.view.MotionEvent;
36import android.view.View;
37
38import com.android.internal.util.Preconditions;
39import 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 */
45public class ChartSweepView extends View {
46
47    private static final boolean DRAW_OUTLINE = false;
48
49    // TODO: clean up all the various padding/offset/margins
50
51    private Drawable mSweep;
52    private Rect mSweepPadding = new Rect();
53
54    /** Offset of content inside this view. */
55    private Rect mContentOffset = new Rect();
56    /** Offset of {@link #mSweep} inside this view. */
57    private Point mSweepOffset = new Point();
58
59    private Rect mMargins = new Rect();
60    private float mNeighborMargin;
61    private int mSafeRegion;
62
63    private int mFollowAxis;
64
65    private int mLabelMinSize;
66    private float mLabelSize;
67
68    private int mLabelTemplateRes;
69    private int mLabelColor;
70
71    private SpannableStringBuilder mLabelTemplate;
72    private DynamicLayout mLabelLayout;
73
74    private ChartAxis mAxis;
75    private long mValue;
76    private long mLabelValue;
77
78    private long mValidAfter;
79    private long mValidBefore;
80    private ChartSweepView mValidAfterDynamic;
81    private ChartSweepView mValidBeforeDynamic;
82
83    private float mLabelOffset;
84
85    private Paint mOutlinePaint = new Paint();
86
87    public static final int HORIZONTAL = 0;
88    public static final int VERTICAL = 1;
89
90    private int mTouchMode = MODE_NONE;
91
92    private static final int MODE_NONE = 0;
93    private static final int MODE_DRAG = 1;
94    private static final int MODE_LABEL = 2;
95
96    private static final int LARGE_WIDTH = 1024;
97
98    private long mDragInterval = 1;
99
100    public interface OnSweepListener {
101        public void onSweep(ChartSweepView sweep, boolean sweepDone);
102        public void requestEdit(ChartSweepView sweep);
103    }
104
105    private OnSweepListener mListener;
106
107    private float mTrackingStart;
108    private MotionEvent mTracking;
109
110    private ChartSweepView[] mNeighbors = new ChartSweepView[0];
111
112    public ChartSweepView(Context context) {
113        this(context, null);
114    }
115
116    public ChartSweepView(Context context, AttributeSet attrs) {
117        this(context, attrs, 0);
118    }
119
120    public ChartSweepView(Context context, AttributeSet attrs, int defStyle) {
121        super(context, attrs, defStyle);
122
123        final TypedArray a = context.obtainStyledAttributes(
124                attrs, R.styleable.ChartSweepView, defStyle, 0);
125
126        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
157    void init(ChartAxis axis) {
158        mAxis = Preconditions.checkNotNull(axis, "missing axis");
159    }
160
161    public void setNeighbors(ChartSweepView... neighbors) {
162        mNeighbors = neighbors;
163    }
164
165    public int getFollowAxis() {
166        return mFollowAxis;
167    }
168
169    public Rect getMargins() {
170        return mMargins;
171    }
172
173    public void setDragInterval(long dragInterval) {
174        mDragInterval = dragInterval;
175    }
176
177    /**
178     * Return the number of pixels that the "target" area is inset from the
179     * {@link View} edge, along the current {@link #setFollowAxis(int)}.
180     */
181    private float getTargetInset() {
182        if (mFollowAxis == VERTICAL) {
183            final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
184                    - mSweepPadding.bottom;
185            return mSweepPadding.top + (targetHeight / 2) + mSweepOffset.y;
186        } else {
187            final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
188                    - mSweepPadding.right;
189            return mSweepPadding.left + (targetWidth / 2) + mSweepOffset.x;
190        }
191    }
192
193    public void addOnSweepListener(OnSweepListener listener) {
194        mListener = listener;
195    }
196
197    private void dispatchOnSweep(boolean sweepDone) {
198        if (mListener != null) {
199            mListener.onSweep(this, sweepDone);
200        }
201    }
202
203    private void dispatchRequestEdit() {
204        if (mListener != null) {
205            mListener.requestEdit(this);
206        }
207    }
208
209    @Override
210    public void setEnabled(boolean enabled) {
211        super.setEnabled(enabled);
212        setFocusable(enabled);
213        requestLayout();
214    }
215
216    public void setSweepDrawable(Drawable sweep) {
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
237    public void setFollowAxis(int followAxis) {
238        mFollowAxis = followAxis;
239    }
240
241    public void setLabelMinSize(int minSize) {
242        mLabelMinSize = minSize;
243        invalidateLabelTemplate();
244    }
245
246    public void setLabelTemplate(int resId) {
247        mLabelTemplateRes = resId;
248        invalidateLabelTemplate();
249    }
250
251    public void setLabelColor(int color) {
252        mLabelColor = color;
253        invalidateLabelTemplate();
254    }
255
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
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     */
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
328    public void jumpDrawablesToCurrentState() {
329        super.jumpDrawablesToCurrentState();
330        if (mSweep != null) {
331            mSweep.jumpToCurrentState();
332        }
333    }
334
335    @Override
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
344    protected boolean verifyDrawable(Drawable who) {
345        return who == mSweep || super.verifyDrawable(who);
346    }
347
348    public ChartAxis getAxis() {
349        return mAxis;
350    }
351
352    public void setValue(long value) {
353        mValue = value;
354        invalidateLabel();
355    }
356
357    public long getValue() {
358        return mValue;
359    }
360
361    public long getLabelValue() {
362        return mLabelValue;
363    }
364
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     */
378    public void setValidRange(long validAfter, long validBefore) {
379        mValidAfter = validAfter;
380        mValidBefore = validBefore;
381    }
382
383    public void setNeighborMargin(float neighborMargin) {
384        mNeighborMargin = neighborMargin;
385    }
386
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     */
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     */
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
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
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