2 * Copyright (C) 2008 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package android.widget;
19 import com.android.internal.R;
21 import android.annotation.Widget;
22 import android.content.Context;
23 import android.os.Handler;
24 import android.text.InputFilter;
25 import android.text.InputType;
26 import android.text.Spanned;
27 import android.text.method.NumberKeyListener;
28 import android.util.AttributeSet;
29 import android.view.LayoutInflater;
30 import android.view.View;
33 * A view for selecting a number
35 * For a dialog using this view, see {@link android.app.TimePickerDialog}.
39 public class NumberPicker extends LinearLayout {
42 * The callback interface used to indicate the number value has been adjusted.
44 public interface OnChangedListener {
46 * @param picker The NumberPicker associated with this listener.
47 * @param oldVal The previous value.
48 * @param newVal The new value.
50 void onChanged(NumberPicker picker, int oldVal, int newVal);
54 * Interface used to format the number into a string for presentation
56 public interface Formatter {
57 String toString(int value);
61 * Use a custom NumberPicker formatting callback to use two-digit
62 * minutes strings like "01". Keeping a static formatter etc. is the
63 * most efficient way to do this; it avoids creating temporary objects
64 * on every call to format().
66 public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER =
67 new NumberPicker.Formatter() {
68 final StringBuilder mBuilder = new StringBuilder();
69 final java.util.Formatter mFmt = new java.util.Formatter(mBuilder);
70 final Object[] mArgs = new Object[1];
71 public String toString(int value) {
73 mBuilder.delete(0, mBuilder.length());
74 mFmt.format("%02d", mArgs);
75 return mFmt.toString();
79 private final Handler mHandler;
80 private final Runnable mRunnable = new Runnable() {
83 changeCurrent(mCurrent + 1);
84 mHandler.postDelayed(this, mSpeed);
85 } else if (mDecrement) {
86 changeCurrent(mCurrent - 1);
87 mHandler.postDelayed(this, mSpeed);
92 private final EditText mText;
93 private final InputFilter mNumberInputFilter;
95 private String[] mDisplayedValues;
98 * Lower value of the range of numbers allowed for the NumberPicker
103 * Upper value of the range of numbers allowed for the NumberPicker
108 * Current value of this NumberPicker
110 private int mCurrent;
113 * Previous value of this NumberPicker.
115 private int mPrevious;
116 private OnChangedListener mListener;
117 private Formatter mFormatter;
118 private long mSpeed = 300;
120 private boolean mIncrement;
121 private boolean mDecrement;
124 * Create a new number picker
125 * @param context the application environment
127 public NumberPicker(Context context) {
132 * Create a new number picker
133 * @param context the application environment
134 * @param attrs a collection of attributes
136 public NumberPicker(Context context, AttributeSet attrs) {
137 super(context, attrs);
138 setOrientation(VERTICAL);
139 LayoutInflater inflater =
140 (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
141 inflater.inflate(R.layout.number_picker, this, true);
142 mHandler = new Handler();
144 OnClickListener clickListener = new OnClickListener() {
145 public void onClick(View v) {
146 validateInput(mText);
147 if (!mText.hasFocus()) mText.requestFocus();
149 // now perform the increment/decrement
150 if (R.id.increment == v.getId()) {
151 changeCurrent(mCurrent + 1);
152 } else if (R.id.decrement == v.getId()) {
153 changeCurrent(mCurrent - 1);
158 OnFocusChangeListener focusListener = new OnFocusChangeListener() {
159 public void onFocusChange(View v, boolean hasFocus) {
161 /* When focus is lost check that the text field
170 OnLongClickListener longClickListener = new OnLongClickListener() {
172 * We start the long click here but rely on the {@link NumberPickerButton}
173 * to inform us when the long click has ended.
175 public boolean onLongClick(View v) {
176 /* The text view may still have focus so clear it's focus which will
177 * trigger the on focus changed and any typed values to be pulled.
181 if (R.id.increment == v.getId()) {
183 mHandler.post(mRunnable);
184 } else if (R.id.decrement == v.getId()) {
186 mHandler.post(mRunnable);
192 InputFilter inputFilter = new NumberPickerInputFilter();
193 mNumberInputFilter = new NumberRangeKeyListener();
194 mIncrementButton = (NumberPickerButton) findViewById(R.id.increment);
195 mIncrementButton.setOnClickListener(clickListener);
196 mIncrementButton.setOnLongClickListener(longClickListener);
197 mIncrementButton.setNumberPicker(this);
199 mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement);
200 mDecrementButton.setOnClickListener(clickListener);
201 mDecrementButton.setOnLongClickListener(longClickListener);
202 mDecrementButton.setNumberPicker(this);
204 mText = (EditText) findViewById(R.id.timepicker_input);
205 mText.setOnFocusChangeListener(focusListener);
206 mText.setFilters(new InputFilter[] {inputFilter});
207 mText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
215 * Set the enabled state of this view. The interpretation of the enabled
216 * state varies by subclass.
218 * @param enabled True if this view is enabled, false otherwise.
221 public void setEnabled(boolean enabled) {
222 super.setEnabled(enabled);
223 mIncrementButton.setEnabled(enabled);
224 mDecrementButton.setEnabled(enabled);
225 mText.setEnabled(enabled);
229 * Set the callback that indicates the number has been adjusted by the user.
230 * @param listener the callback, should not be null.
232 public void setOnChangeListener(OnChangedListener listener) {
233 mListener = listener;
237 * Set the formatter that will be used to format the number for presentation
238 * @param formatter the formatter object. If formatter is null, String.valueOf()
241 public void setFormatter(Formatter formatter) {
242 mFormatter = formatter;
246 * Set the range of numbers allowed for the number picker. The current
247 * value will be automatically set to the start.
249 * @param start the start of the range (inclusive)
250 * @param end the end of the range (inclusive)
252 public void setRange(int start, int end) {
253 setRange(start, end, null/*displayedValues*/);
257 * Set the range of numbers allowed for the number picker. The current
258 * value will be automatically set to the start. Also provide a mapping
259 * for values used to display to the user.
261 * @param start the start of the range (inclusive)
262 * @param end the end of the range (inclusive)
263 * @param displayedValues the values displayed to the user.
265 public void setRange(int start, int end, String[] displayedValues) {
266 mDisplayedValues = displayedValues;
272 if (displayedValues != null) {
273 // Allow text entry rather than strictly numeric entry.
274 mText.setRawInputType(InputType.TYPE_CLASS_TEXT |
275 InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
280 * Set the current value for the number picker.
282 * @param current the current value the start of the range (inclusive)
283 * @throws IllegalArgumentException when current is not within the range
284 * of of the number picker
286 public void setCurrent(int current) {
287 if (current < mStart || current > mEnd) {
288 throw new IllegalArgumentException(
289 "current should be >= start and <= end");
296 * Sets the speed at which the numbers will scroll when the +/-
297 * buttons are longpressed
299 * @param speed The speed (in milliseconds) at which the numbers will scroll
302 public void setSpeed(long speed) {
306 private String formatNumber(int value) {
307 return (mFormatter != null)
308 ? mFormatter.toString(value)
309 : String.valueOf(value);
313 * Sets the current value of this NumberPicker, and sets mPrevious to the previous
314 * value. If current is greater than mEnd less than mStart, the value of mCurrent
317 * Subclasses can override this to change the wrapping behavior
319 * @param current the new value of the NumberPicker
321 protected void changeCurrent(int current) {
322 // Wrap around the values if we go past the start or end
323 if (current > mEnd) {
325 } else if (current < mStart) {
328 mPrevious = mCurrent;
335 * Notifies the listener, if registered, of a change of the value of this
338 private void notifyChange() {
339 if (mListener != null) {
340 mListener.onChanged(this, mPrevious, mCurrent);
345 * Updates the view of this NumberPicker. If displayValues were specified
346 * in {@link #setRange}, the string corresponding to the index specified by
347 * the current value will be returned. Otherwise, the formatter specified
348 * in {@link setFormatter} will be used to format the number.
350 private void updateView() {
351 /* If we don't have displayed values then use the
352 * current number else find the correct value in the
353 * displayed values for the current number.
355 if (mDisplayedValues == null) {
356 mText.setText(formatNumber(mCurrent));
358 mText.setText(mDisplayedValues[mCurrent - mStart]);
360 mText.setSelection(mText.getText().length());
363 private void validateCurrentView(CharSequence str) {
364 int val = getSelectedPos(str.toString());
365 if ((val >= mStart) && (val <= mEnd)) {
366 if (mCurrent != val) {
367 mPrevious = mCurrent;
375 private void validateInput(View v) {
376 String str = String.valueOf(((TextView) v).getText());
377 if ("".equals(str)) {
379 // Restore to the old value as we don't allow empty values
383 // Check the new value and ensure it's in range
384 validateCurrentView(str);
391 public void cancelIncrement() {
398 public void cancelDecrement() {
402 private static final char[] DIGIT_CHARACTERS = new char[] {
403 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
406 private NumberPickerButton mIncrementButton;
407 private NumberPickerButton mDecrementButton;
409 private class NumberPickerInputFilter implements InputFilter {
410 public CharSequence filter(CharSequence source, int start, int end,
411 Spanned dest, int dstart, int dend) {
412 if (mDisplayedValues == null) {
413 return mNumberInputFilter.filter(source, start, end, dest, dstart, dend);
415 CharSequence filtered = String.valueOf(source.subSequence(start, end));
416 String result = String.valueOf(dest.subSequence(0, dstart))
418 + dest.subSequence(dend, dest.length());
419 String str = String.valueOf(result).toLowerCase();
420 for (String val : mDisplayedValues) {
421 val = val.toLowerCase();
422 if (val.startsWith(str)) {
430 private class NumberRangeKeyListener extends NumberKeyListener {
432 // XXX This doesn't allow for range limits when controlled by a
433 // soft input method!
434 public int getInputType() {
435 return InputType.TYPE_CLASS_NUMBER;
439 protected char[] getAcceptedChars() {
440 return DIGIT_CHARACTERS;
444 public CharSequence filter(CharSequence source, int start, int end,
445 Spanned dest, int dstart, int dend) {
447 CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
448 if (filtered == null) {
449 filtered = source.subSequence(start, end);
452 String result = String.valueOf(dest.subSequence(0, dstart))
454 + dest.subSequence(dend, dest.length());
456 if ("".equals(result)) {
459 int val = getSelectedPos(result);
461 /* Ensure the user can't type in a value greater
462 * than the max allowed. We have to allow less than min
463 * as the user might want to delete some numbers
464 * and then type a new number.
474 private int getSelectedPos(String str) {
475 if (mDisplayedValues == null) {
477 return Integer.parseInt(str);
478 } catch (NumberFormatException e) {
479 /* Ignore as if it's not a number we don't care */
482 for (int i = 0; i < mDisplayedValues.length; i++) {
483 /* Don't force the user to type in jan when ja will do */
484 str = str.toLowerCase();
485 if (mDisplayedValues[i].toLowerCase().startsWith(str)) {
490 /* The user might have typed in a number into the month field i.e.
491 * 10 instead of OCT so support that too.
494 return Integer.parseInt(str);
495 } catch (NumberFormatException e) {
497 /* Ignore as if it's not a number we don't care */
504 * Returns the current value of the NumberPicker
505 * @return the current value.
507 public int getCurrent() {
512 * Returns the upper value of the range of the NumberPicker
513 * @return the uppper number of the range.
515 protected int getEndRange() {
520 * Returns the lower value of the range of the NumberPicker
521 * @return the lower number of the range.
523 protected int getBeginRange() {