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