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