• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 
17 package android.widget;
18 
19 import com.android.internal.R;
20 
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;
31 
32 /**
33  * A view for selecting a number
34  *
35  * For a dialog using this view, see {@link android.app.TimePickerDialog}.
36  * @hide
37  */
38 @Widget
39 public class NumberPicker extends LinearLayout {
40 
41     /**
42      * The callback interface used to indicate the number value has been adjusted.
43      */
44     public interface OnChangedListener {
45         /**
46          * @param picker The NumberPicker associated with this listener.
47          * @param oldVal The previous value.
48          * @param newVal The new value.
49          */
onChanged(NumberPicker picker, int oldVal, int newVal)50         void onChanged(NumberPicker picker, int oldVal, int newVal);
51     }
52 
53     /**
54      * Interface used to format the number into a string for presentation
55      */
56     public interface Formatter {
toString(int value)57         String toString(int value);
58     }
59 
60     /*
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().
65      */
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) {
72                     mArgs[0] = value;
73                     mBuilder.delete(0, mBuilder.length());
74                     mFmt.format("%02d", mArgs);
75                     return mFmt.toString();
76                 }
77         };
78 
79     private final Handler mHandler;
80     private final Runnable mRunnable = new Runnable() {
81         public void run() {
82             if (mIncrement) {
83                 changeCurrent(mCurrent + 1);
84                 mHandler.postDelayed(this, mSpeed);
85             } else if (mDecrement) {
86                 changeCurrent(mCurrent - 1);
87                 mHandler.postDelayed(this, mSpeed);
88             }
89         }
90     };
91 
92     private final EditText mText;
93     private final InputFilter mNumberInputFilter;
94 
95     private String[] mDisplayedValues;
96 
97     /**
98      * Lower value of the range of numbers allowed for the NumberPicker
99      */
100     private int mStart;
101 
102     /**
103      * Upper value of the range of numbers allowed for the NumberPicker
104      */
105     private int mEnd;
106 
107     /**
108      * Current value of this NumberPicker
109      */
110     private int mCurrent;
111 
112     /**
113      * Previous value of this NumberPicker.
114      */
115     private int mPrevious;
116     private OnChangedListener mListener;
117     private Formatter mFormatter;
118     private long mSpeed = 300;
119 
120     private boolean mIncrement;
121     private boolean mDecrement;
122 
123     /**
124      * Create a new number picker
125      * @param context the application environment
126      */
NumberPicker(Context context)127     public NumberPicker(Context context) {
128         this(context, null);
129     }
130 
131     /**
132      * Create a new number picker
133      * @param context the application environment
134      * @param attrs a collection of attributes
135      */
NumberPicker(Context context, AttributeSet attrs)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();
143 
144         OnClickListener clickListener = new OnClickListener() {
145             public void onClick(View v) {
146                 validateInput(mText);
147                 if (!mText.hasFocus()) mText.requestFocus();
148 
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);
154                 }
155             }
156         };
157 
158         OnFocusChangeListener focusListener = new OnFocusChangeListener() {
159             public void onFocusChange(View v, boolean hasFocus) {
160 
161                 /* When focus is lost check that the text field
162                  * has valid values.
163                  */
164                 if (!hasFocus) {
165                     validateInput(v);
166                 }
167             }
168         };
169 
170         OnLongClickListener longClickListener = new OnLongClickListener() {
171             /**
172              * We start the long click here but rely on the {@link NumberPickerButton}
173              * to inform us when the long click has ended.
174              */
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.
178                  */
179                 mText.clearFocus();
180 
181                 if (R.id.increment == v.getId()) {
182                     mIncrement = true;
183                     mHandler.post(mRunnable);
184                 } else if (R.id.decrement == v.getId()) {
185                     mDecrement = true;
186                     mHandler.post(mRunnable);
187                 }
188                 return true;
189             }
190         };
191 
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);
198 
199         mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement);
200         mDecrementButton.setOnClickListener(clickListener);
201         mDecrementButton.setOnLongClickListener(longClickListener);
202         mDecrementButton.setNumberPicker(this);
203 
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);
208 
209         if (!isEnabled()) {
210             setEnabled(false);
211         }
212     }
213 
214     /**
215      * Set the enabled state of this view. The interpretation of the enabled
216      * state varies by subclass.
217      *
218      * @param enabled True if this view is enabled, false otherwise.
219      */
220     @Override
setEnabled(boolean enabled)221     public void setEnabled(boolean enabled) {
222         super.setEnabled(enabled);
223         mIncrementButton.setEnabled(enabled);
224         mDecrementButton.setEnabled(enabled);
225         mText.setEnabled(enabled);
226     }
227 
228     /**
229      * Set the callback that indicates the number has been adjusted by the user.
230      * @param listener the callback, should not be null.
231      */
setOnChangeListener(OnChangedListener listener)232     public void setOnChangeListener(OnChangedListener listener) {
233         mListener = listener;
234     }
235 
236     /**
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()
239      * will be used
240      */
setFormatter(Formatter formatter)241     public void setFormatter(Formatter formatter) {
242         mFormatter = formatter;
243     }
244 
245     /**
246      * Set the range of numbers allowed for the number picker. The current
247      * value will be automatically set to the start.
248      *
249      * @param start the start of the range (inclusive)
250      * @param end the end of the range (inclusive)
251      */
setRange(int start, int end)252     public void setRange(int start, int end) {
253         setRange(start, end, null/*displayedValues*/);
254     }
255 
256     /**
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.
260      *
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.
264      */
setRange(int start, int end, String[] displayedValues)265     public void setRange(int start, int end, String[] displayedValues) {
266         mDisplayedValues = displayedValues;
267         mStart = start;
268         mEnd = end;
269         mCurrent = start;
270         updateView();
271 
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);
276         }
277     }
278 
279     /**
280      * Set the current value for the number picker.
281      *
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
285      */
setCurrent(int current)286     public void setCurrent(int current) {
287         if (current < mStart || current > mEnd) {
288             throw new IllegalArgumentException(
289                     "current should be >= start and <= end");
290         }
291         mCurrent = current;
292         updateView();
293     }
294 
295     /**
296      * Sets the speed at which the numbers will scroll when the +/-
297      * buttons are longpressed
298      *
299      * @param speed The speed (in milliseconds) at which the numbers will scroll
300      * default 300ms
301      */
setSpeed(long speed)302     public void setSpeed(long speed) {
303         mSpeed = speed;
304     }
305 
formatNumber(int value)306     private String formatNumber(int value) {
307         return (mFormatter != null)
308                 ? mFormatter.toString(value)
309                 : String.valueOf(value);
310     }
311 
312     /**
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
315      * is wrapped around.
316      *
317      * Subclasses can override this to change the wrapping behavior
318      *
319      * @param current the new value of the NumberPicker
320      */
changeCurrent(int current)321     protected void changeCurrent(int current) {
322         // Wrap around the values if we go past the start or end
323         if (current > mEnd) {
324             current = mStart;
325         } else if (current < mStart) {
326             current = mEnd;
327         }
328         mPrevious = mCurrent;
329         mCurrent = current;
330         notifyChange();
331         updateView();
332     }
333 
334     /**
335      * Notifies the listener, if registered, of a change of the value of this
336      * NumberPicker.
337      */
notifyChange()338     private void notifyChange() {
339         if (mListener != null) {
340             mListener.onChanged(this, mPrevious, mCurrent);
341         }
342     }
343 
344     /**
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.
349      */
updateView()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.
354          */
355         if (mDisplayedValues == null) {
356             mText.setText(formatNumber(mCurrent));
357         } else {
358             mText.setText(mDisplayedValues[mCurrent - mStart]);
359         }
360         mText.setSelection(mText.getText().length());
361     }
362 
validateCurrentView(CharSequence str)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;
368                 mCurrent = val;
369                 notifyChange();
370             }
371         }
372         updateView();
373     }
374 
validateInput(View v)375     private void validateInput(View v) {
376         String str = String.valueOf(((TextView) v).getText());
377         if ("".equals(str)) {
378 
379             // Restore to the old value as we don't allow empty values
380             updateView();
381         } else {
382 
383             // Check the new value and ensure it's in range
384             validateCurrentView(str);
385         }
386     }
387 
388     /**
389      * @hide
390      */
cancelIncrement()391     public void cancelIncrement() {
392         mIncrement = false;
393     }
394 
395     /**
396      * @hide
397      */
cancelDecrement()398     public void cancelDecrement() {
399         mDecrement = false;
400     }
401 
402     private static final char[] DIGIT_CHARACTERS = new char[] {
403         '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
404     };
405 
406     private NumberPickerButton mIncrementButton;
407     private NumberPickerButton mDecrementButton;
408 
409     private class NumberPickerInputFilter implements InputFilter {
filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)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);
414             }
415             CharSequence filtered = String.valueOf(source.subSequence(start, end));
416             String result = String.valueOf(dest.subSequence(0, dstart))
417                     + filtered
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)) {
423                     return filtered;
424                 }
425             }
426             return "";
427         }
428     }
429 
430     private class NumberRangeKeyListener extends NumberKeyListener {
431 
432         // XXX This doesn't allow for range limits when controlled by a
433         // soft input method!
getInputType()434         public int getInputType() {
435             return InputType.TYPE_CLASS_NUMBER;
436         }
437 
438         @Override
getAcceptedChars()439         protected char[] getAcceptedChars() {
440             return DIGIT_CHARACTERS;
441         }
442 
443         @Override
filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)444         public CharSequence filter(CharSequence source, int start, int end,
445                 Spanned dest, int dstart, int dend) {
446 
447             CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
448             if (filtered == null) {
449                 filtered = source.subSequence(start, end);
450             }
451 
452             String result = String.valueOf(dest.subSequence(0, dstart))
453                     + filtered
454                     + dest.subSequence(dend, dest.length());
455 
456             if ("".equals(result)) {
457                 return result;
458             }
459             int val = getSelectedPos(result);
460 
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.
465              */
466             if (val > mEnd) {
467                 return "";
468             } else {
469                 return filtered;
470             }
471         }
472     }
473 
getSelectedPos(String str)474     private int getSelectedPos(String str) {
475         if (mDisplayedValues == null) {
476             try {
477                 return Integer.parseInt(str);
478             } catch (NumberFormatException e) {
479                 /* Ignore as if it's not a number we don't care */
480             }
481         } else {
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)) {
486                     return mStart + i;
487                 }
488             }
489 
490             /* The user might have typed in a number into the month field i.e.
491              * 10 instead of OCT so support that too.
492              */
493             try {
494                 return Integer.parseInt(str);
495             } catch (NumberFormatException e) {
496 
497                 /* Ignore as if it's not a number we don't care */
498             }
499         }
500         return mStart;
501     }
502 
503     /**
504      * Returns the current value of the NumberPicker
505      * @return the current value.
506      */
getCurrent()507     public int getCurrent() {
508         return mCurrent;
509     }
510 
511     /**
512      * Returns the upper value of the range of the NumberPicker
513      * @return the uppper number of the range.
514      */
getEndRange()515     protected int getEndRange() {
516         return mEnd;
517     }
518 
519     /**
520      * Returns the lower value of the range of the NumberPicker
521      * @return the lower number of the range.
522      */
getBeginRange()523     protected int getBeginRange() {
524         return mStart;
525     }
526 }
527