• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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