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