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