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