| /* | |
| * Copyright (C) 2011 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package com.android.settings.widget; | |
| import android.content.Context; | |
| import android.content.res.TypedArray; | |
| import android.graphics.Canvas; | |
| import android.graphics.Color; | |
| import android.graphics.Paint; | |
| import android.graphics.Paint.Style; | |
| import android.graphics.Point; | |
| import android.graphics.Rect; | |
| import android.graphics.drawable.Drawable; | |
| import android.text.DynamicLayout; | |
| import android.text.Layout; | |
| import android.text.Layout.Alignment; | |
| import android.text.SpannableStringBuilder; | |
| import android.text.TextPaint; | |
| import android.util.AttributeSet; | |
| import android.util.MathUtils; | |
| import android.view.MotionEvent; | |
| import android.view.View; | |
| import com.android.internal.util.Preconditions; | |
| import com.android.settings.R; | |
| /** | |
| * Sweep across a {@link ChartView} at a specific {@link ChartAxis} value, which | |
| * a user can drag. | |
| */ | |
| public class ChartSweepView extends View { | |
| private static final boolean DRAW_OUTLINE = false; | |
| // TODO: clean up all the various padding/offset/margins | |
| private Drawable mSweep; | |
| private Rect mSweepPadding = new Rect(); | |
| /** Offset of content inside this view. */ | |
| private Rect mContentOffset = new Rect(); | |
| /** Offset of {@link #mSweep} inside this view. */ | |
| private Point mSweepOffset = new Point(); | |
| private Rect mMargins = new Rect(); | |
| private float mNeighborMargin; | |
| private int mSafeRegion; | |
| private int mFollowAxis; | |
| private int mLabelMinSize; | |
| private float mLabelSize; | |
| private int mLabelTemplateRes; | |
| private int mLabelColor; | |
| private SpannableStringBuilder mLabelTemplate; | |
| private DynamicLayout mLabelLayout; | |
| private ChartAxis mAxis; | |
| private long mValue; | |
| private long mLabelValue; | |
| private long mValidAfter; | |
| private long mValidBefore; | |
| private ChartSweepView mValidAfterDynamic; | |
| private ChartSweepView mValidBeforeDynamic; | |
| private float mLabelOffset; | |
| private Paint mOutlinePaint = new Paint(); | |
| public static final int HORIZONTAL = 0; | |
| public static final int VERTICAL = 1; | |
| private int mTouchMode = MODE_NONE; | |
| private static final int MODE_NONE = 0; | |
| private static final int MODE_DRAG = 1; | |
| private static final int MODE_LABEL = 2; | |
| private static final int LARGE_WIDTH = 1024; | |
| private long mDragInterval = 1; | |
| public interface OnSweepListener { | |
| public void onSweep(ChartSweepView sweep, boolean sweepDone); | |
| public void requestEdit(ChartSweepView sweep); | |
| } | |
| private OnSweepListener mListener; | |
| private float mTrackingStart; | |
| private MotionEvent mTracking; | |
| private ChartSweepView[] mNeighbors = new ChartSweepView[0]; | |
| public ChartSweepView(Context context) { | |
| this(context, null); | |
| } | |
| public ChartSweepView(Context context, AttributeSet attrs) { | |
| this(context, attrs, 0); | |
| } | |
| public ChartSweepView(Context context, AttributeSet attrs, int defStyle) { | |
| super(context, attrs, defStyle); | |
| final TypedArray a = context.obtainStyledAttributes( | |
| attrs, R.styleable.ChartSweepView, defStyle, 0); | |
| final int color = a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE); | |
| setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable), color); | |
| setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1)); | |
| setNeighborMargin(a.getDimensionPixelSize(R.styleable.ChartSweepView_neighborMargin, 0)); | |
| setSafeRegion(a.getDimensionPixelSize(R.styleable.ChartSweepView_safeRegion, 0)); | |
| setLabelMinSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0)); | |
| setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0)); | |
| setLabelColor(color); | |
| // TODO: moved focused state directly into assets | |
| setBackgroundResource(R.drawable.data_usage_sweep_background); | |
| mOutlinePaint.setColor(Color.RED); | |
| mOutlinePaint.setStrokeWidth(1f); | |
| mOutlinePaint.setStyle(Style.STROKE); | |
| a.recycle(); | |
| setClickable(true); | |
| setOnClickListener(mClickListener); | |
| setWillNotDraw(false); | |
| } | |
| private OnClickListener mClickListener = new OnClickListener() { | |
| public void onClick(View v) { | |
| dispatchRequestEdit(); | |
| } | |
| }; | |
| void init(ChartAxis axis) { | |
| mAxis = Preconditions.checkNotNull(axis, "missing axis"); | |
| } | |
| public void setNeighbors(ChartSweepView... neighbors) { | |
| mNeighbors = neighbors; | |
| } | |
| public int getFollowAxis() { | |
| return mFollowAxis; | |
| } | |
| public Rect getMargins() { | |
| return mMargins; | |
| } | |
| public void setDragInterval(long dragInterval) { | |
| mDragInterval = dragInterval; | |
| } | |
| /** | |
| * Return the number of pixels that the "target" area is inset from the | |
| * {@link View} edge, along the current {@link #setFollowAxis(int)}. | |
| */ | |
| private float getTargetInset() { | |
| if (mFollowAxis == VERTICAL) { | |
| final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top | |
| - mSweepPadding.bottom; | |
| return mSweepPadding.top + (targetHeight / 2) + mSweepOffset.y; | |
| } else { | |
| final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left | |
| - mSweepPadding.right; | |
| return mSweepPadding.left + (targetWidth / 2) + mSweepOffset.x; | |
| } | |
| } | |
| public void addOnSweepListener(OnSweepListener listener) { | |
| mListener = listener; | |
| } | |
| private void dispatchOnSweep(boolean sweepDone) { | |
| if (mListener != null) { | |
| mListener.onSweep(this, sweepDone); | |
| } | |
| } | |
| private void dispatchRequestEdit() { | |
| if (mListener != null) { | |
| mListener.requestEdit(this); | |
| } | |
| } | |
| @Override | |
| public void setEnabled(boolean enabled) { | |
| super.setEnabled(enabled); | |
| setFocusable(enabled); | |
| requestLayout(); | |
| } | |
| public void setSweepDrawable(Drawable sweep, int color) { | |
| if (mSweep != null) { | |
| mSweep.setCallback(null); | |
| unscheduleDrawable(mSweep); | |
| } | |
| if (sweep != null) { | |
| sweep.setCallback(this); | |
| if (sweep.isStateful()) { | |
| sweep.setState(getDrawableState()); | |
| } | |
| sweep.setVisible(getVisibility() == VISIBLE, false); | |
| mSweep = sweep; | |
| // Match the text. | |
| mSweep.setTint(color); | |
| sweep.getPadding(mSweepPadding); | |
| } else { | |
| mSweep = null; | |
| } | |
| invalidate(); | |
| } | |
| public void setFollowAxis(int followAxis) { | |
| mFollowAxis = followAxis; | |
| } | |
| public void setLabelMinSize(int minSize) { | |
| mLabelMinSize = minSize; | |
| invalidateLabelTemplate(); | |
| } | |
| public void setLabelTemplate(int resId) { | |
| mLabelTemplateRes = resId; | |
| invalidateLabelTemplate(); | |
| } | |
| public void setLabelColor(int color) { | |
| mLabelColor = color; | |
| invalidateLabelTemplate(); | |
| } | |
| private void invalidateLabelTemplate() { | |
| if (mLabelTemplateRes != 0) { | |
| final CharSequence template = getResources().getText(mLabelTemplateRes); | |
| final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG); | |
| paint.density = getResources().getDisplayMetrics().density; | |
| paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale); | |
| paint.setColor(mLabelColor); | |
| mLabelTemplate = new SpannableStringBuilder(template); | |
| mLabelLayout = DynamicLayout.Builder.obtain(mLabelTemplate, paint, LARGE_WIDTH) | |
| .setAlignment(Alignment.ALIGN_RIGHT) | |
| .setIncludePad(false) | |
| .setUseLineSpacingFromFallbacks(true) | |
| .build(); | |
| invalidateLabel(); | |
| } else { | |
| mLabelTemplate = null; | |
| mLabelLayout = null; | |
| } | |
| invalidate(); | |
| requestLayout(); | |
| } | |
| private void invalidateLabel() { | |
| if (mLabelTemplate != null && mAxis != null) { | |
| mLabelValue = mAxis.buildLabel(getResources(), mLabelTemplate, mValue); | |
| setContentDescription(mLabelTemplate); | |
| invalidateLabelOffset(); | |
| invalidate(); | |
| } else { | |
| mLabelValue = mValue; | |
| } | |
| } | |
| /** | |
| * When overlapping with neighbor, split difference and push label. | |
| */ | |
| public void invalidateLabelOffset() { | |
| float margin; | |
| float labelOffset = 0; | |
| if (mFollowAxis == VERTICAL) { | |
| if (mValidAfterDynamic != null) { | |
| mLabelSize = Math.max(getLabelWidth(this), getLabelWidth(mValidAfterDynamic)); | |
| margin = getLabelTop(mValidAfterDynamic) - getLabelBottom(this); | |
| if (margin < 0) { | |
| labelOffset = margin / 2; | |
| } | |
| } else if (mValidBeforeDynamic != null) { | |
| mLabelSize = Math.max(getLabelWidth(this), getLabelWidth(mValidBeforeDynamic)); | |
| margin = getLabelTop(this) - getLabelBottom(mValidBeforeDynamic); | |
| if (margin < 0) { | |
| labelOffset = -margin / 2; | |
| } | |
| } else { | |
| mLabelSize = getLabelWidth(this); | |
| } | |
| } else { | |
| // TODO: implement horizontal labels | |
| } | |
| mLabelSize = Math.max(mLabelSize, mLabelMinSize); | |
| // when offsetting label, neighbor probably needs to offset too | |
| if (labelOffset != mLabelOffset) { | |
| mLabelOffset = labelOffset; | |
| invalidate(); | |
| if (mValidAfterDynamic != null) mValidAfterDynamic.invalidateLabelOffset(); | |
| if (mValidBeforeDynamic != null) mValidBeforeDynamic.invalidateLabelOffset(); | |
| } | |
| } | |
| @Override | |
| public void jumpDrawablesToCurrentState() { | |
| super.jumpDrawablesToCurrentState(); | |
| if (mSweep != null) { | |
| mSweep.jumpToCurrentState(); | |
| } | |
| } | |
| @Override | |
| public void setVisibility(int visibility) { | |
| super.setVisibility(visibility); | |
| if (mSweep != null) { | |
| mSweep.setVisible(visibility == VISIBLE, false); | |
| } | |
| } | |
| @Override | |
| protected boolean verifyDrawable(Drawable who) { | |
| return who == mSweep || super.verifyDrawable(who); | |
| } | |
| public ChartAxis getAxis() { | |
| return mAxis; | |
| } | |
| public void setValue(long value) { | |
| mValue = value; | |
| invalidateLabel(); | |
| } | |
| public long getValue() { | |
| return mValue; | |
| } | |
| public long getLabelValue() { | |
| return mLabelValue; | |
| } | |
| public float getPoint() { | |
| if (isEnabled()) { | |
| return mAxis.convertToPoint(mValue); | |
| } else { | |
| // when disabled, show along top edge | |
| return 0; | |
| } | |
| } | |
| /** | |
| * Set valid range this sweep can move within, in {@link #mAxis} values. The | |
| * most restrictive combination of all valid ranges is used. | |
| */ | |
| public void setValidRange(long validAfter, long validBefore) { | |
| mValidAfter = validAfter; | |
| mValidBefore = validBefore; | |
| } | |
| public void setNeighborMargin(float neighborMargin) { | |
| mNeighborMargin = neighborMargin; | |
| } | |
| public void setSafeRegion(int safeRegion) { | |
| mSafeRegion = safeRegion; | |
| } | |
| /** | |
| * Set valid range this sweep can move within, defined by the given | |
| * {@link ChartSweepView}. The most restrictive combination of all valid | |
| * ranges is used. | |
| */ | |
| public void setValidRangeDynamic(ChartSweepView validAfter, ChartSweepView validBefore) { | |
| mValidAfterDynamic = validAfter; | |
| mValidBeforeDynamic = validBefore; | |
| } | |
| /** | |
| * Test if given {@link MotionEvent} is closer to another | |
| * {@link ChartSweepView} compared to ourselves. | |
| */ | |
| public boolean isTouchCloserTo(MotionEvent eventInParent, ChartSweepView another) { | |
| final float selfDist = getTouchDistanceFromTarget(eventInParent); | |
| final float anotherDist = another.getTouchDistanceFromTarget(eventInParent); | |
| return anotherDist < selfDist; | |
| } | |
| private float getTouchDistanceFromTarget(MotionEvent eventInParent) { | |
| if (mFollowAxis == HORIZONTAL) { | |
| return Math.abs(eventInParent.getX() - (getX() + getTargetInset())); | |
| } else { | |
| return Math.abs(eventInParent.getY() - (getY() + getTargetInset())); | |
| } | |
| } | |
| @Override | |
| public boolean onTouchEvent(MotionEvent event) { | |
| if (!isEnabled()) return false; | |
| final View parent = (View) getParent(); | |
| switch (event.getAction()) { | |
| case MotionEvent.ACTION_DOWN: { | |
| // only start tracking when in sweet spot | |
| final boolean acceptDrag; | |
| final boolean acceptLabel; | |
| if (mFollowAxis == VERTICAL) { | |
| acceptDrag = event.getX() > getWidth() - (mSweepPadding.right * 8); | |
| acceptLabel = mLabelLayout != null ? event.getX() < mLabelLayout.getWidth() | |
| : false; | |
| } else { | |
| acceptDrag = event.getY() > getHeight() - (mSweepPadding.bottom * 8); | |
| acceptLabel = mLabelLayout != null ? event.getY() < mLabelLayout.getHeight() | |
| : false; | |
| } | |
| final MotionEvent eventInParent = event.copy(); | |
| eventInParent.offsetLocation(getLeft(), getTop()); | |
| // ignore event when closer to a neighbor | |
| for (ChartSweepView neighbor : mNeighbors) { | |
| if (isTouchCloserTo(eventInParent, neighbor)) { | |
| return false; | |
| } | |
| } | |
| if (acceptDrag) { | |
| if (mFollowAxis == VERTICAL) { | |
| mTrackingStart = getTop() - mMargins.top; | |
| } else { | |
| mTrackingStart = getLeft() - mMargins.left; | |
| } | |
| mTracking = event.copy(); | |
| mTouchMode = MODE_DRAG; | |
| // starting drag should activate entire chart | |
| if (!parent.isActivated()) { | |
| parent.setActivated(true); | |
| } | |
| return true; | |
| } else if (acceptLabel) { | |
| mTouchMode = MODE_LABEL; | |
| return true; | |
| } else { | |
| mTouchMode = MODE_NONE; | |
| return false; | |
| } | |
| } | |
| case MotionEvent.ACTION_MOVE: { | |
| if (mTouchMode == MODE_LABEL) { | |
| return true; | |
| } | |
| getParent().requestDisallowInterceptTouchEvent(true); | |
| // content area of parent | |
| final Rect parentContent = getParentContentRect(); | |
| final Rect clampRect = computeClampRect(parentContent); | |
| if (clampRect.isEmpty()) return true; | |
| long value; | |
| if (mFollowAxis == VERTICAL) { | |
| final float currentTargetY = getTop() - mMargins.top; | |
| final float requestedTargetY = mTrackingStart | |
| + (event.getRawY() - mTracking.getRawY()); | |
| final float clampedTargetY = MathUtils.constrain( | |
| requestedTargetY, clampRect.top, clampRect.bottom); | |
| setTranslationY(clampedTargetY - currentTargetY); | |
| value = mAxis.convertToValue(clampedTargetY - parentContent.top); | |
| } else { | |
| final float currentTargetX = getLeft() - mMargins.left; | |
| final float requestedTargetX = mTrackingStart | |
| + (event.getRawX() - mTracking.getRawX()); | |
| final float clampedTargetX = MathUtils.constrain( | |
| requestedTargetX, clampRect.left, clampRect.right); | |
| setTranslationX(clampedTargetX - currentTargetX); | |
| value = mAxis.convertToValue(clampedTargetX - parentContent.left); | |
| } | |
| // round value from drag to nearest increment | |
| value -= value % mDragInterval; | |
| setValue(value); | |
| dispatchOnSweep(false); | |
| return true; | |
| } | |
| case MotionEvent.ACTION_UP: { | |
| if (mTouchMode == MODE_LABEL) { | |
| performClick(); | |
| } else if (mTouchMode == MODE_DRAG) { | |
| mTrackingStart = 0; | |
| mTracking = null; | |
| mValue = mLabelValue; | |
| dispatchOnSweep(true); | |
| setTranslationX(0); | |
| setTranslationY(0); | |
| requestLayout(); | |
| } | |
| mTouchMode = MODE_NONE; | |
| return true; | |
| } | |
| default: { | |
| return false; | |
| } | |
| } | |
| } | |
| /** | |
| * Update {@link #mValue} based on current position, including any | |
| * {@link #onTouchEvent(MotionEvent)} in progress. Typically used when | |
| * {@link ChartAxis} changes during sweep adjustment. | |
| */ | |
| public void updateValueFromPosition() { | |
| final Rect parentContent = getParentContentRect(); | |
| if (mFollowAxis == VERTICAL) { | |
| final float effectiveY = getY() - mMargins.top - parentContent.top; | |
| setValue(mAxis.convertToValue(effectiveY)); | |
| } else { | |
| final float effectiveX = getX() - mMargins.left - parentContent.left; | |
| setValue(mAxis.convertToValue(effectiveX)); | |
| } | |
| } | |
| public int shouldAdjustAxis() { | |
| return mAxis.shouldAdjustAxis(getValue()); | |
| } | |
| private Rect getParentContentRect() { | |
| final View parent = (View) getParent(); | |
| return new Rect(parent.getPaddingLeft(), parent.getPaddingTop(), | |
| parent.getWidth() - parent.getPaddingRight(), | |
| parent.getHeight() - parent.getPaddingBottom()); | |
| } | |
| @Override | |
| public void addOnLayoutChangeListener(OnLayoutChangeListener listener) { | |
| // ignored to keep LayoutTransition from animating us | |
| } | |
| @Override | |
| public void removeOnLayoutChangeListener(OnLayoutChangeListener listener) { | |
| // ignored to keep LayoutTransition from animating us | |
| } | |
| private long getValidAfterDynamic() { | |
| final ChartSweepView dynamic = mValidAfterDynamic; | |
| return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MIN_VALUE; | |
| } | |
| private long getValidBeforeDynamic() { | |
| final ChartSweepView dynamic = mValidBeforeDynamic; | |
| return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MAX_VALUE; | |
| } | |
| /** | |
| * Compute {@link Rect} in {@link #getParent()} coordinates that we should | |
| * be clamped inside of, usually from {@link #setValidRange(long, long)} | |
| * style rules. | |
| */ | |
| private Rect computeClampRect(Rect parentContent) { | |
| // create two rectangles, and pick most restrictive combination | |
| final Rect rect = buildClampRect(parentContent, mValidAfter, mValidBefore, 0f); | |
| final Rect dynamicRect = buildClampRect( | |
| parentContent, getValidAfterDynamic(), getValidBeforeDynamic(), mNeighborMargin); | |
| if (!rect.intersect(dynamicRect)) { | |
| rect.setEmpty(); | |
| } | |
| return rect; | |
| } | |
| private Rect buildClampRect( | |
| Rect parentContent, long afterValue, long beforeValue, float margin) { | |
| if (mAxis instanceof InvertedChartAxis) { | |
| long temp = beforeValue; | |
| beforeValue = afterValue; | |
| afterValue = temp; | |
| } | |
| final boolean afterValid = afterValue != Long.MIN_VALUE && afterValue != Long.MAX_VALUE; | |
| final boolean beforeValid = beforeValue != Long.MIN_VALUE && beforeValue != Long.MAX_VALUE; | |
| final float afterPoint = mAxis.convertToPoint(afterValue) + margin; | |
| final float beforePoint = mAxis.convertToPoint(beforeValue) - margin; | |
| final Rect clampRect = new Rect(parentContent); | |
| if (mFollowAxis == VERTICAL) { | |
| if (beforeValid) clampRect.bottom = clampRect.top + (int) beforePoint; | |
| if (afterValid) clampRect.top += afterPoint; | |
| } else { | |
| if (beforeValid) clampRect.right = clampRect.left + (int) beforePoint; | |
| if (afterValid) clampRect.left += afterPoint; | |
| } | |
| return clampRect; | |
| } | |
| @Override | |
| protected void drawableStateChanged() { | |
| super.drawableStateChanged(); | |
| if (mSweep.isStateful()) { | |
| mSweep.setState(getDrawableState()); | |
| } | |
| } | |
| @Override | |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
| // TODO: handle vertical labels | |
| if (isEnabled() && mLabelLayout != null) { | |
| final int sweepHeight = mSweep.getIntrinsicHeight(); | |
| final int templateHeight = mLabelLayout.getHeight(); | |
| mSweepOffset.x = 0; | |
| mSweepOffset.y = 0; | |
| mSweepOffset.y = (int) ((templateHeight / 2) - getTargetInset()); | |
| setMeasuredDimension(mSweep.getIntrinsicWidth(), Math.max(sweepHeight, templateHeight)); | |
| } else { | |
| mSweepOffset.x = 0; | |
| mSweepOffset.y = 0; | |
| setMeasuredDimension(mSweep.getIntrinsicWidth(), mSweep.getIntrinsicHeight()); | |
| } | |
| if (mFollowAxis == VERTICAL) { | |
| final int targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top | |
| - mSweepPadding.bottom; | |
| mMargins.top = -(mSweepPadding.top + (targetHeight / 2)); | |
| mMargins.bottom = 0; | |
| mMargins.left = -mSweepPadding.left; | |
| mMargins.right = mSweepPadding.right; | |
| } else { | |
| final int targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left | |
| - mSweepPadding.right; | |
| mMargins.left = -(mSweepPadding.left + (targetWidth / 2)); | |
| mMargins.right = 0; | |
| mMargins.top = -mSweepPadding.top; | |
| mMargins.bottom = mSweepPadding.bottom; | |
| } | |
| mContentOffset.set(0, 0, 0, 0); | |
| // make touch target area larger | |
| final int widthBefore = getMeasuredWidth(); | |
| final int heightBefore = getMeasuredHeight(); | |
| if (mFollowAxis == HORIZONTAL) { | |
| final int widthAfter = widthBefore * 3; | |
| setMeasuredDimension(widthAfter, heightBefore); | |
| mContentOffset.left = (widthAfter - widthBefore) / 2; | |
| final int offset = mSweepPadding.bottom * 2; | |
| mContentOffset.bottom -= offset; | |
| mMargins.bottom += offset; | |
| } else { | |
| final int heightAfter = heightBefore * 2; | |
| setMeasuredDimension(widthBefore, heightAfter); | |
| mContentOffset.offset(0, (heightAfter - heightBefore) / 2); | |
| final int offset = mSweepPadding.right * 2; | |
| mContentOffset.right -= offset; | |
| mMargins.right += offset; | |
| } | |
| mSweepOffset.offset(mContentOffset.left, mContentOffset.top); | |
| mMargins.offset(-mSweepOffset.x, -mSweepOffset.y); | |
| } | |
| @Override | |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { | |
| super.onLayout(changed, left, top, right, bottom); | |
| invalidateLabelOffset(); | |
| } | |
| @Override | |
| protected void onDraw(Canvas canvas) { | |
| super.onDraw(canvas); | |
| final int width = getWidth(); | |
| final int height = getHeight(); | |
| final int labelSize; | |
| if (isEnabled() && mLabelLayout != null) { | |
| final int count = canvas.save(); | |
| { | |
| final float alignOffset = mLabelSize - LARGE_WIDTH; | |
| canvas.translate( | |
| mContentOffset.left + alignOffset, mContentOffset.top + mLabelOffset); | |
| mLabelLayout.draw(canvas); | |
| } | |
| canvas.restoreToCount(count); | |
| labelSize = (int) mLabelSize + mSafeRegion; | |
| } else { | |
| labelSize = 0; | |
| } | |
| if (mFollowAxis == VERTICAL) { | |
| mSweep.setBounds(labelSize, mSweepOffset.y, width + mContentOffset.right, | |
| mSweepOffset.y + mSweep.getIntrinsicHeight()); | |
| } else { | |
| mSweep.setBounds(mSweepOffset.x, labelSize, mSweepOffset.x + mSweep.getIntrinsicWidth(), | |
| height + mContentOffset.bottom); | |
| } | |
| mSweep.draw(canvas); | |
| if (DRAW_OUTLINE) { | |
| mOutlinePaint.setColor(Color.RED); | |
| canvas.drawRect(0, 0, width, height, mOutlinePaint); | |
| } | |
| } | |
| public static float getLabelTop(ChartSweepView view) { | |
| return view.getY() + view.mContentOffset.top; | |
| } | |
| public static float getLabelBottom(ChartSweepView view) { | |
| return getLabelTop(view) + view.mLabelLayout.getHeight(); | |
| } | |
| public static float getLabelWidth(ChartSweepView view) { | |
| return Layout.getDesiredWidth(view.mLabelLayout.getText(), view.mLabelLayout.getPaint()); | |
| } | |
| } |