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 android.widget; 18 19 import com.android.internal.R; 20 21 import android.annotation.Widget; 22 import android.content.Context; 23 import android.os.Handler; 24 import android.text.InputFilter; 25 import android.text.InputType; 26 import android.text.Spanned; 27 import android.text.method.NumberKeyListener; 28 import android.util.AttributeSet; 29 import android.view.LayoutInflater; 30 import 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 39 public 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 */ onChanged(NumberPicker picker, int oldVal, int newVal)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 { toString(int value)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(mBuilder); 70 final Object[] mArgs = new Object[1]; 71 public String toString(int value) { 72 mArgs[0] = value; 73 mBuilder.delete(0, mBuilder.length()); 74 mFmt.format("%02d", mArgs); 75 return mFmt.toString(); 76 } 77 }; 78 79 private final Handler mHandler; 80 private final Runnable mRunnable = new Runnable() { 81 public void run() { 82 if (mIncrement) { 83 changeCurrent(mCurrent + 1); 84 mHandler.postDelayed(this, mSpeed); 85 } else if (mDecrement) { 86 changeCurrent(mCurrent - 1); 87 mHandler.postDelayed(this, mSpeed); 88 } 89 } 90 }; 91 92 private final EditText mText; 93 private final InputFilter mNumberInputFilter; 94 95 private String[] mDisplayedValues; 96 97 /** 98 * Lower value of the range of numbers allowed for the NumberPicker 99 */ 100 private int mStart; 101 102 /** 103 * Upper value of the range of numbers allowed for the NumberPicker 104 */ 105 private int mEnd; 106 107 /** 108 * Current value of this NumberPicker 109 */ 110 private int mCurrent; 111 112 /** 113 * Previous value of this NumberPicker. 114 */ 115 private int mPrevious; 116 private OnChangedListener mListener; 117 private Formatter mFormatter; 118 private long mSpeed = 300; 119 120 private boolean mIncrement; 121 private boolean mDecrement; 122 123 /** 124 * Create a new number picker 125 * @param context the application environment 126 */ NumberPicker(Context context)127 public NumberPicker(Context context) { 128 this(context, null); 129 } 130 131 /** 132 * Create a new number picker 133 * @param context the application environment 134 * @param attrs a collection of attributes 135 */ NumberPicker(Context context, AttributeSet attrs)136 public NumberPicker(Context context, AttributeSet attrs) { 137 super(context, attrs); 138 setOrientation(VERTICAL); 139 LayoutInflater inflater = 140 (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 141 inflater.inflate(R.layout.number_picker, this, true); 142 mHandler = new Handler(); 143 144 OnClickListener clickListener = new OnClickListener() { 145 public void onClick(View v) { 146 validateInput(mText); 147 if (!mText.hasFocus()) mText.requestFocus(); 148 149 // now perform the increment/decrement 150 if (R.id.increment == v.getId()) { 151 changeCurrent(mCurrent + 1); 152 } else if (R.id.decrement == v.getId()) { 153 changeCurrent(mCurrent - 1); 154 } 155 } 156 }; 157 158 OnFocusChangeListener focusListener = new OnFocusChangeListener() { 159 public void onFocusChange(View v, boolean hasFocus) { 160 161 /* When focus is lost check that the text field 162 * has valid values. 163 */ 164 if (!hasFocus) { 165 validateInput(v); 166 } 167 } 168 }; 169 170 OnLongClickListener longClickListener = new OnLongClickListener() { 171 /** 172 * We start the long click here but rely on the {@link NumberPickerButton} 173 * to inform us when the long click has ended. 174 */ 175 public boolean onLongClick(View v) { 176 /* The text view may still have focus so clear it's focus which will 177 * trigger the on focus changed and any typed values to be pulled. 178 */ 179 mText.clearFocus(); 180 181 if (R.id.increment == v.getId()) { 182 mIncrement = true; 183 mHandler.post(mRunnable); 184 } else if (R.id.decrement == v.getId()) { 185 mDecrement = true; 186 mHandler.post(mRunnable); 187 } 188 return true; 189 } 190 }; 191 192 InputFilter inputFilter = new NumberPickerInputFilter(); 193 mNumberInputFilter = new NumberRangeKeyListener(); 194 mIncrementButton = (NumberPickerButton) findViewById(R.id.increment); 195 mIncrementButton.setOnClickListener(clickListener); 196 mIncrementButton.setOnLongClickListener(longClickListener); 197 mIncrementButton.setNumberPicker(this); 198 199 mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement); 200 mDecrementButton.setOnClickListener(clickListener); 201 mDecrementButton.setOnLongClickListener(longClickListener); 202 mDecrementButton.setNumberPicker(this); 203 204 mText = (EditText) findViewById(R.id.timepicker_input); 205 mText.setOnFocusChangeListener(focusListener); 206 mText.setFilters(new InputFilter[] {inputFilter}); 207 mText.setRawInputType(InputType.TYPE_CLASS_NUMBER); 208 209 if (!isEnabled()) { 210 setEnabled(false); 211 } 212 } 213 214 /** 215 * Set the enabled state of this view. The interpretation of the enabled 216 * state varies by subclass. 217 * 218 * @param enabled True if this view is enabled, false otherwise. 219 */ 220 @Override setEnabled(boolean enabled)221 public void setEnabled(boolean enabled) { 222 super.setEnabled(enabled); 223 mIncrementButton.setEnabled(enabled); 224 mDecrementButton.setEnabled(enabled); 225 mText.setEnabled(enabled); 226 } 227 228 /** 229 * Set the callback that indicates the number has been adjusted by the user. 230 * @param listener the callback, should not be null. 231 */ setOnChangeListener(OnChangedListener listener)232 public void setOnChangeListener(OnChangedListener listener) { 233 mListener = listener; 234 } 235 236 /** 237 * Set the formatter that will be used to format the number for presentation 238 * @param formatter the formatter object. If formatter is null, String.valueOf() 239 * will be used 240 */ setFormatter(Formatter formatter)241 public void setFormatter(Formatter formatter) { 242 mFormatter = formatter; 243 } 244 245 /** 246 * Set the range of numbers allowed for the number picker. The current 247 * value will be automatically set to the start. 248 * 249 * @param start the start of the range (inclusive) 250 * @param end the end of the range (inclusive) 251 */ setRange(int start, int end)252 public void setRange(int start, int end) { 253 setRange(start, end, null/*displayedValues*/); 254 } 255 256 /** 257 * Set the range of numbers allowed for the number picker. The current 258 * value will be automatically set to the start. Also provide a mapping 259 * for values used to display to the user. 260 * 261 * @param start the start of the range (inclusive) 262 * @param end the end of the range (inclusive) 263 * @param displayedValues the values displayed to the user. 264 */ setRange(int start, int end, String[] displayedValues)265 public void setRange(int start, int end, String[] displayedValues) { 266 mDisplayedValues = displayedValues; 267 mStart = start; 268 mEnd = end; 269 mCurrent = start; 270 updateView(); 271 272 if (displayedValues != null) { 273 // Allow text entry rather than strictly numeric entry. 274 mText.setRawInputType(InputType.TYPE_CLASS_TEXT | 275 InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); 276 } 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 */ setCurrent(int current)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 */ setSpeed(long speed)302 public void setSpeed(long speed) { 303 mSpeed = speed; 304 } 305 formatNumber(int value)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 */ changeCurrent(int current)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 */ notifyChange()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 */ updateView()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 validateCurrentView(CharSequence str)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 validateInput(View v)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 */ cancelIncrement()391 public void cancelIncrement() { 392 mIncrement = false; 393 } 394 395 /** 396 * @hide 397 */ cancelDecrement()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 { filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)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! getInputType()434 public int getInputType() { 435 return InputType.TYPE_CLASS_NUMBER; 436 } 437 438 @Override getAcceptedChars()439 protected char[] getAcceptedChars() { 440 return DIGIT_CHARACTERS; 441 } 442 443 @Override filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)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 getSelectedPos(String str)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 */ getCurrent()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 */ getEndRange()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 */ getBeginRange()523 protected int getBeginRange() { 524 return mStart; 525 } 526 } 527