See {@link R.styleable#ValueBar ValueBar Attributes}
23 | */ 24 | public class ValueBar extends View { 25 | 26 | private int maxValue = 100; 27 | private int currentValue = 0; 28 | 29 | private boolean animated; 30 | private float valueToDraw; //for use during an animation 31 | private long animationDuration = 4000l; //4 second default. this is the time it takes to traverse the entire bar 32 | ValueAnimator animation = null; 33 | 34 | //instance variables for storing xml attributes 35 | private int barHeight; 36 | private int circleRadius; 37 | private int spaceAfterBar; 38 | private int circleTextSize; 39 | private int maxValueTextSize; 40 | private int labelTextSize; 41 | private int labelTextColor; 42 | private int currentValueTextColor; 43 | private int circleTextColor; 44 | private int baseColor; 45 | private int fillColor; 46 | 47 | private String labelText; 48 | 49 | //objects used for drawing 50 | private Paint labelPaint; 51 | private Paint maxValuePaint; 52 | private Paint barBasePaint; 53 | private Paint barFillPaint; 54 | private Paint circlePaint; 55 | private Paint currentValuePaint; 56 | 57 | 58 | public ValueBar(Context context, AttributeSet attrs) { 59 | super(context, attrs); 60 | init(context, attrs); 61 | } 62 | 63 | /** 64 | * Set the maximum value that will be allowed 65 | * 66 | * @param maxValue 67 | */ 68 | public void setMaxValue(int maxValue) { 69 | this.maxValue = maxValue; 70 | invalidate(); 71 | requestLayout(); 72 | } 73 | 74 | /** 75 | * Sets the value of the bar. If the passed in value exceeds the maximum, the value 76 | * will be set to the maximum. 77 | * 78 | * @param newValue 79 | */ 80 | public void setValue(int newValue) { 81 | int previousValue = currentValue; 82 | if(newValue < 0) { 83 | currentValue = 0; 84 | } else if (newValue > maxValue) { 85 | currentValue = maxValue; 86 | } else { 87 | currentValue = newValue; 88 | } 89 | 90 | if(animation != null) { 91 | animation.cancel(); 92 | } 93 | 94 | if(animated) { 95 | animation = ValueAnimator.ofFloat(previousValue, currentValue); 96 | //animationDuration specifies how long it should take to animate the entire graph, so the 97 | //actual value to use depends on how much the value needs to change 98 | int changeInValue = Math.abs(currentValue - previousValue); 99 | long durationToUse = (long) (animationDuration * ((float) changeInValue / (float) maxValue)); 100 | animation.setDuration(durationToUse); 101 | 102 | animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 103 | @Override 104 | public void onAnimationUpdate(ValueAnimator valueAnimator) { 105 | valueToDraw = (float) valueAnimator.getAnimatedValue(); 106 | ValueBar.this.invalidate(); 107 | } 108 | }); 109 | 110 | animation.start(); 111 | } else { 112 | valueToDraw = currentValue; 113 | } 114 | 115 | invalidate(); 116 | } 117 | 118 | /** 119 | * Get the current value 120 | * 121 | * @return 122 | */ 123 | public int getValue() { 124 | return currentValue; 125 | } 126 | 127 | private void init(Context context, AttributeSet attrs) { 128 | setSaveEnabled(true); 129 | 130 | //read xml attributes 131 | TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ValueBar, 0, 0); 132 | barHeight = ta.getDimensionPixelSize(R.styleable.ValueBar_barHeight, 0); 133 | circleRadius = ta.getDimensionPixelSize(R.styleable.ValueBar_circleRadius, 0); 134 | spaceAfterBar = ta.getDimensionPixelSize(R.styleable.ValueBar_spaceAfterBar, 0); 135 | circleTextSize = ta.getDimensionPixelSize(R.styleable.ValueBar_circleTextSize, 0); 136 | maxValueTextSize = ta.getDimensionPixelSize(R.styleable.ValueBar_maxValueTextSize, 0); 137 | labelTextSize = ta.getDimensionPixelSize(R.styleable.ValueBar_labelTextSize, 0); 138 | labelTextColor = ta.getColor(R.styleable.ValueBar_labelTextColor, Color.BLACK); 139 | currentValueTextColor = ta.getColor(R.styleable.ValueBar_maxValueTextColor, Color.BLACK); 140 | circleTextColor = ta.getColor(R.styleable.ValueBar_circleTextColor, Color.BLACK); 141 | baseColor = ta.getColor(R.styleable.ValueBar_baseColor, Color.BLACK); 142 | fillColor = ta.getColor(R.styleable.ValueBar_fillColor, Color.BLACK); 143 | labelText = ta.getString(R.styleable.ValueBar_labelText); 144 | ta.recycle(); 145 | 146 | labelPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 147 | labelPaint.setTextSize(labelTextSize); 148 | labelPaint.setColor(labelTextColor); 149 | labelPaint.setTextAlign(Paint.Align.LEFT); 150 | labelPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); 151 | 152 | maxValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 153 | maxValuePaint.setTextSize(maxValueTextSize); 154 | maxValuePaint.setColor(currentValueTextColor); 155 | maxValuePaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); 156 | maxValuePaint.setTextAlign(Paint.Align.RIGHT); 157 | 158 | barBasePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 159 | barBasePaint.setColor(baseColor); 160 | 161 | barFillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 162 | barFillPaint.setColor(fillColor); 163 | 164 | circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 165 | circlePaint.setColor(fillColor); 166 | 167 | currentValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 168 | currentValuePaint.setTextSize(circleTextSize); 169 | currentValuePaint.setColor(circleTextColor); 170 | currentValuePaint.setTextAlign(Paint.Align.CENTER); 171 | } 172 | 173 | private int measureHeight(int measureSpec) { 174 | 175 | int size = getPaddingTop() + getPaddingBottom(); 176 | size += labelPaint.getFontSpacing(); 177 | float maxValueTextSpacing = maxValuePaint.getFontSpacing(); 178 | size += Math.max(maxValueTextSpacing, Math.max(barHeight, circleRadius * 2)); 179 | 180 | return resolveSizeAndState(size, measureSpec, 0); 181 | } 182 | 183 | private int measureWidth(int measureSpec) { 184 | 185 | int size = getPaddingLeft() + getPaddingRight(); 186 | Rect bounds = new Rect(); 187 | labelPaint.getTextBounds(labelText, 0, labelText.length(), bounds); 188 | size += bounds.width(); 189 | 190 | bounds = new Rect(); 191 | String maxValueText = String.valueOf(maxValue); 192 | maxValuePaint.getTextBounds(maxValueText, 0, maxValueText.length(), bounds); 193 | size += bounds.width(); 194 | 195 | return resolveSizeAndState(size, measureSpec, 0); 196 | } 197 | 198 | @Override 199 | protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { 200 | setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec)); 201 | } 202 | 203 | @Override 204 | protected void onDraw (Canvas canvas) { 205 | drawLabel(canvas); 206 | drawBar(canvas); 207 | drawMaxValue(canvas); 208 | } 209 | 210 | private void drawLabel(Canvas canvas) { 211 | float x = getPaddingLeft(); 212 | //the y coordinate marks the bottom of the text, so we need to factor in the height 213 | Rect bounds = new Rect(); 214 | labelPaint.getTextBounds(labelText, 0, labelText.length(), bounds); 215 | float y = getPaddingTop() + bounds.height(); 216 | canvas.drawText(labelText, x, y, labelPaint); 217 | } 218 | 219 | private void drawBar(Canvas canvas) { 220 | String maxValueString = String.valueOf(maxValue); 221 | Rect maxValueRect = new Rect(); 222 | maxValuePaint.getTextBounds(maxValueString, 0, maxValueString.length(), maxValueRect); 223 | float barLength = getWidth() - getPaddingRight() - getPaddingLeft() - circleRadius - maxValueRect.width() - spaceAfterBar; 224 | 225 | float barCenter = getBarCenter(); 226 | 227 | float halfBarHeight = barHeight / 2; 228 | float top = barCenter - halfBarHeight; 229 | float bottom = barCenter + halfBarHeight; 230 | float left = getPaddingLeft(); 231 | float right = getPaddingLeft() + barLength; 232 | RectF rect = new RectF(left, top, right, bottom); 233 | canvas.drawRoundRect(rect, halfBarHeight, halfBarHeight, barBasePaint); 234 | 235 | 236 | float percentFilled = (float) valueToDraw / (float) maxValue; 237 | float fillLength = barLength * percentFilled; 238 | float fillPosition = left + fillLength; 239 | RectF fillRect = new RectF(left, top, fillPosition, bottom); 240 | canvas.drawRoundRect(fillRect, halfBarHeight, halfBarHeight, barFillPaint); 241 | 242 | canvas.drawCircle(fillPosition, barCenter, circleRadius, circlePaint); 243 | 244 | Rect bounds = new Rect(); 245 | String valueString = String.valueOf(Math.round(valueToDraw)); 246 | currentValuePaint.getTextBounds(valueString, 0, valueString.length(), bounds); 247 | float y = barCenter + (bounds.height() / 2); 248 | canvas.drawText(valueString, fillPosition, y, currentValuePaint); 249 | } 250 | 251 | private void drawMaxValue(Canvas canvas) { 252 | String maxValue = String.valueOf(this.maxValue); 253 | Rect maxValueRect = new Rect(); 254 | maxValuePaint.getTextBounds(maxValue, 0, maxValue.length(), maxValueRect); 255 | 256 | float xPos = getWidth() - getPaddingRight(); 257 | float yPos = getBarCenter() + maxValueRect.height() / 2; 258 | canvas.drawText(maxValue, xPos, yPos, maxValuePaint); 259 | 260 | } 261 | 262 | private float getBarCenter() { 263 | //position the bar slightly below the middle of the drawable area 264 | float barCenter = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2; //this is the center 265 | barCenter += getPaddingTop() + .1f * getHeight(); //move it down a bit 266 | return barCenter; 267 | } 268 | 269 | /** 270 | * Indicate whether or not the graph should use animation when the value is changed. If true, the 271 | * indicator will "slide" to the new value. 272 | * 273 | *See {@link #setAnimationDuration(long)}
274 | * 275 | * @param animated whether or not the graph should use animation 276 | */ 277 | public void setAnimated(boolean animated) { 278 | this.animated = animated; 279 | } 280 | 281 | /** 282 | * Set the time (in milliseconds) that the animation should use to traverse the entire graph. The actual 283 | * value used will depend on how much tha value changes. 284 | * 285 | * @param animationDuration duration of the animation 286 | */ 287 | public void setAnimationDuration(long animationDuration) { 288 | this.animationDuration = animationDuration; 289 | } 290 | 291 | @Override 292 | public Parcelable onSaveInstanceState() { 293 | Parcelable superState = super.onSaveInstanceState(); 294 | SavedState ss = new SavedState(superState); 295 | ss.value = currentValue; 296 | return ss; 297 | } 298 | 299 | @Override 300 | public void onRestoreInstanceState(Parcelable state) { 301 | SavedState ss = (SavedState) state; 302 | super.onRestoreInstanceState(ss.getSuperState()); 303 | currentValue = ss.value; 304 | valueToDraw = currentValue; //set valueToDraw directly to prevent re-animation 305 | } 306 | 307 | private static class SavedState extends BaseSavedState { 308 | int value; 309 | 310 | SavedState(Parcelable superState) { 311 | super(superState); 312 | } 313 | 314 | private SavedState(Parcel in) { 315 | super(in); 316 | value = in.readInt(); 317 | } 318 | 319 | @Override 320 | public void writeToParcel(Parcel out, int flags) { 321 | super.writeToParcel(out, flags); 322 | out.writeInt(value); 323 | } 324 | 325 | public static final Parcelable.Creator15 | * This view can be configured with a minimum and maximum value. There is also a label that will 16 | * display below the current value. 17 | *
18 | * 19 | */ 20 | public class ValueSelector extends RelativeLayout { 21 | 22 | private int minValue = Integer.MIN_VALUE; 23 | private int maxValue = Integer.MAX_VALUE; 24 | 25 | private boolean plusButtonIsPressed = false; 26 | private boolean minusButtonIsPressed = false; 27 | private final long REPEAT_INTERVAL_MS = 100l; 28 | 29 | View rootView; 30 | TextView valueTextView; 31 | View minusButton; 32 | View plusButton; 33 | 34 | Handler handler = new Handler(); 35 | 36 | public ValueSelector(Context context) { 37 | super(context); 38 | init(context); 39 | } 40 | 41 | public ValueSelector(Context context, AttributeSet attrs) { 42 | super(context, attrs); 43 | init(context); 44 | } 45 | 46 | public ValueSelector(Context context, AttributeSet attrs, int defStyle) { 47 | super(context, attrs, defStyle); 48 | init(context); 49 | } 50 | 51 | /** 52 | * Get the current minimum value that is allowed 53 | * 54 | * @return 55 | */ 56 | public int getMinValue() { 57 | return minValue; 58 | } 59 | 60 | /** 61 | * Set the minimum value that will be allowed 62 | * 63 | * @param minValue 64 | */ 65 | public void setMinValue(int minValue) { 66 | this.minValue = minValue; 67 | } 68 | 69 | /** 70 | * Get the current maximum value that is allowed 71 | * 72 | * @return 73 | */ 74 | public int getMaxValue() { 75 | return maxValue; 76 | } 77 | 78 | /** 79 | * Set the maximum value that will be allowed 80 | * 81 | * @param maxValue 82 | */ 83 | public void setMaxValue(int maxValue) { 84 | this.maxValue = maxValue; 85 | } 86 | 87 | /** 88 | * Get the current value 89 | * 90 | * @return the current value 91 | */ 92 | public int getValue() { 93 | return Integer.valueOf(valueTextView.getText().toString()); 94 | } 95 | 96 | /** 97 | * Set the current value. If the passed in value exceeds the current min or max, the value 98 | * will be set to the respective min/max. 99 | * 100 | * @param newValue new value 101 | */ 102 | public void setValue(int newValue) { 103 | int value = newValue; 104 | if(newValue < minValue) { 105 | value = minValue; 106 | } else if (newValue > maxValue) { 107 | value = maxValue; 108 | } 109 | 110 | valueTextView.setText(String.valueOf(value)); 111 | } 112 | 113 | private void init(Context context) { 114 | rootView = inflate(context, R.layout.value_selector, this); 115 | valueTextView = (TextView) rootView.findViewById(R.id.valueTextView); 116 | 117 | minusButton = rootView.findViewById(R.id.minusButton); 118 | plusButton = rootView.findViewById(R.id.plusButton); 119 | 120 | minusButton.setOnClickListener(new View.OnClickListener() { 121 | @Override 122 | public void onClick(View v) { 123 | decrementValue(); 124 | } 125 | }); 126 | minusButton.setOnLongClickListener( 127 | new View.OnLongClickListener() { 128 | @Override 129 | public boolean onLongClick(View arg0) { 130 | minusButtonIsPressed = true; 131 | handler.post(new AutoDecrementer()); 132 | return false; 133 | } 134 | } 135 | ); 136 | minusButton.setOnTouchListener(new View.OnTouchListener() { 137 | @Override 138 | public boolean onTouch(View v, MotionEvent event) { 139 | if ((event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL)) { 140 | minusButtonIsPressed = false; 141 | } 142 | return false; 143 | } 144 | }); 145 | 146 | plusButton.setOnClickListener(new View.OnClickListener() { 147 | @Override 148 | public void onClick(View v) { 149 | incrementValue(); 150 | } 151 | }); 152 | plusButton.setOnLongClickListener( 153 | new View.OnLongClickListener() { 154 | @Override 155 | public boolean onLongClick(View arg0) { 156 | plusButtonIsPressed = true; 157 | handler.post(new AutoIncrementer()); 158 | return false; 159 | } 160 | } 161 | ); 162 | 163 | plusButton.setOnTouchListener(new View.OnTouchListener() { 164 | @Override 165 | public boolean onTouch(View v, MotionEvent event) { 166 | if ((event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL)) { 167 | plusButtonIsPressed = false; 168 | } 169 | return false; 170 | } 171 | }); 172 | } 173 | 174 | private void incrementValue() { 175 | int currentVal = Integer.valueOf(valueTextView.getText().toString()); 176 | if(currentVal < maxValue) { 177 | valueTextView.setText(String.valueOf(currentVal + 1)); 178 | } 179 | } 180 | 181 | private void decrementValue() { 182 | int currentVal = Integer.valueOf(valueTextView.getText().toString()); 183 | if(currentVal > minValue) { 184 | valueTextView.setText(String.valueOf(currentVal - 1)); 185 | } 186 | } 187 | 188 | private class AutoIncrementer implements Runnable { 189 | @Override 190 | public void run() { 191 | if(plusButtonIsPressed){ 192 | incrementValue(); 193 | handler.postDelayed( new AutoIncrementer(), REPEAT_INTERVAL_MS); 194 | } 195 | } 196 | } 197 | private class AutoDecrementer implements Runnable { 198 | @Override 199 | public void run() { 200 | if(minusButtonIsPressed){ 201 | decrementValue(); 202 | handler.postDelayed(new AutoDecrementer(), REPEAT_INTERVAL_MS); 203 | } 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /customviews/src/main/res/drawable/valueselect_minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntertechInc/android-custom-view-tutorial/fbf85ce8270c06a0cab5e732ed7326dfffedecb2/customviews/src/main/res/drawable/valueselect_minus.png -------------------------------------------------------------------------------- /customviews/src/main/res/drawable/valueselect_minus_state.xml: -------------------------------------------------------------------------------- 1 |