ChartSweepView.java revision e2afc0f283f58ce60c107643978bfff25ec5d5c1
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
17package com.android.settings.widget;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Canvas;
22import android.graphics.Color;
23import android.graphics.Paint;
24import android.graphics.Point;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
27import android.text.DynamicLayout;
28import android.text.Layout.Alignment;
29import android.text.SpannableStringBuilder;
30import android.text.TextPaint;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.util.MathUtils;
34import android.view.MotionEvent;
35import android.view.View;
36import android.widget.FrameLayout;
37
38import com.android.settings.R;
39import com.google.common.base.Preconditions;
40
41/**
42 * Sweep across a {@link ChartView} at a specific {@link ChartAxis} value, which
43 * a user can drag.
44 */
45public class ChartSweepView extends FrameLayout {
46
47    private Drawable mSweep;
48    private Rect mSweepPadding = new Rect();
49    private Point mSweepOffset = new Point();
50
51    private Rect mMargins = new Rect();
52    private float mNeighborMargin;
53
54    private int mFollowAxis;
55
56    private int mLabelSize;
57    private int mLabelTemplateRes;
58    private int mLabelColor;
59
60    private SpannableStringBuilder mLabelTemplate;
61    private DynamicLayout mLabelLayout;
62
63    private ChartAxis mAxis;
64    private long mValue;
65
66    private long mValidAfter;
67    private long mValidBefore;
68    private ChartSweepView mValidAfterDynamic;
69    private ChartSweepView mValidBeforeDynamic;
70
71    public static final int HORIZONTAL = 0;
72    public static final int VERTICAL = 1;
73
74    public interface OnSweepListener {
75        public void onSweep(ChartSweepView sweep, boolean sweepDone);
76    }
77
78    private OnSweepListener mListener;
79    private MotionEvent mTracking;
80
81    public ChartSweepView(Context context) {
82        this(context, null, 0);
83    }
84
85    public ChartSweepView(Context context, AttributeSet attrs) {
86        this(context, attrs, 0);
87    }
88
89    public ChartSweepView(Context context, AttributeSet attrs, int defStyle) {
90        super(context, attrs, defStyle);
91
92        final TypedArray a = context.obtainStyledAttributes(
93                attrs, R.styleable.ChartSweepView, defStyle, 0);
94
95        setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable));
96        setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1));
97        setNeighborMargin(a.getDimensionPixelSize(R.styleable.ChartSweepView_neighborMargin, 0));
98
99        setLabelSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0));
100        setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0));
101        setLabelColor(a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE));
102
103        a.recycle();
104
105        setClipToPadding(false);
106        setClipChildren(false);
107        setWillNotDraw(false);
108    }
109
110    void init(ChartAxis axis) {
111        mAxis = Preconditions.checkNotNull(axis, "missing axis");
112    }
113
114    public int getFollowAxis() {
115        return mFollowAxis;
116    }
117
118    public Rect getMargins() {
119        return mMargins;
120    }
121
122    /**
123     * Return the number of pixels that the "target" area is inset from the
124     * {@link View} edge, along the current {@link #setFollowAxis(int)}.
125     */
126    private float getTargetInset() {
127        if (mFollowAxis == VERTICAL) {
128            final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
129                    - mSweepPadding.bottom;
130            return mSweepPadding.top + (targetHeight / 2);
131        } else {
132            final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
133                    - mSweepPadding.right;
134            return mSweepPadding.left + (targetWidth / 2);
135        }
136    }
137
138    public void addOnSweepListener(OnSweepListener listener) {
139        mListener = listener;
140    }
141
142    private void dispatchOnSweep(boolean sweepDone) {
143        if (mListener != null) {
144            mListener.onSweep(this, sweepDone);
145        }
146    }
147
148    @Override
149    public void setEnabled(boolean enabled) {
150        super.setEnabled(enabled);
151        requestLayout();
152    }
153
154    public void setSweepDrawable(Drawable sweep) {
155        if (mSweep != null) {
156            mSweep.setCallback(null);
157            unscheduleDrawable(mSweep);
158        }
159
160        if (sweep != null) {
161            sweep.setCallback(this);
162            if (sweep.isStateful()) {
163                sweep.setState(getDrawableState());
164            }
165            sweep.setVisible(getVisibility() == VISIBLE, false);
166            mSweep = sweep;
167            sweep.getPadding(mSweepPadding);
168        } else {
169            mSweep = null;
170        }
171
172        invalidate();
173    }
174
175    public void setFollowAxis(int followAxis) {
176        mFollowAxis = followAxis;
177    }
178
179    public void setLabelSize(int size) {
180        mLabelSize = size;
181        invalidateLabelTemplate();
182    }
183
184    public void setLabelTemplate(int resId) {
185        mLabelTemplateRes = resId;
186        invalidateLabelTemplate();
187    }
188
189    public void setLabelColor(int color) {
190        mLabelColor = color;
191        invalidateLabelTemplate();
192    }
193
194    private void invalidateLabelTemplate() {
195        if (mLabelTemplateRes != 0) {
196            final CharSequence template = getResources().getText(mLabelTemplateRes);
197
198            final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
199            paint.density = getResources().getDisplayMetrics().density;
200            paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
201            paint.setColor(mLabelColor);
202
203            mLabelTemplate = new SpannableStringBuilder(template);
204            mLabelLayout = new DynamicLayout(
205                    mLabelTemplate, paint, mLabelSize, Alignment.ALIGN_RIGHT, 1f, 0f, false);
206            invalidateLabel();
207
208        } else {
209            mLabelTemplate = null;
210            mLabelLayout = null;
211        }
212
213        invalidate();
214        requestLayout();
215    }
216
217    private void invalidateLabel() {
218        if (mLabelTemplate != null && mAxis != null) {
219            mAxis.buildLabel(getResources(), mLabelTemplate, mValue);
220            invalidate();
221        }
222    }
223
224    @Override
225    public void jumpDrawablesToCurrentState() {
226        super.jumpDrawablesToCurrentState();
227        if (mSweep != null) {
228            mSweep.jumpToCurrentState();
229        }
230    }
231
232    @Override
233    public void setVisibility(int visibility) {
234        super.setVisibility(visibility);
235        if (mSweep != null) {
236            mSweep.setVisible(visibility == VISIBLE, false);
237        }
238    }
239
240    @Override
241    protected boolean verifyDrawable(Drawable who) {
242        return who == mSweep || super.verifyDrawable(who);
243    }
244
245    public ChartAxis getAxis() {
246        return mAxis;
247    }
248
249    public void setValue(long value) {
250        mValue = value;
251        invalidateLabel();
252    }
253
254    public long getValue() {
255        return mValue;
256    }
257
258    public float getPoint() {
259        if (isEnabled()) {
260            return mAxis.convertToPoint(mValue);
261        } else {
262            // when disabled, show along top edge
263            return 0;
264        }
265    }
266
267    /**
268     * Set valid range this sweep can move within, in {@link #mAxis} values. The
269     * most restrictive combination of all valid ranges is used.
270     */
271    public void setValidRange(long validAfter, long validBefore) {
272        mValidAfter = validAfter;
273        mValidBefore = validBefore;
274    }
275
276    public void setNeighborMargin(float neighborMargin) {
277        mNeighborMargin = neighborMargin;
278    }
279
280    /**
281     * Set valid range this sweep can move within, defined by the given
282     * {@link ChartSweepView}. The most restrictive combination of all valid
283     * ranges is used.
284     */
285    public void setValidRangeDynamic(ChartSweepView validAfter, ChartSweepView validBefore) {
286        mValidAfterDynamic = validAfter;
287        mValidBeforeDynamic = validBefore;
288    }
289
290    @Override
291    public boolean onTouchEvent(MotionEvent event) {
292        if (!isEnabled()) return false;
293
294        final View parent = (View) getParent();
295        switch (event.getAction()) {
296            case MotionEvent.ACTION_DOWN: {
297
298                // only start tracking when in sweet spot
299                final boolean accept;
300                if (mFollowAxis == VERTICAL) {
301                    accept = event.getX() > getWidth() - (mSweepPadding.right * 2);
302                } else {
303                    accept = event.getY() > getHeight() - (mSweepPadding.bottom * 2);
304                }
305
306                if (accept) {
307                    mTracking = event.copy();
308
309                    // starting drag should activate entire chart
310                    if (!parent.isActivated()) {
311                        parent.setActivated(true);
312                    }
313
314                    return true;
315                } else {
316                    return false;
317                }
318            }
319            case MotionEvent.ACTION_MOVE: {
320                getParent().requestDisallowInterceptTouchEvent(true);
321
322                // content area of parent
323                final Rect parentContent = getParentContentRect();
324                final Rect clampRect = computeClampRect(parentContent);
325
326                if (mFollowAxis == VERTICAL) {
327                    final float currentTargetY = getTop() - mMargins.top;
328                    final float requestedTargetY = currentTargetY
329                            + (event.getRawY() - mTracking.getRawY());
330                    final float clampedTargetY = MathUtils.constrain(
331                            requestedTargetY, clampRect.top, clampRect.bottom);
332                    setTranslationY(clampedTargetY - currentTargetY);
333
334                    setValue(mAxis.convertToValue(clampedTargetY - parentContent.top));
335                } else {
336                    final float currentTargetX = getLeft() - mMargins.left;
337                    final float requestedTargetX = currentTargetX
338                            + (event.getRawX() - mTracking.getRawX());
339                    final float clampedTargetX = MathUtils.constrain(
340                            requestedTargetX, clampRect.left, clampRect.right);
341                    setTranslationX(clampedTargetX - currentTargetX);
342
343                    setValue(mAxis.convertToValue(clampedTargetX - parentContent.left));
344                }
345
346                dispatchOnSweep(false);
347                return true;
348            }
349            case MotionEvent.ACTION_UP: {
350                mTracking = null;
351                dispatchOnSweep(true);
352                setTranslationX(0);
353                setTranslationY(0);
354                requestLayout();
355                return true;
356            }
357            default: {
358                return false;
359            }
360        }
361    }
362
363    /**
364     * Update {@link #mValue} based on current position, including any
365     * {@link #onTouchEvent(MotionEvent)} in progress. Typically used when
366     * {@link ChartAxis} changes during sweep adjustment.
367     */
368    public void updateValueFromPosition() {
369        final Rect parentContent = getParentContentRect();
370        if (mFollowAxis == VERTICAL) {
371            final float effectiveY = getY() - mMargins.top - parentContent.top;
372            setValue(mAxis.convertToValue(effectiveY));
373        } else {
374            final float effectiveX = getX() - mMargins.left - parentContent.left;
375            setValue(mAxis.convertToValue(effectiveX));
376        }
377    }
378
379    public int shouldAdjustAxis() {
380        return mAxis.shouldAdjustAxis(getValue());
381    }
382
383    private Rect getParentContentRect() {
384        final View parent = (View) getParent();
385        return new Rect(parent.getPaddingLeft(), parent.getPaddingTop(),
386                parent.getWidth() - parent.getPaddingRight(),
387                parent.getHeight() - parent.getPaddingBottom());
388    }
389
390    @Override
391    public void addOnLayoutChangeListener(OnLayoutChangeListener listener) {
392        // ignored to keep LayoutTransition from animating us
393    }
394
395    @Override
396    public void removeOnLayoutChangeListener(OnLayoutChangeListener listener) {
397        // ignored to keep LayoutTransition from animating us
398    }
399
400    private long getValidAfterDynamic() {
401        final ChartSweepView dynamic = mValidAfterDynamic;
402        return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MIN_VALUE;
403    }
404
405    private long getValidBeforeDynamic() {
406        final ChartSweepView dynamic = mValidBeforeDynamic;
407        return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MAX_VALUE;
408    }
409
410    /**
411     * Compute {@link Rect} in {@link #getParent()} coordinates that we should
412     * be clamped inside of, usually from {@link #setValidRange(long, long)}
413     * style rules.
414     */
415    private Rect computeClampRect(Rect parentContent) {
416        // create two rectangles, and pick most restrictive combination
417        final Rect rect = buildClampRect(parentContent, mValidAfter, mValidBefore, 0f);
418        final Rect dynamicRect = buildClampRect(
419                parentContent, getValidAfterDynamic(), getValidBeforeDynamic(), mNeighborMargin);
420
421        rect.intersect(dynamicRect);
422        return rect;
423    }
424
425    private Rect buildClampRect(
426            Rect parentContent, long afterValue, long beforeValue, float margin) {
427        if (mAxis instanceof InvertedChartAxis) {
428            long temp = beforeValue;
429            beforeValue = afterValue;
430            afterValue = temp;
431        }
432
433        final boolean afterValid = afterValue != Long.MIN_VALUE && afterValue != Long.MAX_VALUE;
434        final boolean beforeValid = beforeValue != Long.MIN_VALUE && beforeValue != Long.MAX_VALUE;
435
436        final float afterPoint = mAxis.convertToPoint(afterValue) + margin;
437        final float beforePoint = mAxis.convertToPoint(beforeValue) - margin;
438
439        final Rect clampRect = new Rect(parentContent);
440        if (mFollowAxis == VERTICAL) {
441            if (beforeValid) clampRect.bottom = clampRect.top + (int) beforePoint;
442            if (afterValid) clampRect.top += afterPoint;
443        } else {
444            if (beforeValid) clampRect.right = clampRect.left + (int) beforePoint;
445            if (afterValid) clampRect.left += afterPoint;
446        }
447        return clampRect;
448    }
449
450    @Override
451    protected void drawableStateChanged() {
452        super.drawableStateChanged();
453        if (mSweep.isStateful()) {
454            mSweep.setState(getDrawableState());
455        }
456    }
457
458    @Override
459    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
460
461        // TODO: handle vertical labels
462        if (isEnabled() && mLabelLayout != null) {
463            final int sweepHeight = mSweep.getIntrinsicHeight();
464            final int templateHeight = mLabelLayout.getHeight();
465
466            mSweepOffset.x = 0;
467            mSweepOffset.y = (int) ((templateHeight / 2) - getTargetInset());
468            setMeasuredDimension(mSweep.getIntrinsicWidth(), Math.max(sweepHeight, templateHeight));
469
470        } else {
471            mSweepOffset.x = 0;
472            mSweepOffset.y = 0;
473            setMeasuredDimension(mSweep.getIntrinsicWidth(), mSweep.getIntrinsicHeight());
474        }
475
476        if (mFollowAxis == VERTICAL) {
477            final int targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
478                    - mSweepPadding.bottom;
479            mMargins.top = -(mSweepPadding.top + (targetHeight / 2));
480            mMargins.bottom = 0;
481            mMargins.left = -mSweepPadding.left;
482            mMargins.right = mSweepPadding.right;
483        } else {
484            final int targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
485                    - mSweepPadding.right;
486            mMargins.left = -(mSweepPadding.left + (targetWidth / 2));
487            mMargins.right = 0;
488            mMargins.top = -mSweepPadding.top;
489            mMargins.bottom = mSweepPadding.bottom;
490        }
491
492        mMargins.offset(-mSweepOffset.x, -mSweepOffset.y);
493    }
494
495    @Override
496    protected void onDraw(Canvas canvas) {
497        final int width = getWidth();
498        final int height = getHeight();
499
500        final int labelSize;
501        if (isEnabled() && mLabelLayout != null) {
502            mLabelLayout.draw(canvas);
503            labelSize = mLabelSize;
504        } else {
505            labelSize = 0;
506        }
507
508        if (mFollowAxis == VERTICAL) {
509            mSweep.setBounds(labelSize, mSweepOffset.y, width,
510                    mSweepOffset.y + mSweep.getIntrinsicHeight());
511        } else {
512            mSweep.setBounds(mSweepOffset.x, labelSize,
513                    mSweepOffset.x + mSweep.getIntrinsicWidth(), height);
514        }
515
516        mSweep.draw(canvas);
517    }
518
519}
520