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