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