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.internal.util.Preconditions;
39 import 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  */
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     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 {
onSweep(ChartSweepView sweep, boolean sweepDone)101         public void onSweep(ChartSweepView sweep, boolean sweepDone);
requestEdit(ChartSweepView sweep)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 
ChartSweepView(Context context)112     public ChartSweepView(Context context) {
113         this(context, null);
114     }
115 
ChartSweepView(Context context, AttributeSet attrs)116     public ChartSweepView(Context context, AttributeSet attrs) {
117         this(context, attrs, 0);
118     }
119 
ChartSweepView(Context context, AttributeSet attrs, int defStyle)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 
init(ChartAxis axis)157     void init(ChartAxis axis) {
158         mAxis = Preconditions.checkNotNull(axis, "missing axis");
159     }
160 
setNeighbors(ChartSweepView... neighbors)161     public void setNeighbors(ChartSweepView... neighbors) {
162         mNeighbors = neighbors;
163     }
164 
getFollowAxis()165     public int getFollowAxis() {
166         return mFollowAxis;
167     }
168 
getMargins()169     public Rect getMargins() {
170         return mMargins;
171     }
172 
setDragInterval(long dragInterval)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      */
getTargetInset()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 
addOnSweepListener(OnSweepListener listener)193     public void addOnSweepListener(OnSweepListener listener) {
194         mListener = listener;
195     }
196 
dispatchOnSweep(boolean sweepDone)197     private void dispatchOnSweep(boolean sweepDone) {
198         if (mListener != null) {
199             mListener.onSweep(this, sweepDone);
200         }
201     }
202 
dispatchRequestEdit()203     private void dispatchRequestEdit() {
204         if (mListener != null) {
205             mListener.requestEdit(this);
206         }
207     }
208 
209     @Override
setEnabled(boolean enabled)210     public void setEnabled(boolean enabled) {
211         super.setEnabled(enabled);
212         setFocusable(enabled);
213         requestLayout();
214     }
215 
setSweepDrawable(Drawable sweep)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 
setFollowAxis(int followAxis)237     public void setFollowAxis(int followAxis) {
238         mFollowAxis = followAxis;
239     }
240 
setLabelMinSize(int minSize)241     public void setLabelMinSize(int minSize) {
242         mLabelMinSize = minSize;
243         invalidateLabelTemplate();
244     }
245 
setLabelTemplate(int resId)246     public void setLabelTemplate(int resId) {
247         mLabelTemplateRes = resId;
248         invalidateLabelTemplate();
249     }
250 
setLabelColor(int color)251     public void setLabelColor(int color) {
252         mLabelColor = color;
253         invalidateLabelTemplate();
254     }
255 
invalidateLabelTemplate()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 
invalidateLabel()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      */
invalidateLabelOffset()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
jumpDrawablesToCurrentState()328     public void jumpDrawablesToCurrentState() {
329         super.jumpDrawablesToCurrentState();
330         if (mSweep != null) {
331             mSweep.jumpToCurrentState();
332         }
333     }
334 
335     @Override
setVisibility(int visibility)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
verifyDrawable(Drawable who)344     protected boolean verifyDrawable(Drawable who) {
345         return who == mSweep || super.verifyDrawable(who);
346     }
347 
getAxis()348     public ChartAxis getAxis() {
349         return mAxis;
350     }
351 
setValue(long value)352     public void setValue(long value) {
353         mValue = value;
354         invalidateLabel();
355     }
356 
getValue()357     public long getValue() {
358         return mValue;
359     }
360 
getLabelValue()361     public long getLabelValue() {
362         return mLabelValue;
363     }
364 
getPoint()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      */
setValidRange(long validAfter, long validBefore)378     public void setValidRange(long validAfter, long validBefore) {
379         mValidAfter = validAfter;
380         mValidBefore = validBefore;
381     }
382 
setNeighborMargin(float neighborMargin)383     public void setNeighborMargin(float neighborMargin) {
384         mNeighborMargin = neighborMargin;
385     }
386 
setSafeRegion(int safeRegion)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      */
setValidRangeDynamic(ChartSweepView validAfter, ChartSweepView validBefore)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      */
isTouchCloserTo(MotionEvent eventInParent, ChartSweepView another)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 
getTouchDistanceFromTarget(MotionEvent eventInParent)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
onTouchEvent(MotionEvent event)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