ChartSweepView.java revision e6c5003278184c202833209164ddf1ae8c083f12
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
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 {
100        public void onSweep(ChartSweepView sweep, boolean sweepDone);
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
111    public ChartSweepView(Context context) {
112        this(context, null);
113    }
114
115    public ChartSweepView(Context context, AttributeSet attrs) {
116        this(context, attrs, 0);
117    }
118
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
155    void init(ChartAxis axis) {
156        mAxis = Preconditions.checkNotNull(axis, "missing axis");
157    }
158
159    public void setNeighbors(ChartSweepView... neighbors) {
160        mNeighbors = neighbors;
161    }
162
163    public int getFollowAxis() {
164        return mFollowAxis;
165    }
166
167    public Rect getMargins() {
168        return mMargins;
169    }
170
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     */
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
191    public void addOnSweepListener(OnSweepListener listener) {
192        mListener = listener;
193    }
194
195    private void dispatchOnSweep(boolean sweepDone) {
196        if (mListener != null) {
197            mListener.onSweep(this, sweepDone);
198        }
199    }
200
201    private void dispatchRequestEdit() {
202        if (mListener != null) {
203            mListener.requestEdit(this);
204        }
205    }
206
207    @Override
208    public void setEnabled(boolean enabled) {
209        super.setEnabled(enabled);
210        setFocusable(enabled);
211        requestLayout();
212    }
213
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
235    public void setFollowAxis(int followAxis) {
236        mFollowAxis = followAxis;
237    }
238
239    public void setLabelMinSize(int minSize) {
240        mLabelMinSize = minSize;
241        invalidateLabelTemplate();
242    }
243
244    public void setLabelTemplate(int resId) {
245        mLabelTemplateRes = resId;
246        invalidateLabelTemplate();
247    }
248
249    public void setLabelColor(int color) {
250        mLabelColor = color;
251        invalidateLabelTemplate();
252    }
253
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
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     */
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
327    public void jumpDrawablesToCurrentState() {
328        super.jumpDrawablesToCurrentState();
329        if (mSweep != null) {
330            mSweep.jumpToCurrentState();
331        }
332    }
333
334    @Override
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
343    protected boolean verifyDrawable(Drawable who) {
344        return who == mSweep || super.verifyDrawable(who);
345    }
346
347    public ChartAxis getAxis() {
348        return mAxis;
349    }
350
351    public void setValue(long value) {
352        mValue = value;
353        invalidateLabel();
354    }
355
356    public long getValue() {
357        return mValue;
358    }
359
360    public long getLabelValue() {
361        return mLabelValue;
362    }
363
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     */
377    public void setValidRange(long validAfter, long validBefore) {
378        mValidAfter = validAfter;
379        mValidBefore = validBefore;
380    }
381
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     */
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     */
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
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
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