• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
onChanged(NumberPicker picker, int oldVal, int newVal)41     void onChanged(NumberPicker picker, int oldVal, int newVal);
42   }
43 
44   public interface Formatter {
toString(int value)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 
NumberPicker(Context context)94   public NumberPicker(Context context) {
95     this(context, null);
96   }
97 
NumberPicker(Context context, AttributeSet attrs)98   public NumberPicker(Context context, AttributeSet attrs) {
99     this(context, attrs, 0);
100   }
101 
NumberPicker(Context context, AttributeSet attrs, int defStyle)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
setEnabled(boolean enabled)131   public void setEnabled(boolean enabled) {
132     super.setEnabled(enabled);
133     mIncrementButton.setEnabled(enabled);
134     mDecrementButton.setEnabled(enabled);
135     mText.setEnabled(enabled);
136   }
137 
setOnChangeListener(OnChangedListener listener)138   public void setOnChangeListener(OnChangedListener listener) {
139     mListener = listener;
140   }
141 
setFormatter(Formatter formatter)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    */
setRange(int start, int end)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    */
setRange(int start, int end, String[] displayedValues)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 
setCurrent(int current)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    */
setSpeed(long speed)190   public void setSpeed(long speed) {
191     mSpeed = speed;
192   }
193 
onClick(View v)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 
formatNumber(int value)208   private String formatNumber(int value) {
209     return (mFormatter != null) ? mFormatter.toString(value) : String.valueOf(value);
210   }
211 
changeCurrent(int current)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 
notifyChange()226   private void notifyChange() {
227     if (mListener != null) {
228       mListener.onChanged(this, mPrevious, mCurrent);
229     }
230   }
231 
updateView()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 
validateCurrentView(CharSequence str)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 
onFocusChange(View v, boolean hasFocus)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 
validateInput(View v)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    */
onLongClick(View v)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 
cancelIncrement()301   public void cancelIncrement() {
302     mIncrement = false;
303   }
304 
cancelDecrement()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 {
filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)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!
getInputType()340     public int getInputType() {
341       return InputType.TYPE_CLASS_NUMBER;
342     }
343 
344     @Override
getAcceptedChars()345     protected char[] getAcceptedChars() {
346       return DIGIT_CHARACTERS;
347     }
348 
349     @Override
filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)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 
getSelectedPos(String str)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    */
getCurrent()409   public int getCurrent() {
410     return mCurrent;
411   }
412 }