diff options
Diffstat (limited to 'src/com/android/settings/widget/ChartSweepView.java')
-rw-r--r-- | src/com/android/settings/widget/ChartSweepView.java | 437 |
1 files changed, 437 insertions, 0 deletions
diff --git a/src/com/android/settings/widget/ChartSweepView.java b/src/com/android/settings/widget/ChartSweepView.java new file mode 100644 index 0000000..4e37657 --- /dev/null +++ b/src/com/android/settings/widget/ChartSweepView.java @@ -0,0 +1,437 @@ +/* + * 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.Point; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.DynamicLayout; +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 android.widget.FrameLayout; + +import com.android.settings.R; +import com.google.common.base.Preconditions; + +/** + * Sweep across a {@link ChartView} at a specific {@link ChartAxis} value, which + * a user can drag. + */ +public class ChartSweepView extends FrameLayout { + + private Drawable mSweep; + private Rect mSweepPadding = new Rect(); + private Point mSweepOffset = new Point(); + + private Rect mMargins = new Rect(); + + private int mFollowAxis; + + private int mLabelSize; + private int mLabelTemplateRes; + private int mLabelColor; + + private SpannableStringBuilder mLabelTemplate; + private DynamicLayout mLabelLayout; + + private ChartAxis mAxis; + private long mValue; + + private ChartSweepView mClampAfter; + private ChartSweepView mClampBefore; + + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + public interface OnSweepListener { + public void onSweep(ChartSweepView sweep, boolean sweepDone); + } + + private OnSweepListener mListener; + private MotionEvent mTracking; + + public ChartSweepView(Context context) { + this(context, null, 0); + } + + 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); + + setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable)); + setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1)); + + setLabelSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0)); + setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0)); + setLabelColor(a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE)); + + a.recycle(); + + setClipToPadding(false); + setClipChildren(false); + setWillNotDraw(false); + } + + void init(ChartAxis axis) { + mAxis = Preconditions.checkNotNull(axis, "missing axis"); + } + + public int getFollowAxis() { + return mFollowAxis; + } + + public Rect getMargins() { + return mMargins; + } + + /** + * 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); + } else { + final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left + - mSweepPadding.right; + return mSweepPadding.left + (targetWidth / 2); + } + } + + public void addOnSweepListener(OnSweepListener listener) { + mListener = listener; + } + + private void dispatchOnSweep(boolean sweepDone) { + if (mListener != null) { + mListener.onSweep(this, sweepDone); + } + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + requestLayout(); + } + + public void setSweepDrawable(Drawable sweep) { + 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; + sweep.getPadding(mSweepPadding); + } else { + mSweep = null; + } + + invalidate(); + } + + public void setFollowAxis(int followAxis) { + mFollowAxis = followAxis; + } + + public void setLabelSize(int size) { + mLabelSize = size; + 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 = new DynamicLayout( + mLabelTemplate, paint, mLabelSize, Alignment.ALIGN_RIGHT, 1f, 0f, false); + invalidateLabel(); + + } else { + mLabelTemplate = null; + mLabelLayout = null; + } + + invalidate(); + requestLayout(); + } + + private void invalidateLabel() { + if (mLabelTemplate != null && mAxis != null) { + mAxis.buildLabel(getResources(), mLabelTemplate, mValue); + invalidate(); + } + } + + @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 float getPoint() { + if (isEnabled()) { + return mAxis.convertToPoint(mValue); + } else { + // when disabled, show along top edge + return 0; + } + } + + public void setClampAfter(ChartSweepView clampAfter) { + mClampAfter = clampAfter; + } + + public void setClampBefore(ChartSweepView clampBefore) { + mClampBefore = clampBefore; + } + + @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 accept; + if (mFollowAxis == VERTICAL) { + accept = event.getX() > getWidth() - (mSweepPadding.right * 2); + } else { + accept = event.getY() > getHeight() - (mSweepPadding.bottom * 2); + } + + if (accept) { + mTracking = event.copy(); + return true; + } else { + return false; + } + } + case MotionEvent.ACTION_MOVE: { + getParent().requestDisallowInterceptTouchEvent(true); + + // content area of parent + final Rect parentContent = new Rect(parent.getPaddingLeft(), parent.getPaddingTop(), + parent.getWidth() - parent.getPaddingRight(), + parent.getHeight() - parent.getPaddingBottom()); + final Rect clampRect = computeClampRect(parentContent); + + if (mFollowAxis == VERTICAL) { + final float currentTargetY = getTop() - mMargins.top; + final float requestedTargetY = currentTargetY + + (event.getRawY() - mTracking.getRawY()); + final float clampedTargetY = MathUtils.constrain( + requestedTargetY, clampRect.top, clampRect.bottom); + setTranslationY(clampedTargetY - currentTargetY); + + setValue(mAxis.convertToValue(clampedTargetY - parentContent.top)); + } else { + final float currentTargetX = getLeft() - mMargins.left; + final float requestedTargetX = currentTargetX + + (event.getRawX() - mTracking.getRawX()); + final float clampedTargetX = MathUtils.constrain( + requestedTargetX, clampRect.left, clampRect.right); + setTranslationX(clampedTargetX - currentTargetX); + + setValue(mAxis.convertToValue(clampedTargetX - parentContent.left)); + } + + dispatchOnSweep(false); + return true; + } + case MotionEvent.ACTION_UP: { + mTracking = null; + dispatchOnSweep(true); + setTranslationX(0); + setTranslationY(0); + requestLayout(); + return true; + } + default: { + return false; + } + } + } + + /** + * Compute {@link Rect} in {@link #getParent()} coordinates that we should + * be clamped inside of, usually from {@link #setClampAfter(ChartSweepView)} + * style rules. + */ + private Rect computeClampRect(Rect parentContent) { + final Rect clampRect = new Rect(parentContent); + + final ChartSweepView after = mClampAfter; + final ChartSweepView before = mClampBefore; + + if (mFollowAxis == VERTICAL) { + if (after != null) { + clampRect.top += after.getPoint(); + } + if (before != null) { + clampRect.bottom -= clampRect.height() - before.getPoint(); + } + } else { + if (after != null) { + clampRect.left += after.getPoint(); + } + if (before != null) { + clampRect.right -= clampRect.width() - before.getPoint(); + } + } + 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 = (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; + } + + mMargins.offset(-mSweepOffset.x, -mSweepOffset.y); + } + + @Override + protected void onDraw(Canvas canvas) { + final int width = getWidth(); + final int height = getHeight(); + + final int labelSize; + if (isEnabled() && mLabelLayout != null) { + mLabelLayout.draw(canvas); + labelSize = mLabelSize; + } else { + labelSize = 0; + } + + if (mFollowAxis == VERTICAL) { + mSweep.setBounds(labelSize, mSweepOffset.y, width, + mSweepOffset.y + mSweep.getIntrinsicHeight()); + } else { + mSweep.setBounds(mSweepOffset.x, labelSize, + mSweepOffset.x + mSweep.getIntrinsicWidth(), height); + } + + mSweep.draw(canvas); + } + +} |