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 com.googlecode.android_scripting.widget;
     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.EditText;
     32 import android.widget.LinearLayout;
     33 import android.widget.TextView;
     34 
     35 import com.googlecode.android_scripting.R;
     36 
     37 public class NumberPicker extends LinearLayout implements OnClickListener, OnFocusChangeListener,
     38     OnLongClickListener {
     39 
     40   public interface OnChangedListener {
     41     void onChanged(NumberPicker picker, int oldVal, int newVal);
     42   }
     43 
     44   public interface Formatter {
     45     String toString(int value);
     46   }
     47 
     48   /*
     49    * Use a custom NumberPicker formatting callback to use two-digit minutes strings like "01".
     50    * Keeping a static formatter etc. is the most efficient way to do this; it avoids creating
     51    * temporary objects on every call to format().
     52    */
     53   public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() {
     54     final StringBuilder mBuilder = new StringBuilder();
     55     final java.util.Formatter mFmt = new java.util.Formatter(mBuilder);
     56     final Object[] mArgs = new Object[1];
     57 
     58     public String toString(int value) {
     59       mArgs[0] = value;
     60       mBuilder.delete(0, mBuilder.length());
     61       mFmt.format("%02d", mArgs);
     62       return mFmt.toString();
     63     }
     64   };
     65 
     66   private final Handler mHandler;
     67   private final Runnable mRunnable = new Runnable() {
     68     public void run() {
     69       if (mIncrement) {
     70         changeCurrent(mCurrent + 1);
     71         mHandler.postDelayed(this, mSpeed);
     72       } else if (mDecrement) {
     73         changeCurrent(mCurrent - 1);
     74         mHandler.postDelayed(this, mSpeed);
     75       }
     76     }
     77   };
     78 
     79   private final EditText mText;
     80   private final InputFilter mNumberInputFilter;
     81 
     82   private String[] mDisplayedValues;
     83   private int mStart;
     84   private int mEnd;
     85   private int mCurrent;
     86   private int mPrevious;
     87   private OnChangedListener mListener;
     88   private Formatter mFormatter;
     89   private long mSpeed = 300;
     90 
     91   private boolean mIncrement;
     92   private boolean mDecrement;
     93 
     94   public NumberPicker(Context context) {
     95     this(context, null);
     96   }
     97 
     98   public NumberPicker(Context context, AttributeSet attrs) {
     99     this(context, attrs, 0);
    100   }
    101 
    102   public NumberPicker(Context context, AttributeSet attrs, int defStyle) {
    103     super(context, attrs);
    104     setOrientation(VERTICAL);
    105     LayoutInflater inflater =
    106         (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    107     inflater.inflate(R.layout.number_picker, this, true);
    108     mHandler = new Handler();
    109     InputFilter inputFilter = new NumberPickerInputFilter();
    110     mNumberInputFilter = new NumberRangeKeyListener();
    111     mIncrementButton = (NumberPickerButton) findViewById(R.id.increment);
    112     mIncrementButton.setOnClickListener(this);
    113     mIncrementButton.setOnLongClickListener(this);
    114     mIncrementButton.setNumberPicker(this);
    115     mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement);
    116     mDecrementButton.setOnClickListener(this);
    117     mDecrementButton.setOnLongClickListener(this);
    118     mDecrementButton.setNumberPicker(this);
    119 
    120     mText = (EditText) findViewById(R.id.timepicker_input);
    121     mText.setOnFocusChangeListener(this);
    122     mText.setFilters(new InputFilter[] { inputFilter });
    123     mText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
    124 
    125     if (!isEnabled()) {
    126       setEnabled(false);
    127     }
    128   }
    129 
    130   @Override
    131   public void setEnabled(boolean enabled) {
    132     super.setEnabled(enabled);
    133     mIncrementButton.setEnabled(enabled);
    134     mDecrementButton.setEnabled(enabled);
    135     mText.setEnabled(enabled);
    136   }
    137 
    138   public void setOnChangeListener(OnChangedListener listener) {
    139     mListener = listener;
    140   }
    141 
    142   public void setFormatter(Formatter formatter) {
    143     mFormatter = formatter;
    144   }
    145 
    146   /**
    147    * Set the range of numbers allowed for the number picker. The current value will be automatically
    148    * set to the start.
    149    *
    150    * @param start
    151    *          the start of the range (inclusive)
    152    * @param end
    153    *          the end of the range (inclusive)
    154    */
    155   public void setRange(int start, int end) {
    156     mStart = start;
    157     mEnd = end;
    158     mCurrent = start;
    159     updateView();
    160   }
    161 
    162   /**
    163    * Set the range of numbers allowed for the number picker. The current value will be automatically
    164    * set to the start. Also provide a mapping for values used to display to the user.
    165    *
    166    * @param start
    167    *          the start of the range (inclusive)
    168    * @param end
    169    *          the end of the range (inclusive)
    170    * @param displayedValues
    171    *          the values displayed to the user.
    172    */
    173   public void setRange(int start, int end, String[] displayedValues) {
    174     mDisplayedValues = displayedValues;
    175     mStart = start;
    176     mEnd = end;
    177     mCurrent = start;
    178     updateView();
    179   }
    180 
    181   public void setCurrent(int current) {
    182     mCurrent = current;
    183     updateView();
    184   }
    185 
    186   /**
    187    * The speed (in milliseconds) at which the numbers will scroll when the the +/- buttons are
    188    * longpressed. Default is 300ms.
    189    */
    190   public void setSpeed(long speed) {
    191     mSpeed = speed;
    192   }
    193 
    194   public void onClick(View v) {
    195     validateInput(mText);
    196     if (!mText.hasFocus()) {
    197       mText.requestFocus();
    198     }
    199 
    200     // now perform the increment/decrement
    201     if (R.id.increment == v.getId()) {
    202       changeCurrent(mCurrent + 1);
    203     } else if (R.id.decrement == v.getId()) {
    204       changeCurrent(mCurrent - 1);
    205     }
    206   }
    207 
    208   private String formatNumber(int value) {
    209     return (mFormatter != null) ? mFormatter.toString(value) : String.valueOf(value);
    210   }
    211 
    212   private void changeCurrent(int current) {
    213 
    214     // Wrap around the values if we go past the start or end
    215     if (current > mEnd) {
    216       current = mStart;
    217     } else if (current < mStart) {
    218       current = mEnd;
    219     }
    220     mPrevious = mCurrent;
    221     mCurrent = current;
    222     notifyChange();
    223     updateView();
    224   }
    225 
    226   private void notifyChange() {
    227     if (mListener != null) {
    228       mListener.onChanged(this, mPrevious, mCurrent);
    229     }
    230   }
    231 
    232   private void updateView() {
    233 
    234     /*
    235      * If we don't have displayed values then use the current number else find the correct value in
    236      * the displayed values for the current number.
    237      */
    238     if (mDisplayedValues == null) {
    239       mText.setText(formatNumber(mCurrent));
    240     } else {
    241       mText.setText(mDisplayedValues[mCurrent - mStart]);
    242     }
    243     mText.setSelection(mText.getText().length());
    244   }
    245 
    246   private void validateCurrentView(CharSequence str) {
    247     int val = getSelectedPos(str.toString());
    248     if ((val >= mStart) && (val <= mEnd)) {
    249       mPrevious = mCurrent;
    250       mCurrent = val;
    251       notifyChange();
    252     }
    253     updateView();
    254   }
    255 
    256   public void onFocusChange(View v, boolean hasFocus) {
    257 
    258     /*
    259      * When focus is lost check that the text field has valid values.
    260      */
    261     if (!hasFocus) {
    262       validateInput(v);
    263     }
    264   }
    265 
    266   private void validateInput(View v) {
    267     String str = String.valueOf(((TextView) v).getText());
    268     if ("".equals(str)) {
    269 
    270       // Restore to the old value as we don't allow empty values
    271       updateView();
    272     } else {
    273 
    274       // Check the new value and ensure it's in range
    275       validateCurrentView(str);
    276     }
    277   }
    278 
    279   /**
    280    * We start the long click here but rely on the {@link NumberPickerButton} to inform us when the
    281    * long click has ended.
    282    */
    283   public boolean onLongClick(View v) {
    284 
    285     /*
    286      * The text view may still have focus so clear it's focus which will trigger the on focus
    287      * changed and any typed values to be pulled.
    288      */
    289     mText.clearFocus();
    290 
    291     if (R.id.increment == v.getId()) {
    292       mIncrement = true;
    293       mHandler.post(mRunnable);
    294     } else if (R.id.decrement == v.getId()) {
    295       mDecrement = true;
    296       mHandler.post(mRunnable);
    297     }
    298     return true;
    299   }
    300 
    301   public void cancelIncrement() {
    302     mIncrement = false;
    303   }
    304 
    305   public void cancelDecrement() {
    306     mDecrement = false;
    307   }
    308 
    309   private static final char[] DIGIT_CHARACTERS =
    310       new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
    311 
    312   private final NumberPickerButton mIncrementButton;
    313   private final NumberPickerButton mDecrementButton;
    314 
    315   private class NumberPickerInputFilter implements InputFilter {
    316     public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
    317         int dend) {
    318       if (mDisplayedValues == null) {
    319         return mNumberInputFilter.filter(source, start, end, dest, dstart, dend);
    320       }
    321       CharSequence filtered = String.valueOf(source.subSequence(start, end));
    322       String result =
    323           String.valueOf(dest.subSequence(0, dstart)) + filtered
    324               + dest.subSequence(dend, dest.length());
    325       String str = String.valueOf(result).toLowerCase();
    326       for (String val : mDisplayedValues) {
    327         val = val.toLowerCase();
    328         if (val.startsWith(str)) {
    329           return filtered;
    330         }
    331       }
    332       return "";
    333     }
    334   }
    335 
    336   private class NumberRangeKeyListener extends NumberKeyListener {
    337 
    338     // XXX This doesn't allow for range limits when controlled by a
    339     // soft input method!
    340     public int getInputType() {
    341       return InputType.TYPE_CLASS_NUMBER;
    342     }
    343 
    344     @Override
    345     protected char[] getAcceptedChars() {
    346       return DIGIT_CHARACTERS;
    347     }
    348 
    349     @Override
    350     public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
    351         int dend) {
    352 
    353       CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
    354       if (filtered == null) {
    355         filtered = source.subSequence(start, end);
    356       }
    357 
    358       String result =
    359           String.valueOf(dest.subSequence(0, dstart)) + filtered
    360               + dest.subSequence(dend, dest.length());
    361 
    362       if ("".equals(result)) {
    363         return result;
    364       }
    365       int val = getSelectedPos(result);
    366 
    367       /*
    368        * Ensure the user can't type in a value greater than the max allowed. We have to allow less
    369        * than min as the user might want to delete some numbers and then type a new number.
    370        */
    371       if (val > mEnd) {
    372         return "";
    373       } else {
    374         return filtered;
    375       }
    376     }
    377   }
    378 
    379   private int getSelectedPos(String str) {
    380     if (mDisplayedValues == null) {
    381       return Integer.parseInt(str);
    382     } else {
    383       for (int i = 0; i < mDisplayedValues.length; i++) {
    384 
    385         /* Don't force the user to type in jan when ja will do */
    386         str = str.toLowerCase();
    387         if (mDisplayedValues[i].toLowerCase().startsWith(str)) {
    388           return mStart + i;
    389         }
    390       }
    391 
    392       /*
    393        * The user might have typed in a number into the month field i.e. 10 instead of OCT so
    394        * support that too.
    395        */
    396       try {
    397         return Integer.parseInt(str);
    398       } catch (NumberFormatException e) {
    399 
    400         /* Ignore as if it's not a number we don't care */
    401       }
    402     }
    403     return mStart;
    404   }
    405 
    406   /**
    407    * @return the current value.
    408    */
    409   public int getCurrent() {
    410     return mCurrent;
    411   }
    412 }