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