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