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