Home | History | Annotate | Download | only in widget
      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          */
     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