ChartSweepView.java revision 28130d96385d7d7b17992b45fb5d124836d85880
12ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler/*
22ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler * Copyright (C) 2011 The Android Open Source Project
32ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler *
42ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler * Licensed under the Apache License, Version 2.0 (the "License");
52ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler * you may not use this file except in compliance with the License.
62ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler * You may obtain a copy of the License at
72ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler *
82ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler *      http://www.apache.org/licenses/LICENSE-2.0
92ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler *
102ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler * Unless required by applicable law or agreed to in writing, software
112ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler * distributed under the License is distributed on an "AS IS" BASIS,
122ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
132ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler * See the License for the specific language governing permissions and
142ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler * limitations under the License.
152ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler */
162ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler
172ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadlerpackage com.android.settings.widget;
182ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler
1938089f6c4222ab56582899f1f228966c5ebf75e8Makoto Onukiimport android.content.Context;
202ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadlerimport android.content.res.TypedArray;
212ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadlerimport android.graphics.Canvas;
22308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onukiimport android.graphics.Color;
232ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadlerimport android.graphics.Paint;
249f14d6b0de74b81f087295bfbaded133f4076dd5Tony Mantlerimport android.graphics.Paint.Style;
2557f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadlerimport android.graphics.Point;
2697874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadlerimport android.graphics.Rect;
2797874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadlerimport android.graphics.drawable.Drawable;
282ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadlerimport android.text.DynamicLayout;
2935b0e95ca795e17b6dc8dd98c7ab847d65d9aa0cMarc Blankimport android.text.Layout.Alignment;
300e6a521747970d5427f10c25cdc070d2341dc93aBen Komaloimport android.text.SpannableStringBuilder;
31f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport android.text.TextPaint;
327b9f7ff76fd9812d7e3ae4dd42c1ba97b6e347e7Tony Mantlerimport android.util.AttributeSet;
3338f22dbf08664b885b4cf063ea665c02edfb1c32Paul Westbrookimport android.util.MathUtils;
3435b0e95ca795e17b6dc8dd98c7ab847d65d9aa0cMarc Blankimport android.view.MotionEvent;
352ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadlerimport android.view.View;
362ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler
372ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadlerimport com.android.settings.R;
38fd14496c494a0d38c35c3788c9cc55f1984592e4Andrew Stadlerimport com.google.common.base.Preconditions;
39fd14496c494a0d38c35c3788c9cc55f1984592e4Andrew Stadler
40fd14496c494a0d38c35c3788c9cc55f1984592e4Andrew Stadler/**
41fd14496c494a0d38c35c3788c9cc55f1984592e4Andrew Stadler * Sweep across a {@link ChartView} at a specific {@link ChartAxis} value, which
42fd14496c494a0d38c35c3788c9cc55f1984592e4Andrew Stadler * a user can drag.
43fd14496c494a0d38c35c3788c9cc55f1984592e4Andrew Stadler */
442ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadlerpublic class ChartSweepView extends View {
452ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler
468f5ca5a790c4c05dd4ee6a8c769ff9817f40123bTony Mantler    private static final boolean DRAW_OUTLINE = false;
47308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki
48308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    private Drawable mSweep;
49308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    private Rect mSweepPadding = new Rect();
50308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki
51308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    /** Offset of content inside this view. */
52308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    private Point mContentOffset = new Point();
539c65c146f3d8e60f35f46c815d4121749ad13abdAndrew Stadler    /** Offset of {@link #mSweep} inside this view. */
543955f6794f23c1380749d4470b5f2264d2109adcBen Komalo    private Point mSweepOffset = new Point();
552c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon
562c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon    private Rect mMargins = new Rect();
5757f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    private float mNeighborMargin;
589c65c146f3d8e60f35f46c815d4121749ad13abdAndrew Stadler
599c65c146f3d8e60f35f46c815d4121749ad13abdAndrew Stadler    private int mFollowAxis;
60fb9deb96c3af56bf422e28e8ae3b7b838f343155Tony Mantler
615a3888f35b669ffb3cc785d7dfe4862879a3896cJorge Lugo    private int mLabelSize;
6257f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    private int mLabelTemplateRes;
6357f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    private int mLabelColor;
6457f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler
6557f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    private SpannableStringBuilder mLabelTemplate;
6657f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    private DynamicLayout mLabelLayout;
6757f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler
6857f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    private ChartAxis mAxis;
699c65c146f3d8e60f35f46c815d4121749ad13abdAndrew Stadler    private long mValue;
7057f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    private long mLabelValue;
712ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler
7257f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    private long mValidAfter;
73dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook    private long mValidBefore;
74dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook    private ChartSweepView mValidAfterDynamic;
752ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler    private ChartSweepView mValidBeforeDynamic;
76c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert
77c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert    private Paint mOutlinePaint = new Paint();
78c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert
79c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert    public static final int HORIZONTAL = 0;
80c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert    public static final int VERTICAL = 1;
81c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert
82c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert    public interface OnSweepListener {
83c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert        public void onSweep(ChartSweepView sweep, boolean sweepDone);
84c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert    }
85c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert
86c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert    private OnSweepListener mListener;
87c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert    private MotionEvent mTracking;
8806415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler
89c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert    public ChartSweepView(Context context) {
90c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert        this(context, null);
91c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert    }
92c4d139c4f4d924eae0307e8349ae977441dabbedAlon Albert
932ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler    public ChartSweepView(Context context, AttributeSet attrs) {
942ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler        this(context, attrs, 0);
952ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler    }
962ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler
97f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    public ChartSweepView(Context context, AttributeSet attrs, int defStyle) {
98a2cc46c810eb802c172a4af8ecc67fca53dd584fDianne Hackborn        super(context, attrs, defStyle);
99a2cc46c810eb802c172a4af8ecc67fca53dd584fDianne Hackborn
100a2cc46c810eb802c172a4af8ecc67fca53dd584fDianne Hackborn        final TypedArray a = context.obtainStyledAttributes(
101a2cc46c810eb802c172a4af8ecc67fca53dd584fDianne Hackborn                attrs, R.styleable.ChartSweepView, defStyle, 0);
102fb9deb96c3af56bf422e28e8ae3b7b838f343155Tony Mantler
103fb9deb96c3af56bf422e28e8ae3b7b838f343155Tony Mantler        setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable));
10474c79a50432fcbf127fbfeadc1a461263ea92135Marc Blank        setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1));
105fb9deb96c3af56bf422e28e8ae3b7b838f343155Tony Mantler        setNeighborMargin(a.getDimensionPixelSize(R.styleable.ChartSweepView_neighborMargin, 0));
10606415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler
10706415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler        setLabelSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0));
10806415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler        setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0));
10906415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler        setLabelColor(a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE));
11006415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler
11106415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler        mOutlinePaint.setColor(Color.RED);
112f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        mOutlinePaint.setStrokeWidth(1f);
1139f14d6b0de74b81f087295bfbaded133f4076dd5Tony Mantler        mOutlinePaint.setStyle(Style.STROKE);
114a2cc46c810eb802c172a4af8ecc67fca53dd584fDianne Hackborn
115f4894131427ec7562fcb05388b9f3aa094e388bcAndy Stadler        a.recycle();
11606415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler
11706415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler        setWillNotDraw(false);
1188c03e2af9f439c6e0c6abb38b0c371da7ccdb72aTony Mantler    }
11906415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler
12006415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler    void init(ChartAxis axis) {
121f4894131427ec7562fcb05388b9f3aa094e388bcAndy Stadler        mAxis = Preconditions.checkNotNull(axis, "missing axis");
122a2cc46c810eb802c172a4af8ecc67fca53dd584fDianne Hackborn    }
1239c65c146f3d8e60f35f46c815d4121749ad13abdAndrew Stadler
12457f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    public int getFollowAxis() {
12538089f6c4222ab56582899f1f228966c5ebf75e8Makoto Onuki        return mFollowAxis;
1269f14d6b0de74b81f087295bfbaded133f4076dd5Tony Mantler    }
1279f14d6b0de74b81f087295bfbaded133f4076dd5Tony Mantler
1289f14d6b0de74b81f087295bfbaded133f4076dd5Tony Mantler    public Rect getMargins() {
1299f14d6b0de74b81f087295bfbaded133f4076dd5Tony Mantler        return mMargins;
1309f14d6b0de74b81f087295bfbaded133f4076dd5Tony Mantler    }
131b7d137bfb6b59b1a4da4b14eb6022ce0df7cf637Ben Komalo
132dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook    /**
1332ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler     * Return the number of pixels that the "target" area is inset from the
1342ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler     * {@link View} edge, along the current {@link #setFollowAxis(int)}.
1352ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler     */
13657f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    private float getTargetInset() {
13757f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler        if (mFollowAxis == VERTICAL) {
13857f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler            final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
1399f14d6b0de74b81f087295bfbaded133f4076dd5Tony Mantler                    - mSweepPadding.bottom;
14057f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler            return mSweepPadding.top + (targetHeight / 2) + mSweepOffset.y;
14157f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler        } else {
14257f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler            final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
14357f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler                    - mSweepPadding.right;
14457f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler            return mSweepPadding.left + (targetWidth / 2) + mSweepOffset.x;
14557f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler        }
14657f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    }
14757f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler
14857f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    public void addOnSweepListener(OnSweepListener listener) {
14957f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler        mListener = listener;
15057f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    }
15157f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler
152b387560384d38e3280090176a0f3b2cf8b1f9ab5Andrew Stadler    private void dispatchOnSweep(boolean sweepDone) {
15397874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadler        if (mListener != null) {
15497874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadler            mListener.onSweep(this, sweepDone);
155525e8ad321967b3f4b15cadf63efb3deafc216abPaul Westbrook        }
156dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook    }
157dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook
15897874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadler    @Override
15997874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadler    public void setEnabled(boolean enabled) {
16097874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadler        super.setEnabled(enabled);
16197874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadler        requestLayout();
162dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook    }
163dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook
164dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook    public void setSweepDrawable(Drawable sweep) {
165dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook        if (mSweep != null) {
166dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook            mSweep.setCallback(null);
167dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook            unscheduleDrawable(mSweep);
168dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook        }
169dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook
170dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook        if (sweep != null) {
171dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook            sweep.setCallback(this);
172dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook            if (sweep.isStateful()) {
173dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook                sweep.setState(getDrawableState());
17497874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadler            }
17597874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadler            sweep.setVisible(getVisibility() == VISIBLE, false);
17638089f6c4222ab56582899f1f228966c5ebf75e8Makoto Onuki            mSweep = sweep;
17738089f6c4222ab56582899f1f228966c5ebf75e8Makoto Onuki            sweep.getPadding(mSweepPadding);
17838089f6c4222ab56582899f1f228966c5ebf75e8Makoto Onuki        } else {
17938089f6c4222ab56582899f1f228966c5ebf75e8Makoto Onuki            mSweep = null;
18038089f6c4222ab56582899f1f228966c5ebf75e8Makoto Onuki        }
18138089f6c4222ab56582899f1f228966c5ebf75e8Makoto Onuki
18238089f6c4222ab56582899f1f228966c5ebf75e8Makoto Onuki        invalidate();
18338f22dbf08664b885b4cf063ea665c02edfb1c32Paul Westbrook    }
184dbb8b75a4bd201f8472a511ef77ca2ed05bd808bPaul Westbrook
18538f22dbf08664b885b4cf063ea665c02edfb1c32Paul Westbrook    public void setFollowAxis(int followAxis) {
18697874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadler        mFollowAxis = followAxis;
18797874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadler    }
188b387560384d38e3280090176a0f3b2cf8b1f9ab5Andrew Stadler
18997874770fc7cbe6a89a6ea706658fb42dff77a95Andy Stadler    public void setLabelSize(int size) {
190b387560384d38e3280090176a0f3b2cf8b1f9ab5Andrew Stadler        mLabelSize = size;
191b387560384d38e3280090176a0f3b2cf8b1f9ab5Andrew Stadler        invalidateLabelTemplate();
1923955f6794f23c1380749d4470b5f2264d2109adcBen Komalo    }
19306415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler
19406415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler    public void setLabelTemplate(int resId) {
19506415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler        mLabelTemplateRes = resId;
196983e1ad53b3ca3105655bf6d961713c61060a7f8Andy Stadler        invalidateLabelTemplate();
197983e1ad53b3ca3105655bf6d961713c61060a7f8Andy Stadler    }
19857f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler
19957f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    public void setLabelColor(int color) {
20057f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler        mLabelColor = color;
20157f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler        invalidateLabelTemplate();
20257f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    }
203a14a24a5bc2ffa426f7ef8e5e6938cffe3f35829Andrew Stadler
20476472ae40cd55d17edb0420e8fc2a7bae60c50deTony Mantler    private void invalidateLabelTemplate() {
20576472ae40cd55d17edb0420e8fc2a7bae60c50deTony Mantler        if (mLabelTemplateRes != 0) {
206a14a24a5bc2ffa426f7ef8e5e6938cffe3f35829Andrew Stadler            final CharSequence template = getResources().getText(mLabelTemplateRes);
207a14a24a5bc2ffa426f7ef8e5e6938cffe3f35829Andrew Stadler
2082ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler            final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
20906415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler            paint.density = getResources().getDisplayMetrics().density;
21006415a635f5f01d8e1620b29f44d68dc4dfdf435Tony Mantler            paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
2111dcdc09e0338ab8f019a424d2b412b27491e918eTony Mantler            paint.setColor(mLabelColor);
2121dcdc09e0338ab8f019a424d2b412b27491e918eTony Mantler            paint.setShadowLayer(4 * paint.density, 0, 0, Color.BLACK);
2131dcdc09e0338ab8f019a424d2b412b27491e918eTony Mantler
2141dcdc09e0338ab8f019a424d2b412b27491e918eTony Mantler            mLabelTemplate = new SpannableStringBuilder(template);
2152c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon            mLabelLayout = new DynamicLayout(
2162c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon                    mLabelTemplate, paint, mLabelSize, Alignment.ALIGN_RIGHT, 1f, 0f, false);
2172c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon            invalidateLabel();
2182c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon
2192c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon        } else {
2202c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon            mLabelTemplate = null;
2212c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon            mLabelLayout = null;
2222c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon        }
2232c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon
2242c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon        invalidate();
2252c26bb3b09700ce2531eedbe66d389d21107a416Martin Hibdon        requestLayout();
22657f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    }
2272ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler
2282ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler    private void invalidateLabel() {
2292ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler        if (mLabelTemplate != null && mAxis != null) {
2302ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler            mLabelValue = mAxis.buildLabel(getResources(), mLabelTemplate, mValue);
2312ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler            invalidate();
2322ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler        }
2332ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler    }
2342ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler
2352ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler    @Override
2369f14d6b0de74b81f087295bfbaded133f4076dd5Tony Mantler    public void jumpDrawablesToCurrentState() {
2371a5e1e159352f6e21bde878eebca3e3a1896045cAndrew Stadler        super.jumpDrawablesToCurrentState();
23857f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler        if (mSweep != null) {
23957f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler            mSweep.jumpToCurrentState();
24057f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler        }
24157f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    }
24257f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler
24357f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    @Override
24457f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler    public void setVisibility(int visibility) {
24557f125a01b5fbb5860b144b3057153a50d07ddd1Andrew Stadler        super.setVisibility(visibility);
2461dcdc09e0338ab8f019a424d2b412b27491e918eTony Mantler        if (mSweep != null) {
2471dcdc09e0338ab8f019a424d2b412b27491e918eTony Mantler            mSweep.setVisible(visibility == VISIBLE, false);
2481dcdc09e0338ab8f019a424d2b412b27491e918eTony Mantler        }
2491dcdc09e0338ab8f019a424d2b412b27491e918eTony Mantler    }
2501a5e1e159352f6e21bde878eebca3e3a1896045cAndrew Stadler
2511a5e1e159352f6e21bde878eebca3e3a1896045cAndrew Stadler    @Override
2522ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler    protected boolean verifyDrawable(Drawable who) {
2532ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler        return who == mSweep || super.verifyDrawable(who);
2542ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler    }
2553955f6794f23c1380749d4470b5f2264d2109adcBen Komalo
2563955f6794f23c1380749d4470b5f2264d2109adcBen Komalo    public ChartAxis getAxis() {
2573955f6794f23c1380749d4470b5f2264d2109adcBen Komalo        return mAxis;
2581a5e1e159352f6e21bde878eebca3e3a1896045cAndrew Stadler    }
25972a24f12a2a0a48528cf0f826397e2348fe8ace2Ben Komalo
2602ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler    public void setValue(long value) {
2612ae2a12d6b049a4347c0781bd4daa17229bf1340Andrew Stadler        mValue = value;
262        invalidateLabel();
263    }
264
265    public long getValue() {
266        return mValue;
267    }
268
269    public long getLabelValue() {
270        return mLabelValue;
271    }
272
273    public float getPoint() {
274        if (isEnabled()) {
275            return mAxis.convertToPoint(mValue);
276        } else {
277            // when disabled, show along top edge
278            return 0;
279        }
280    }
281
282    /**
283     * Set valid range this sweep can move within, in {@link #mAxis} values. The
284     * most restrictive combination of all valid ranges is used.
285     */
286    public void setValidRange(long validAfter, long validBefore) {
287        mValidAfter = validAfter;
288        mValidBefore = validBefore;
289    }
290
291    public void setNeighborMargin(float neighborMargin) {
292        mNeighborMargin = neighborMargin;
293    }
294
295    /**
296     * Set valid range this sweep can move within, defined by the given
297     * {@link ChartSweepView}. The most restrictive combination of all valid
298     * ranges is used.
299     */
300    public void setValidRangeDynamic(ChartSweepView validAfter, ChartSweepView validBefore) {
301        mValidAfterDynamic = validAfter;
302        mValidBeforeDynamic = validBefore;
303    }
304
305    /**
306     * Test if given {@link MotionEvent} is closer to another
307     * {@link ChartSweepView} compared to ourselves.
308     */
309    public boolean isTouchCloserTo(MotionEvent eventInParent, ChartSweepView another) {
310        if (another == null) return false;
311
312        if (mFollowAxis == HORIZONTAL) {
313            final float selfDist = Math.abs(eventInParent.getX() - (getX() + getTargetInset()));
314            final float anotherDist = Math.abs(
315                    eventInParent.getX() - (another.getX() + another.getTargetInset()));
316            return anotherDist < selfDist;
317        } else {
318            final float selfDist = Math.abs(eventInParent.getY() - (getY() + getTargetInset()));
319            final float anotherDist = Math.abs(
320                    eventInParent.getY() - (another.getY() + another.getTargetInset()));
321            return anotherDist < selfDist;
322        }
323    }
324
325    @Override
326    public boolean onTouchEvent(MotionEvent event) {
327        if (!isEnabled()) return false;
328
329        final View parent = (View) getParent();
330        switch (event.getAction()) {
331            case MotionEvent.ACTION_DOWN: {
332
333                // only start tracking when in sweet spot
334                final boolean accept;
335                if (mFollowAxis == VERTICAL) {
336                    accept = event.getX() > getWidth() - (mSweepPadding.right * 3);
337                } else {
338                    accept = event.getY() > getHeight() - (mSweepPadding.bottom * 3);
339                }
340
341                final MotionEvent eventInParent = event.copy();
342                eventInParent.offsetLocation(getLeft(), getTop());
343
344                // ignore event when closer to a neighbor
345                if (isTouchCloserTo(eventInParent, mValidAfterDynamic)
346                        || isTouchCloserTo(eventInParent, mValidBeforeDynamic)) {
347                    return false;
348                }
349
350                if (accept) {
351                    mTracking = event.copy();
352
353                    // starting drag should activate entire chart
354                    if (!parent.isActivated()) {
355                        parent.setActivated(true);
356                    }
357
358                    return true;
359                } else {
360                    return false;
361                }
362            }
363            case MotionEvent.ACTION_MOVE: {
364                getParent().requestDisallowInterceptTouchEvent(true);
365
366                // content area of parent
367                final Rect parentContent = getParentContentRect();
368                final Rect clampRect = computeClampRect(parentContent);
369
370                if (mFollowAxis == VERTICAL) {
371                    final float currentTargetY = getTop() - mMargins.top;
372                    final float requestedTargetY = currentTargetY
373                            + (event.getRawY() - mTracking.getRawY());
374                    final float clampedTargetY = MathUtils.constrain(
375                            requestedTargetY, clampRect.top, clampRect.bottom);
376                    setTranslationY(clampedTargetY - currentTargetY);
377
378                    setValue(mAxis.convertToValue(clampedTargetY - parentContent.top));
379                } else {
380                    final float currentTargetX = getLeft() - mMargins.left;
381                    final float requestedTargetX = currentTargetX
382                            + (event.getRawX() - mTracking.getRawX());
383                    final float clampedTargetX = MathUtils.constrain(
384                            requestedTargetX, clampRect.left, clampRect.right);
385                    setTranslationX(clampedTargetX - currentTargetX);
386
387                    setValue(mAxis.convertToValue(clampedTargetX - parentContent.left));
388                }
389
390                dispatchOnSweep(false);
391                return true;
392            }
393            case MotionEvent.ACTION_UP: {
394                mTracking = null;
395                dispatchOnSweep(true);
396                setTranslationX(0);
397                setTranslationY(0);
398                requestLayout();
399                return true;
400            }
401            default: {
402                return false;
403            }
404        }
405    }
406
407    /**
408     * Update {@link #mValue} based on current position, including any
409     * {@link #onTouchEvent(MotionEvent)} in progress. Typically used when
410     * {@link ChartAxis} changes during sweep adjustment.
411     */
412    public void updateValueFromPosition() {
413        final Rect parentContent = getParentContentRect();
414        if (mFollowAxis == VERTICAL) {
415            final float effectiveY = getY() - mMargins.top - parentContent.top;
416            setValue(mAxis.convertToValue(effectiveY));
417        } else {
418            final float effectiveX = getX() - mMargins.left - parentContent.left;
419            setValue(mAxis.convertToValue(effectiveX));
420        }
421    }
422
423    public int shouldAdjustAxis() {
424        return mAxis.shouldAdjustAxis(getValue());
425    }
426
427    private Rect getParentContentRect() {
428        final View parent = (View) getParent();
429        return new Rect(parent.getPaddingLeft(), parent.getPaddingTop(),
430                parent.getWidth() - parent.getPaddingRight(),
431                parent.getHeight() - parent.getPaddingBottom());
432    }
433
434    @Override
435    public void addOnLayoutChangeListener(OnLayoutChangeListener listener) {
436        // ignored to keep LayoutTransition from animating us
437    }
438
439    @Override
440    public void removeOnLayoutChangeListener(OnLayoutChangeListener listener) {
441        // ignored to keep LayoutTransition from animating us
442    }
443
444    private long getValidAfterDynamic() {
445        final ChartSweepView dynamic = mValidAfterDynamic;
446        return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MIN_VALUE;
447    }
448
449    private long getValidBeforeDynamic() {
450        final ChartSweepView dynamic = mValidBeforeDynamic;
451        return dynamic != null && dynamic.isEnabled() ? dynamic.getValue() : Long.MAX_VALUE;
452    }
453
454    /**
455     * Compute {@link Rect} in {@link #getParent()} coordinates that we should
456     * be clamped inside of, usually from {@link #setValidRange(long, long)}
457     * style rules.
458     */
459    private Rect computeClampRect(Rect parentContent) {
460        // create two rectangles, and pick most restrictive combination
461        final Rect rect = buildClampRect(parentContent, mValidAfter, mValidBefore, 0f);
462        final Rect dynamicRect = buildClampRect(
463                parentContent, getValidAfterDynamic(), getValidBeforeDynamic(), mNeighborMargin);
464
465        rect.intersect(dynamicRect);
466        return rect;
467    }
468
469    private Rect buildClampRect(
470            Rect parentContent, long afterValue, long beforeValue, float margin) {
471        if (mAxis instanceof InvertedChartAxis) {
472            long temp = beforeValue;
473            beforeValue = afterValue;
474            afterValue = temp;
475        }
476
477        final boolean afterValid = afterValue != Long.MIN_VALUE && afterValue != Long.MAX_VALUE;
478        final boolean beforeValid = beforeValue != Long.MIN_VALUE && beforeValue != Long.MAX_VALUE;
479
480        final float afterPoint = mAxis.convertToPoint(afterValue) + margin;
481        final float beforePoint = mAxis.convertToPoint(beforeValue) - margin;
482
483        final Rect clampRect = new Rect(parentContent);
484        if (mFollowAxis == VERTICAL) {
485            if (beforeValid) clampRect.bottom = clampRect.top + (int) beforePoint;
486            if (afterValid) clampRect.top += afterPoint;
487        } else {
488            if (beforeValid) clampRect.right = clampRect.left + (int) beforePoint;
489            if (afterValid) clampRect.left += afterPoint;
490        }
491        return clampRect;
492    }
493
494    @Override
495    protected void drawableStateChanged() {
496        super.drawableStateChanged();
497        if (mSweep.isStateful()) {
498            mSweep.setState(getDrawableState());
499        }
500    }
501
502    @Override
503    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
504
505        // TODO: handle vertical labels
506        if (isEnabled() && mLabelLayout != null) {
507            final int sweepHeight = mSweep.getIntrinsicHeight();
508            final int templateHeight = mLabelLayout.getHeight();
509
510            mSweepOffset.x = 0;
511            mSweepOffset.y = 0;
512            mSweepOffset.y = (int) ((templateHeight / 2) - getTargetInset());
513            setMeasuredDimension(mSweep.getIntrinsicWidth(), Math.max(sweepHeight, templateHeight));
514
515        } else {
516            mSweepOffset.x = 0;
517            mSweepOffset.y = 0;
518            setMeasuredDimension(mSweep.getIntrinsicWidth(), mSweep.getIntrinsicHeight());
519        }
520
521        if (mFollowAxis == VERTICAL) {
522            final int targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
523                    - mSweepPadding.bottom;
524            mMargins.top = -(mSweepPadding.top + (targetHeight / 2));
525            mMargins.bottom = 0;
526            mMargins.left = -mSweepPadding.left;
527            mMargins.right = mSweepPadding.right;
528        } else {
529            final int targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
530                    - mSweepPadding.right;
531            mMargins.left = -(mSweepPadding.left + (targetWidth / 2));
532            mMargins.right = 0;
533            mMargins.top = -mSweepPadding.top;
534            mMargins.bottom = mSweepPadding.bottom;
535        }
536
537        mContentOffset.x = 0;
538        mContentOffset.y = 0;
539
540        // make touch target area larger
541        if (mFollowAxis == HORIZONTAL) {
542            final int widthBefore = getMeasuredWidth();
543            final int widthAfter = widthBefore * 3;
544            setMeasuredDimension(widthAfter, getMeasuredHeight());
545            mContentOffset.offset((widthAfter - widthBefore) / 2, 0);
546        } else {
547            final int heightBefore = getMeasuredHeight();
548            final int heightAfter = heightBefore * 3;
549            setMeasuredDimension(getMeasuredWidth(), heightAfter);
550            mContentOffset.offset(0, (heightAfter - heightBefore) / 2);
551        }
552
553        mSweepOffset.offset(mContentOffset.x, mContentOffset.y);
554        mMargins.offset(-mSweepOffset.x, -mSweepOffset.y);
555    }
556
557    @Override
558    protected void onDraw(Canvas canvas) {
559        final int width = getWidth();
560        final int height = getHeight();
561
562        if (DRAW_OUTLINE) {
563            canvas.drawRect(0, 0, width, height, mOutlinePaint);
564        }
565
566        // when overlapping with neighbor, split difference and push label
567        float margin;
568        float labelOffset = 0;
569        if (mFollowAxis == VERTICAL) {
570            if (mValidAfterDynamic != null) {
571                margin = getLabelTop(mValidAfterDynamic) - getLabelBottom(this);
572                if (margin < 0) {
573                    labelOffset = margin / 2;
574                }
575            } else if (mValidBeforeDynamic != null) {
576                margin = getLabelTop(this) - getLabelBottom(mValidBeforeDynamic);
577                if (margin < 0) {
578                    labelOffset = -margin / 2;
579                }
580            }
581        } else {
582            // TODO: implement horizontal labels
583        }
584
585        // when offsetting label, neighbor probably needs to offset too
586        if (labelOffset != 0) {
587            if (mValidAfterDynamic != null) mValidAfterDynamic.invalidate();
588            if (mValidBeforeDynamic != null) mValidBeforeDynamic.invalidate();
589        }
590
591        final int labelSize;
592        if (isEnabled() && mLabelLayout != null) {
593            final int count = canvas.save();
594            {
595                canvas.translate(mContentOffset.x, mContentOffset.y + labelOffset);
596                mLabelLayout.draw(canvas);
597            }
598            canvas.restoreToCount(count);
599            labelSize = mLabelSize;
600        } else {
601            labelSize = 0;
602        }
603
604        if (mFollowAxis == VERTICAL) {
605            mSweep.setBounds(labelSize, mSweepOffset.y, width,
606                    mSweepOffset.y + mSweep.getIntrinsicHeight());
607        } else {
608            mSweep.setBounds(mSweepOffset.x, labelSize,
609                    mSweepOffset.x + mSweep.getIntrinsicWidth(), height);
610        }
611
612        mSweep.draw(canvas);
613    }
614
615    public static float getLabelTop(ChartSweepView view) {
616        return view.getY() + view.mContentOffset.y;
617    }
618
619    public static float getLabelBottom(ChartSweepView view) {
620        return getLabelTop(view) + view.mLabelLayout.getHeight();
621    }
622}
623