NumberPicker.java revision 8bcbebd4178b2e9aca9ee3bd9e1e12c42e74c8db
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 com.android.internal.R;
20
21import android.annotation.Widget;
22import android.content.Context;
23import android.os.Handler;
24import android.text.InputFilter;
25import android.text.InputType;
26import android.text.Spanned;
27import android.text.method.NumberKeyListener;
28import android.util.AttributeSet;
29import android.view.LayoutInflater;
30import 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
39public 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         */
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 {
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
302    public void setSpeed(long speed) {
303        mSpeed = speed;
304    }
305
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     */
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     */
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     */
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
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
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     */
391    public void cancelIncrement() {
392        mIncrement = false;
393    }
394
395    /**
396     * @hide
397     */
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 {
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!
434        public int getInputType() {
435            return InputType.TYPE_CLASS_NUMBER;
436        }
437
438        @Override
439        protected char[] getAcceptedChars() {
440            return DIGIT_CHARACTERS;
441        }
442
443        @Override
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
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     */
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     */
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     */
523    protected int getBeginRange() {
524        return mStart;
525    }
526}
527