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