| /* |
| * 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 long mValidAfter; |
| private long mValidBefore; |
| private ChartSweepView mValidAfterDynamic; |
| private ChartSweepView mValidBeforeDynamic; |
| private long mValidBufferArea; |
| |
| 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; |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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, long bufferArea) { |
| mValidAfterDynamic = validAfter; |
| mValidBeforeDynamic = validBefore; |
| mValidBufferArea = bufferArea; |
| } |
| |
| @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(); |
| |
| // starting drag should activate entire chart |
| if (!parent.isActivated()) { |
| parent.setActivated(true); |
| } |
| |
| 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; |
| } |
| } |
| } |
| |
| @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 getValidAfterValue() { |
| final ChartSweepView dynamic = mValidAfterDynamic; |
| final boolean dynamicEnabled = dynamic != null && dynamic.isEnabled(); |
| return Math.max(mValidAfter, |
| dynamicEnabled ? dynamic.getValue() + mValidBufferArea : Long.MIN_VALUE); |
| } |
| |
| private long getValidBeforeValue() { |
| final ChartSweepView dynamic = mValidBeforeDynamic; |
| final boolean dynamicEnabled = dynamic != null && dynamic.isEnabled(); |
| return Math.min(mValidBefore, |
| dynamicEnabled ? dynamic.getValue() - mValidBufferArea : 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) { |
| final Rect clampRect = new Rect(parentContent); |
| |
| float validAfterPoint = mAxis.convertToPoint(getValidAfterValue()); |
| float validBeforePoint = mAxis.convertToPoint(getValidBeforeValue()); |
| if (validAfterPoint > validBeforePoint) { |
| float swap = validBeforePoint; |
| validBeforePoint = validAfterPoint; |
| validAfterPoint = swap; |
| } |
| |
| if (mFollowAxis == VERTICAL) { |
| clampRect.bottom = clampRect.top + (int) validBeforePoint; |
| clampRect.top += validAfterPoint; |
| } else { |
| clampRect.right = clampRect.left + (int) validBeforePoint; |
| clampRect.left += validAfterPoint; |
| } |
| 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); |
| } |
| |
| } |