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