1 |
/**
|
2 |
* Copyright 2008 by John Lussmyer
|
3 |
*
|
4 |
* http://casadelgato.com/content/numberpicker
|
5 |
*
|
6 |
*/
|
7 |
|
8 |
package com.casadelgato.widgets;
|
9 |
|
10 |
import android.content.Context;
|
11 |
import android.os.Handler;
|
12 |
import android.text.InputType;
|
13 |
import android.util.AttributeSet;
|
14 |
import android.util.Log;
|
15 |
import android.view.Gravity;
|
16 |
import android.view.MotionEvent;
|
17 |
import android.view.View;
|
18 |
import android.widget.Button;
|
19 |
import android.widget.EditText;
|
20 |
import android.widget.RelativeLayout;
|
21 |
|
22 |
/**
|
23 |
* This is a widget that provides a standard Spin button control for numbers.
|
24 |
* XML Attributes that may be set
|
25 |
* <ul>
|
26 |
* <li>plusminus_width - width of + and - buttons, defaults to WRAP_CONTENT</li>
|
27 |
* <li>plusminus_height - height of + and - buttons, defaults to WRAP_CONTENT</li>
|
28 |
* <li>textarea_width - width of text number field, defaults to WRAP_CONTENT</li>
|
29 |
* <li>textarea_height - height of text number field, defaults to WRAP_CONTENT</li>
|
30 |
* <li>textSize - size of text to use, integer, defaults to 25</li>
|
31 |
* <li>vertical - specifies vertical arrangement, defaults to false</li>
|
32 |
* <li>minValue - Minimum value allowed (defaults to 0)</li>
|
33 |
* <li>maxValue - Maximum value allowed (defaults to 100)</li>
|
34 |
* <li>defaultValue - default value (defaults to minValue)</li>
|
35 |
* <li>repeatInterval - initial milliseconds between repeats (defaults to 200)</li>
|
36 |
* <li>repeatAcceleration - Num milliseconds to decrement the repeat interval on each repeat (defaults to 0)</li>
|
37 |
* </ul>
|
38 |
*
|
39 |
* @author John Lussmyer
|
40 |
*/
|
41 |
public class NumberPicker extends RelativeLayout
|
42 |
{
|
43 |
|
44 |
/** If this is true, debug statements will be included */
|
45 |
private static final boolean debug = false;
|
46 |
|
47 |
private static final int DEF_MINVAL = 0;
|
48 |
private static final int DEF_MAXVAL = 100;
|
49 |
private static final int DEF_TXTSIZE = 25;
|
50 |
private static final int DEF_REPINT = 200;
|
51 |
/** Minimum Repeat interval */
|
52 |
private static final int MIN_REPINT = 40;
|
53 |
|
54 |
private int mIdDec = getNextID();
|
55 |
private int mIdInc = getNextID();
|
56 |
private int mIdTxt = getNextID();
|
57 |
private int mRepeatValue = 0;
|
58 |
private Button mBtnDec;
|
59 |
private Button mBtnInc;
|
60 |
private int mCurVal = DEF_MINVAL;
|
61 |
private int mMaxValue = DEF_MAXVAL;
|
62 |
private int mMinValue = DEF_MINVAL;
|
63 |
private EditText mTxtNum;
|
64 |
private int mTxtSize = DEF_TXTSIZE;
|
65 |
private int mRepeatDefInt = DEF_REPINT;
|
66 |
private int mRepeatInterval = DEF_REPINT;
|
67 |
private Handler mRepeatHandler = new Handler();
|
68 |
private int mRepeatAccel = 0;
|
69 |
private ValueChangeListener mVCListener;
|
70 |
private ButtonRepeater mRepeater = new ButtonRepeater();
|
71 |
|
72 |
private static int mNextID = 7734;
|
73 |
|
74 |
private static int getNextID()
|
75 |
{
|
76 |
return mNextID++;
|
77 |
}
|
78 |
|
79 |
/**
|
80 |
* Constructor used when building via XML entry
|
81 |
*
|
82 |
* @param context
|
83 |
* Context to use for controls
|
84 |
* @param attrs
|
85 |
* XML values
|
86 |
*/
|
87 |
public NumberPicker(Context context, AttributeSet attrs)
|
88 |
{
|
89 |
super(context, attrs);
|
90 |
|
91 |
// Attributes that may be set via XML tags
|
92 |
// TODO: font
|
93 |
boolean vertical = attrs.getAttributeBooleanValue(null, "vertical", false);
|
94 |
int btnW = attrs.getAttributeIntValue(null, "plusminus_width", android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
|
95 |
int btnH = attrs.getAttributeIntValue(null, "plusminus_height", android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
|
96 |
int txtW = attrs.getAttributeIntValue(null, "textarea_width", android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
|
97 |
int txtH = attrs.getAttributeIntValue(null, "textarea_height", android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
|
98 |
mTxtSize = attrs.getAttributeIntValue(null, "textSize", mTxtSize);
|
99 |
|
100 |
mMinValue = attrs.getAttributeIntValue(null, "minValue", mMinValue);
|
101 |
mMaxValue = attrs.getAttributeIntValue(null, "maxValue", mMaxValue);
|
102 |
mCurVal = attrs.getAttributeIntValue(null, "defaultValue", mMinValue);
|
103 |
|
104 |
mRepeatDefInt = attrs.getAttributeIntValue(null, "repeatInterval", mRepeatDefInt);
|
105 |
mRepeatAccel = attrs.getAttributeIntValue(null, "repeatAcceleration", mRepeatAccel);
|
106 |
|
107 |
buildContent(context, attrs, vertical, btnW, btnH, txtW, txtH);
|
108 |
|
109 |
return;
|
110 |
}
|
111 |
|
112 |
/**
|
113 |
* Constructor when building widget programmatically
|
114 |
*
|
115 |
* @param context
|
116 |
* Context for widget
|
117 |
* @param vertical
|
118 |
* true if vertical arrangement, false for horizontal
|
119 |
* @param btnW
|
120 |
* Width of +/- buttons
|
121 |
* @param btnH
|
122 |
* Height of +/- buttons
|
123 |
* @param txtW
|
124 |
* Width of number text field
|
125 |
* @param txtH
|
126 |
* Height of number text field
|
127 |
*/
|
128 |
public NumberPicker(Context context, boolean vertical, int btnW, int btnH, int txtW, int txtH)
|
129 |
{
|
130 |
super(context);
|
131 |
buildContent(context, null, vertical, btnW, btnH, txtW, txtH);
|
132 |
return;
|
133 |
}
|
134 |
|
135 |
/**
|
136 |
* Create all the sub widgets and arrange them appropriately.
|
137 |
*
|
138 |
* @param context
|
139 |
* Context for widget
|
140 |
* @param vertical
|
141 |
* true if vertical arrangement, false for horizontal
|
142 |
* @param btnW
|
143 |
* Width of +/- buttons
|
144 |
* @param btnH
|
145 |
* Height of +/- buttons
|
146 |
* @param txtW
|
147 |
* Width of number text field
|
148 |
* @param txtH
|
149 |
* Height of number text field
|
150 |
*/
|
151 |
private void buildContent(Context context, AttributeSet attrs, boolean vertical, int btnW, int btnH, int txtW, int txtH)
|
152 |
{
|
153 |
// Build the member control/widgets
|
154 |
mBtnInc = buildIncButton(context, attrs);
|
155 |
mBtnDec = buildDecButton(context, attrs);
|
156 |
mTxtNum = buildValueText(context, attrs);
|
157 |
|
158 |
RelativeLayout.LayoutParams lp;
|
159 |
|
160 |
lp = new RelativeLayout.LayoutParams(btnW, btnH);
|
161 |
if (vertical)
|
162 |
{
|
163 |
lp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
|
164 |
lp.addRule(RelativeLayout.CENTER_HORIZONTAL);
|
165 |
lp.addRule(RelativeLayout.ALIGN_LEFT, mIdInc);
|
166 |
lp.addRule(RelativeLayout.ALIGN_RIGHT, mIdInc);
|
167 |
}
|
168 |
else
|
169 |
{
|
170 |
// lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
|
171 |
lp.addRule(RelativeLayout.CENTER_VERTICAL);
|
172 |
}
|
173 |
addView(mBtnDec, lp);
|
174 |
|
175 |
lp = new RelativeLayout.LayoutParams(txtW, txtH);
|
176 |
if (vertical)
|
177 |
{
|
178 |
lp.addRule(RelativeLayout.ABOVE, mIdDec);
|
179 |
lp.addRule(RelativeLayout.BELOW, mIdInc);
|
180 |
lp.addRule(RelativeLayout.CENTER_HORIZONTAL);
|
181 |
}
|
182 |
else
|
183 |
{
|
184 |
lp.addRule(RelativeLayout.RIGHT_OF, mIdDec);
|
185 |
// lp.addRule(RelativeLayout.LEFT_OF, ID_INC);
|
186 |
lp.addRule(RelativeLayout.CENTER_VERTICAL);
|
187 |
}
|
188 |
addView(mTxtNum, lp);
|
189 |
|
190 |
lp = new RelativeLayout.LayoutParams(btnW, btnH);
|
191 |
if (vertical)
|
192 |
{
|
193 |
lp.addRule(RelativeLayout.ALIGN_PARENT_TOP);
|
194 |
lp.addRule(RelativeLayout.CENTER_HORIZONTAL);
|
195 |
}
|
196 |
else
|
197 |
{
|
198 |
lp.addRule(RelativeLayout.RIGHT_OF, mIdTxt);
|
199 |
lp.addRule(RelativeLayout.CENTER_VERTICAL);
|
200 |
}
|
201 |
addView(mBtnInc, lp);
|
202 |
}
|
203 |
|
204 |
/**
|
205 |
* Get the Maximum value this widget will allow
|
206 |
*
|
207 |
* @return Maximum spin value
|
208 |
*/
|
209 |
public int getMaxValue()
|
210 |
{
|
211 |
return mMaxValue;
|
212 |
}
|
213 |
|
214 |
/**
|
215 |
* Get the Minimum value this widget will allow
|
216 |
*
|
217 |
* @return Minimum spin value
|
218 |
*/
|
219 |
public int getMinValue()
|
220 |
{
|
221 |
return mMinValue;
|
222 |
}
|
223 |
|
224 |
/**
|
225 |
* Get the current value.
|
226 |
*
|
227 |
* @return Current value.
|
228 |
*/
|
229 |
public int getValue()
|
230 |
{
|
231 |
return mCurVal;
|
232 |
}
|
233 |
|
234 |
/**
|
235 |
* Increment the value by 1.
|
236 |
*/
|
237 |
public void increment()
|
238 |
{
|
239 |
int old = mCurVal;
|
240 |
mCurVal = Math.min(mMaxValue, mCurVal + 1);
|
241 |
|
242 |
if (mCurVal != old)
|
243 |
{
|
244 |
mTxtNum.setText(String.valueOf(mCurVal));
|
245 |
|
246 |
if (mVCListener != null)
|
247 |
{
|
248 |
mVCListener.onNumberPickerValueChange(this, mCurVal);
|
249 |
}
|
250 |
}
|
251 |
|
252 |
return;
|
253 |
}
|
254 |
|
255 |
/**
|
256 |
* Decrement the value by 1
|
257 |
*/
|
258 |
public void decrement()
|
259 |
{
|
260 |
int old = mCurVal;
|
261 |
mCurVal = Math.max(mMinValue, mCurVal - 1);
|
262 |
|
263 |
if (mCurVal != old)
|
264 |
{
|
265 |
mTxtNum.setText(String.valueOf(mCurVal));
|
266 |
|
267 |
if (mVCListener != null)
|
268 |
{
|
269 |
mVCListener.onNumberPickerValueChange(this, mCurVal);
|
270 |
}
|
271 |
}
|
272 |
|
273 |
return;
|
274 |
}
|
275 |
|
276 |
/**
|
277 |
* Build the Decrement button
|
278 |
*
|
279 |
* @param context
|
280 |
* @return Decrement Button
|
281 |
*/
|
282 |
private Button buildDecButton(Context context, AttributeSet attrs)
|
283 |
{
|
284 |
Button btn = new Button(context, attrs);
|
285 |
btn.setTextSize(mTxtSize);
|
286 |
btn.setText("-");
|
287 |
btn.setId(mIdDec);
|
288 |
|
289 |
// Decrement once for a click
|
290 |
btn.setOnClickListener(new View.OnClickListener()
|
291 |
{
|
292 |
public void onClick(View v)
|
293 |
{
|
294 |
decrement();
|
295 |
}
|
296 |
});
|
297 |
|
298 |
// Auto Decrement for a long click
|
299 |
btn.setOnLongClickListener(new View.OnLongClickListener()
|
300 |
{
|
301 |
public boolean onLongClick(View arg0)
|
302 |
{
|
303 |
startRepeating(-1);
|
304 |
return false;
|
305 |
}
|
306 |
});
|
307 |
|
308 |
// When the button is released, if we're auto decrementing, stop
|
309 |
btn.setOnTouchListener(new View.OnTouchListener()
|
310 |
{
|
311 |
public boolean onTouch(View v, MotionEvent event)
|
312 |
{
|
313 |
if (event.getAction() == MotionEvent.ACTION_UP)
|
314 |
{
|
315 |
startRepeating(0);
|
316 |
}
|
317 |
return false;
|
318 |
}
|
319 |
});
|
320 |
|
321 |
return btn;
|
322 |
}
|
323 |
|
324 |
/**
|
325 |
* Build the Increment button
|
326 |
*
|
327 |
* @param context
|
328 |
* @return Increment button
|
329 |
*/
|
330 |
private Button buildIncButton(Context context, AttributeSet attrs)
|
331 |
{
|
332 |
Button btn = new Button(context, attrs);
|
333 |
btn.setTextSize(mTxtSize);
|
334 |
btn.setText("+");
|
335 |
btn.setId(mIdInc);
|
336 |
|
337 |
// Increment once for a click
|
338 |
btn.setOnClickListener(new View.OnClickListener()
|
339 |
{
|
340 |
public void onClick(View v)
|
341 |
{
|
342 |
increment();
|
343 |
}
|
344 |
});
|
345 |
|
346 |
// Auto increment for a long click
|
347 |
btn.setOnLongClickListener(new View.OnLongClickListener()
|
348 |
{
|
349 |
public boolean onLongClick(View arg0)
|
350 |
{
|
351 |
startRepeating(1);
|
352 |
return false;
|
353 |
}
|
354 |
});
|
355 |
|
356 |
// When the button is released, if we're auto incrementing, stop
|
357 |
btn.setOnTouchListener(new View.OnTouchListener()
|
358 |
{
|
359 |
public boolean onTouch(View v, MotionEvent event)
|
360 |
{
|
361 |
if (event.getAction() == MotionEvent.ACTION_UP)
|
362 |
{
|
363 |
startRepeating(0); // stops it
|
364 |
}
|
365 |
return false;
|
366 |
}
|
367 |
});
|
368 |
|
369 |
return btn;
|
370 |
}
|
371 |
|
372 |
/**
|
373 |
* Build the text field
|
374 |
*
|
375 |
* @param context
|
376 |
* @return Text field for value display
|
377 |
*/
|
378 |
private EditText buildValueText(Context context, AttributeSet attrs)
|
379 |
{
|
380 |
EditText txt = new EditText(context, attrs);
|
381 |
|
382 |
txt.setTextSize(mTxtSize);
|
383 |
txt.setId(mIdTxt);
|
384 |
txt.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL);
|
385 |
txt.setText(String.valueOf(mCurVal));
|
386 |
txt.setInputType(InputType.TYPE_CLASS_NUMBER);
|
387 |
|
388 |
// Highlight the number when we get focus
|
389 |
txt.setOnFocusChangeListener(new View.OnFocusChangeListener()
|
390 |
{
|
391 |
public void onFocusChange(View v, boolean hasFocus)
|
392 |
{
|
393 |
if (hasFocus)
|
394 |
{
|
395 |
((EditText) v).selectAll();
|
396 |
}
|
397 |
else
|
398 |
{
|
399 |
int oldValue = mCurVal;
|
400 |
try
|
401 |
{
|
402 |
mCurVal = Integer.parseInt(((EditText) v).getText().toString());
|
403 |
|
404 |
if (mCurVal > mMaxValue)
|
405 |
{
|
406 |
mCurVal = mMaxValue;
|
407 |
}
|
408 |
else if (mCurVal < mMinValue)
|
409 |
{
|
410 |
mCurVal = mMinValue;
|
411 |
}
|
412 |
((EditText) v).setText(String.valueOf(mCurVal));
|
413 |
if (debug)
|
414 |
{
|
415 |
Log.d("NumberPicker", "NumberPicker.buildValueText: new value " + mCurVal);
|
416 |
}
|
417 |
if (mVCListener != null)
|
418 |
{
|
419 |
mVCListener.onNumberPickerValueChange(NumberPicker.this, mCurVal);
|
420 |
}
|
421 |
}
|
422 |
catch (NumberFormatException nfe)
|
423 |
{
|
424 |
mCurVal = oldValue;
|
425 |
if (debug)
|
426 |
{
|
427 |
Log.d("NumberPicker", "NumberPicker.buildValueText: bad value");
|
428 |
}
|
429 |
}
|
430 |
}
|
431 |
}
|
432 |
});
|
433 |
|
434 |
return txt;
|
435 |
}
|
436 |
|
437 |
/**
|
438 |
* Set the maximum value the control will display.
|
439 |
*
|
440 |
* @param maxValue
|
441 |
*/
|
442 |
public void setMaxValue(int maxValue)
|
443 |
{
|
444 |
mMaxValue = maxValue;
|
445 |
setValue(getValue()); // Make sure we are within the new limits
|
446 |
return;
|
447 |
}
|
448 |
|
449 |
/**
|
450 |
* Set the minimum value the control will display.
|
451 |
*
|
452 |
* @param minValue
|
453 |
*/
|
454 |
public void setMinValue(int minValue)
|
455 |
{
|
456 |
mMinValue = minValue;
|
457 |
setValue(getValue()); // Make sure we are within the new limits
|
458 |
return;
|
459 |
}
|
460 |
|
461 |
/**
|
462 |
* Set the current number for the spin control
|
463 |
*
|
464 |
* @param newVal
|
465 |
* New number to use
|
466 |
* @return Actual number used. (due to Min/Max)
|
467 |
*/
|
468 |
public int setValue(int newVal)
|
469 |
{
|
470 |
if (newVal > mMaxValue)
|
471 |
{
|
472 |
newVal = mMaxValue;
|
473 |
}
|
474 |
if (newVal < mMinValue)
|
475 |
{
|
476 |
newVal = mMinValue;
|
477 |
}
|
478 |
mCurVal = newVal;
|
479 |
mTxtNum.setText(String.valueOf(mCurVal));
|
480 |
mTxtNum.invalidate();
|
481 |
|
482 |
if (mVCListener != null)
|
483 |
{
|
484 |
mVCListener.onNumberPickerValueChange(this, mCurVal);
|
485 |
}
|
486 |
if (debug)
|
487 |
{
|
488 |
Log.d("Metronome", "NumberPicker.setValue: val=" + newVal + ", min=" + mMinValue + ", max=" + mMaxValue);
|
489 |
}
|
490 |
return mCurVal;
|
491 |
}
|
492 |
|
493 |
/**
|
494 |
* This is used to force display of some arbitrary text in the text field.
|
495 |
* It does NOT change the current value.
|
496 |
*
|
497 |
* @param txt
|
498 |
* Text to be displayed.
|
499 |
*/
|
500 |
public void setText(String txt)
|
501 |
{
|
502 |
mTxtNum.setText(txt);
|
503 |
return;
|
504 |
}
|
505 |
|
506 |
/**
|
507 |
* Interface for objects to be nofified of changes to the picker value.
|
508 |
*
|
509 |
* @author Cougar
|
510 |
*/
|
511 |
public interface ValueChangeListener
|
512 |
{
|
513 |
public void onNumberPickerValueChange(NumberPicker picker, int value);
|
514 |
}
|
515 |
|
516 |
/**
|
517 |
* Set the listener to be notified of changes to the picker value.
|
518 |
*
|
519 |
* @param listener
|
520 |
* listener to be called.
|
521 |
*/
|
522 |
public void setOnValueChangeListener(ValueChangeListener listener)
|
523 |
{
|
524 |
mVCListener = listener;
|
525 |
return;
|
526 |
}
|
527 |
|
528 |
/**
|
529 |
* Start auto repeating an increment/or decrement. Will stop when this is
|
530 |
* called with an inc of 0.
|
531 |
*
|
532 |
* @param inc
|
533 |
* Amount to change the value by each interval. 0 to stop.
|
534 |
*/
|
535 |
private void startRepeating(int inc)
|
536 |
{
|
537 |
mRepeatValue = inc;
|
538 |
if (inc != 0)
|
539 |
{
|
540 |
mRepeatHandler.postDelayed(mRepeater, mRepeatInterval);
|
541 |
}
|
542 |
return;
|
543 |
}
|
544 |
|
545 |
/**
|
546 |
* Handle doing continuous increments or decrements if the button is held
|
547 |
* down. It stops when mRepeatValue is set to 0.
|
548 |
*
|
549 |
* @author Cougar
|
550 |
*/
|
551 |
private class ButtonRepeater implements Runnable
|
552 |
{
|
553 |
public void run()
|
554 |
{
|
555 |
if (mRepeatValue > 0)
|
556 |
{
|
557 |
increment();
|
558 |
}
|
559 |
else if (mRepeatValue < 0)
|
560 |
{
|
561 |
decrement();
|
562 |
}
|
563 |
if (mRepeatValue != 0)
|
564 |
{
|
565 |
mRepeatHandler.postDelayed(mRepeater, mRepeatInterval);
|
566 |
mRepeatInterval -= mRepeatAccel;
|
567 |
if (mRepeatInterval < MIN_REPINT)
|
568 |
{
|
569 |
mRepeatInterval = MIN_REPINT;
|
570 |
}
|
571 |
}
|
572 |
else
|
573 |
{ // Restore to default repeat rate
|
574 |
mRepeatInterval = mRepeatDefInt;
|
575 |
}
|
576 |
return;
|
577 |
}
|
578 |
}
|
579 |
|
580 |
}
|