NumberPicker.java revision 68f2f547f56f239b60d13b2b62a08a65874f6662
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
17package android.widget;
18
19import android.annotation.Widget;
20import android.content.Context;
21import android.os.Handler;
22import android.text.InputFilter;
23import android.text.InputType;
24import android.text.Spanned;
25import android.text.method.NumberKeyListener;
26import android.util.AttributeSet;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.View.OnClickListener;
30import android.view.View.OnFocusChangeListener;
31import android.view.View.OnLongClickListener;
32import android.widget.TextView;
33import android.widget.LinearLayout;
34import android.widget.EditText;
35
36import com.android.internal.R;
37
38/**
39 * A view for selecting a number
40 *
41 * For a dialog using this view, see {@link android.app.TimePickerDialog}.
42 */
43@Widget
44public class NumberPicker extends LinearLayout {
45
46    /**
47     * The callback interface used to indicate the number value has been adjusted.
48     */
49    public interface OnChangedListener {
50        /**
51         * @param picker The NumberPicker associated with this listener.
52         * @param oldVal The previous value.
53         * @param newVal The new value.
54         */
55        void onChanged(NumberPicker picker, int oldVal, int newVal);
56    }
57
58    /**
59     * Interface used to format the number into a string for presentation
60     */
61    public interface Formatter {
62        String toString(int value);
63    }
64
65    /*
66     * Use a custom NumberPicker formatting callback to use two-digit
67     * minutes strings like "01".  Keeping a static formatter etc. is the
68     * most efficient way to do this; it avoids creating temporary objects
69     * on every call to format().
70     */
71    public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER =
72            new NumberPicker.Formatter() {
73                final StringBuilder mBuilder = new StringBuilder();
74                final java.util.Formatter mFmt = new java.util.Formatter(mBuilder);
75                final Object[] mArgs = new Object[1];
76                public String toString(int value) {
77                    mArgs[0] = value;
78                    mBuilder.delete(0, mBuilder.length());
79                    mFmt.format("%02d", mArgs);
80                    return mFmt.toString();
81                }
82        };
83
84    private final Handler mHandler;
85    private final Runnable mRunnable = new Runnable() {
86        public void run() {
87            if (mIncrement) {
88                changeCurrent(mCurrent + 1);
89                mHandler.postDelayed(this, mSpeed);
90            } else if (mDecrement) {
91                changeCurrent(mCurrent - 1);
92                mHandler.postDelayed(this, mSpeed);
93            }
94        }
95    };
96
97    private final EditText mText;
98    private final InputFilter mNumberInputFilter;
99
100    private String[] mDisplayedValues;
101
102    /**
103     * Lower value of the range of numbers allowed for the NumberPicker
104     */
105    private int mStart;
106
107    /**
108     * Upper value of the range of numbers allowed for the NumberPicker
109     */
110    private int mEnd;
111
112    /**
113     * Current value of this NumberPicker
114     */
115    private int mCurrent;
116
117    /**
118     * Previous value of this NumberPicker.
119     */
120    private int mPrevious;
121    private OnChangedListener mListener;
122    private Formatter mFormatter;
123    private long mSpeed = 300;
124
125    private boolean mIncrement;
126    private boolean mDecrement;
127
128    /**
129     * Create a new number picker
130     * @param context the application environment
131     */
132    public NumberPicker(Context context) {
133        this(context, null);
134    }
135
136    /**
137     * Create a new number picker
138     * @param context the application environment
139     * @param attrs a collection of attributes
140     */
141    public NumberPicker(Context context, AttributeSet attrs) {
142        super(context, attrs);
143        setOrientation(VERTICAL);
144        LayoutInflater inflater =
145                (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
146        inflater.inflate(R.layout.number_picker, this, true);
147        mHandler = new Handler();
148
149        OnClickListener clickListener = new OnClickListener() {
150            public void onClick(View v) {
151                validateInput(mText);
152                if (!mText.hasFocus()) mText.requestFocus();
153
154                // now perform the increment/decrement
155                if (R.id.increment == v.getId()) {
156                    changeCurrent(mCurrent + 1);
157                } else if (R.id.decrement == v.getId()) {
158                    changeCurrent(mCurrent - 1);
159                }
160            }
161        };
162
163        OnFocusChangeListener focusListener = new OnFocusChangeListener() {
164            public void onFocusChange(View v, boolean hasFocus) {
165
166                /* When focus is lost check that the text field
167                 * has valid values.
168                 */
169                if (!hasFocus) {
170                    validateInput(v);
171                }
172            }
173        };
174
175        OnLongClickListener longClickListener = new OnLongClickListener() {
176            /**
177             * We start the long click here but rely on the {@link NumberPickerButton}
178             * to inform us when the long click has ended.
179             */
180            public boolean onLongClick(View v) {
181                /* The text view may still have focus so clear it's focus which will
182                 * trigger the on focus changed and any typed values to be pulled.
183                 */
184                mText.clearFocus();
185
186                if (R.id.increment == v.getId()) {
187                    mIncrement = true;
188                    mHandler.post(mRunnable);
189                } else if (R.id.decrement == v.getId()) {
190                    mDecrement = true;
191                    mHandler.post(mRunnable);
192                }
193                return true;
194            }
195        };
196
197        InputFilter inputFilter = new NumberPickerInputFilter();
198        mNumberInputFilter = new NumberRangeKeyListener();
199        mIncrementButton = (NumberPickerButton) findViewById(R.id.increment);
200        mIncrementButton.setOnClickListener(clickListener);
201        mIncrementButton.setOnLongClickListener(longClickListener);
202        mIncrementButton.setNumberPicker(this);
203
204        mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement);
205        mDecrementButton.setOnClickListener(clickListener);
206        mDecrementButton.setOnLongClickListener(longClickListener);
207        mDecrementButton.setNumberPicker(this);
208
209        mText = (EditText) findViewById(R.id.timepicker_input);
210        mText.setOnFocusChangeListener(focusListener);
211        mText.setFilters(new InputFilter[] {inputFilter});
212        mText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
213
214        if (!isEnabled()) {
215            setEnabled(false);
216        }
217    }
218
219    /**
220     * Set the enabled state of this view. The interpretation of the enabled
221     * state varies by subclass.
222     *
223     * @param enabled True if this view is enabled, false otherwise.
224     */
225    @Override
226    public void setEnabled(boolean enabled) {
227        super.setEnabled(enabled);
228        mIncrementButton.setEnabled(enabled);
229        mDecrementButton.setEnabled(enabled);
230        mText.setEnabled(enabled);
231    }
232
233    /**
234     * Set the callback that indicates the number has been adjusted by the user.
235     * @param listener the callback, should not be null.
236     */
237    public void setOnChangeListener(OnChangedListener listener) {
238        mListener = listener;
239    }
240
241    /**
242     * Set the formatter that will be used to format the number for presentation
243     * @param formatter the formatter object.  If formatter is null, String.valueOf()
244     * will be used
245     */
246    public void setFormatter(Formatter formatter) {
247        mFormatter = formatter;
248    }
249
250    /**
251     * Set the range of numbers allowed for the number picker. The current
252     * value will be automatically set to the start.
253     *
254     * @param start the start of the range (inclusive)
255     * @param end the end of the range (inclusive)
256     */
257    public void setRange(int start, int end) {
258        setRange(start, end, null/*displayedValues*/);
259    }
260
261    /**
262     * Set the range of numbers allowed for the number picker. The current
263     * value will be automatically set to the start. Also provide a mapping
264     * for values used to display to the user.
265     *
266     * @param start the start of the range (inclusive)
267     * @param end the end of the range (inclusive)
268     * @param displayedValues the values displayed to the user.
269     */
270    public void setRange(int start, int end, String[] displayedValues) {
271        mDisplayedValues = displayedValues;
272        mStart = start;
273        mEnd = end;
274        mCurrent = start;
275        updateView();
276    }
277
278    /**
279     * Set the current value for the number picker.
280     *
281     * @param current the current value the start of the range (inclusive)
282     * @throws IllegalArgumentException when current is not within the range
283     *         of of the number picker
284     */
285    public void setCurrent(int current) {
286        if (current < mStart || current > mEnd) {
287            throw new IllegalArgumentException(
288                    "current should be >= start and <= end");
289        }
290        mCurrent = current;
291        updateView();
292    }
293
294    /**
295     * Sets the speed at which the numbers will scroll when the +/-
296     * buttons are longpressed
297     *
298     * @param speed The speed (in milliseconds) at which the numbers will scroll
299     * default 300ms
300     */
301    public void setSpeed(long speed) {
302        mSpeed = speed;
303    }
304
305    private String formatNumber(int value) {
306        return (mFormatter != null)
307                ? mFormatter.toString(value)
308                : String.valueOf(value);
309    }
310
311    /**
312     * Sets the current value of this NumberPicker, and sets mPrevious to the previous
313     * value.  If current is greater than mEnd less than mStart, the value of mCurrent
314     * is wrapped around.
315     *
316     * Subclasses can override this to change the wrapping behavior
317     *
318     * @param current the new value of the NumberPicker
319     */
320    protected void changeCurrent(int current) {
321        // Wrap around the values if we go past the start or end
322        if (current > mEnd) {
323            current = mStart;
324        } else if (current < mStart) {
325            current = mEnd;
326        }
327        mPrevious = mCurrent;
328        mCurrent = current;
329        notifyChange();
330        updateView();
331    }
332
333    /**
334     * Notifies the listener, if registered, of a change of the value of this
335     * NumberPicker.
336     */
337    private void notifyChange() {
338        if (mListener != null) {
339            mListener.onChanged(this, mPrevious, mCurrent);
340        }
341    }
342
343    /**
344     * Updates the view of this NumberPicker.  If displayValues were specified
345     * in {@link #setRange}, the string corresponding to the index specified by
346     * the current value will be returned.  Otherwise, the formatter specified
347     * in {@link setFormatter} will be used to format the number.
348     */
349    private void updateView() {
350        /* If we don't have displayed values then use the
351         * current number else find the correct value in the
352         * displayed values for the current number.
353         */
354        if (mDisplayedValues == null) {
355            mText.setText(formatNumber(mCurrent));
356        } else {
357            mText.setText(mDisplayedValues[mCurrent - mStart]);
358        }
359        mText.setSelection(mText.getText().length());
360    }
361
362    private void validateCurrentView(CharSequence str) {
363        int val = getSelectedPos(str.toString());
364        if ((val >= mStart) && (val <= mEnd)) {
365            if (mCurrent != val) {
366                mPrevious = mCurrent;
367                mCurrent = val;
368                notifyChange();
369            }
370        }
371        updateView();
372    }
373
374    private void validateInput(View v) {
375        String str = String.valueOf(((TextView) v).getText());
376        if ("".equals(str)) {
377
378            // Restore to the old value as we don't allow empty values
379            updateView();
380        } else {
381
382            // Check the new value and ensure it's in range
383            validateCurrentView(str);
384        }
385    }
386
387    /**
388     * @hide
389     */
390    public void cancelIncrement() {
391        mIncrement = false;
392    }
393
394    /**
395     * @hide
396     */
397    public void cancelDecrement() {
398        mDecrement = false;
399    }
400
401    private static final char[] DIGIT_CHARACTERS = new char[] {
402        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
403    };
404
405    private NumberPickerButton mIncrementButton;
406    private NumberPickerButton mDecrementButton;
407
408    private class NumberPickerInputFilter implements InputFilter {
409        public CharSequence filter(CharSequence source, int start, int end,
410                Spanned dest, int dstart, int dend) {
411            if (mDisplayedValues == null) {
412                return mNumberInputFilter.filter(source, start, end, dest, dstart, dend);
413            }
414            CharSequence filtered = String.valueOf(source.subSequence(start, end));
415            String result = String.valueOf(dest.subSequence(0, dstart))
416                    + filtered
417                    + dest.subSequence(dend, dest.length());
418            String str = String.valueOf(result).toLowerCase();
419            for (String val : mDisplayedValues) {
420                val = val.toLowerCase();
421                if (val.startsWith(str)) {
422                    return filtered;
423                }
424            }
425            return "";
426        }
427    }
428
429    private class NumberRangeKeyListener extends NumberKeyListener {
430
431        // XXX This doesn't allow for range limits when controlled by a
432        // soft input method!
433        public int getInputType() {
434            return InputType.TYPE_CLASS_NUMBER;
435        }
436
437        @Override
438        protected char[] getAcceptedChars() {
439            return DIGIT_CHARACTERS;
440        }
441
442        @Override
443        public CharSequence filter(CharSequence source, int start, int end,
444                Spanned dest, int dstart, int dend) {
445
446            CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
447            if (filtered == null) {
448                filtered = source.subSequence(start, end);
449            }
450
451            String result = String.valueOf(dest.subSequence(0, dstart))
452                    + filtered
453                    + dest.subSequence(dend, dest.length());
454
455            if ("".equals(result)) {
456                return result;
457            }
458            int val = getSelectedPos(result);
459
460            /* Ensure the user can't type in a value greater
461             * than the max allowed. We have to allow less than min
462             * as the user might want to delete some numbers
463             * and then type a new number.
464             */
465            if (val > mEnd) {
466                return "";
467            } else {
468                return filtered;
469            }
470        }
471    }
472
473    private int getSelectedPos(String str) {
474        if (mDisplayedValues == null) {
475            return Integer.parseInt(str);
476        } else {
477            for (int i = 0; i < mDisplayedValues.length; i++) {
478
479                /* Don't force the user to type in jan when ja will do */
480                str = str.toLowerCase();
481                if (mDisplayedValues[i].toLowerCase().startsWith(str)) {
482                    return mStart + i;
483                }
484            }
485
486            /* The user might have typed in a number into the month field i.e.
487             * 10 instead of OCT so support that too.
488             */
489            try {
490                return Integer.parseInt(str);
491            } catch (NumberFormatException e) {
492
493                /* Ignore as if it's not a number we don't care */
494            }
495        }
496        return mStart;
497    }
498
499    /**
500     * Returns the current value of the NumberPicker
501     * @return the current value.
502     */
503    public int getCurrent() {
504        return mCurrent;
505    }
506
507    /**
508     * Returns the upper value of the range of the NumberPicker
509     * @return the uppper number of the range.
510     */
511    protected int getEndRange() {
512        return mEnd;
513    }
514
515    /**
516     * Returns the lower value of the range of the NumberPicker
517     * @return the lower number of the range.
518     */
519    protected int getBeginRange() {
520        return mStart;
521    }
522}
523