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