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