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