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.googlecode.android_scripting.widget; 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.EditText; 32import android.widget.LinearLayout; 33import android.widget.TextView; 34 35import com.googlecode.android_scripting.R; 36 37public class NumberPicker extends LinearLayout implements OnClickListener, OnFocusChangeListener, 38 OnLongClickListener { 39 40 public interface OnChangedListener { 41 void onChanged(NumberPicker picker, int oldVal, int newVal); 42 } 43 44 public interface Formatter { 45 String toString(int value); 46 } 47 48 /* 49 * Use a custom NumberPicker formatting callback to use two-digit minutes strings like "01". 50 * Keeping a static formatter etc. is the most efficient way to do this; it avoids creating 51 * temporary objects on every call to format(). 52 */ 53 public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() { 54 final StringBuilder mBuilder = new StringBuilder(); 55 final java.util.Formatter mFmt = new java.util.Formatter(mBuilder); 56 final Object[] mArgs = new Object[1]; 57 58 public String toString(int value) { 59 mArgs[0] = value; 60 mBuilder.delete(0, mBuilder.length()); 61 mFmt.format("%02d", mArgs); 62 return mFmt.toString(); 63 } 64 }; 65 66 private final Handler mHandler; 67 private final Runnable mRunnable = new Runnable() { 68 public void run() { 69 if (mIncrement) { 70 changeCurrent(mCurrent + 1); 71 mHandler.postDelayed(this, mSpeed); 72 } else if (mDecrement) { 73 changeCurrent(mCurrent - 1); 74 mHandler.postDelayed(this, mSpeed); 75 } 76 } 77 }; 78 79 private final EditText mText; 80 private final InputFilter mNumberInputFilter; 81 82 private String[] mDisplayedValues; 83 private int mStart; 84 private int mEnd; 85 private int mCurrent; 86 private int mPrevious; 87 private OnChangedListener mListener; 88 private Formatter mFormatter; 89 private long mSpeed = 300; 90 91 private boolean mIncrement; 92 private boolean mDecrement; 93 94 public NumberPicker(Context context) { 95 this(context, null); 96 } 97 98 public NumberPicker(Context context, AttributeSet attrs) { 99 this(context, attrs, 0); 100 } 101 102 public NumberPicker(Context context, AttributeSet attrs, int defStyle) { 103 super(context, attrs); 104 setOrientation(VERTICAL); 105 LayoutInflater inflater = 106 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 107 inflater.inflate(R.layout.number_picker, this, true); 108 mHandler = new Handler(); 109 InputFilter inputFilter = new NumberPickerInputFilter(); 110 mNumberInputFilter = new NumberRangeKeyListener(); 111 mIncrementButton = (NumberPickerButton) findViewById(R.id.increment); 112 mIncrementButton.setOnClickListener(this); 113 mIncrementButton.setOnLongClickListener(this); 114 mIncrementButton.setNumberPicker(this); 115 mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement); 116 mDecrementButton.setOnClickListener(this); 117 mDecrementButton.setOnLongClickListener(this); 118 mDecrementButton.setNumberPicker(this); 119 120 mText = (EditText) findViewById(R.id.timepicker_input); 121 mText.setOnFocusChangeListener(this); 122 mText.setFilters(new InputFilter[] { inputFilter }); 123 mText.setRawInputType(InputType.TYPE_CLASS_NUMBER); 124 125 if (!isEnabled()) { 126 setEnabled(false); 127 } 128 } 129 130 @Override 131 public void setEnabled(boolean enabled) { 132 super.setEnabled(enabled); 133 mIncrementButton.setEnabled(enabled); 134 mDecrementButton.setEnabled(enabled); 135 mText.setEnabled(enabled); 136 } 137 138 public void setOnChangeListener(OnChangedListener listener) { 139 mListener = listener; 140 } 141 142 public void setFormatter(Formatter formatter) { 143 mFormatter = formatter; 144 } 145 146 /** 147 * Set the range of numbers allowed for the number picker. The current value will be automatically 148 * set to the start. 149 * 150 * @param start 151 * the start of the range (inclusive) 152 * @param end 153 * the end of the range (inclusive) 154 */ 155 public void setRange(int start, int end) { 156 mStart = start; 157 mEnd = end; 158 mCurrent = start; 159 updateView(); 160 } 161 162 /** 163 * Set the range of numbers allowed for the number picker. The current value will be automatically 164 * set to the start. Also provide a mapping for values used to display to the user. 165 * 166 * @param start 167 * the start of the range (inclusive) 168 * @param end 169 * the end of the range (inclusive) 170 * @param displayedValues 171 * the values displayed to the user. 172 */ 173 public void setRange(int start, int end, String[] displayedValues) { 174 mDisplayedValues = displayedValues; 175 mStart = start; 176 mEnd = end; 177 mCurrent = start; 178 updateView(); 179 } 180 181 public void setCurrent(int current) { 182 mCurrent = current; 183 updateView(); 184 } 185 186 /** 187 * The speed (in milliseconds) at which the numbers will scroll when the the +/- buttons are 188 * longpressed. Default is 300ms. 189 */ 190 public void setSpeed(long speed) { 191 mSpeed = speed; 192 } 193 194 public void onClick(View v) { 195 validateInput(mText); 196 if (!mText.hasFocus()) { 197 mText.requestFocus(); 198 } 199 200 // now perform the increment/decrement 201 if (R.id.increment == v.getId()) { 202 changeCurrent(mCurrent + 1); 203 } else if (R.id.decrement == v.getId()) { 204 changeCurrent(mCurrent - 1); 205 } 206 } 207 208 private String formatNumber(int value) { 209 return (mFormatter != null) ? mFormatter.toString(value) : String.valueOf(value); 210 } 211 212 private void changeCurrent(int current) { 213 214 // Wrap around the values if we go past the start or end 215 if (current > mEnd) { 216 current = mStart; 217 } else if (current < mStart) { 218 current = mEnd; 219 } 220 mPrevious = mCurrent; 221 mCurrent = current; 222 notifyChange(); 223 updateView(); 224 } 225 226 private void notifyChange() { 227 if (mListener != null) { 228 mListener.onChanged(this, mPrevious, mCurrent); 229 } 230 } 231 232 private void updateView() { 233 234 /* 235 * If we don't have displayed values then use the current number else find the correct value in 236 * the displayed values for the current number. 237 */ 238 if (mDisplayedValues == null) { 239 mText.setText(formatNumber(mCurrent)); 240 } else { 241 mText.setText(mDisplayedValues[mCurrent - mStart]); 242 } 243 mText.setSelection(mText.getText().length()); 244 } 245 246 private void validateCurrentView(CharSequence str) { 247 int val = getSelectedPos(str.toString()); 248 if ((val >= mStart) && (val <= mEnd)) { 249 mPrevious = mCurrent; 250 mCurrent = val; 251 notifyChange(); 252 } 253 updateView(); 254 } 255 256 public void onFocusChange(View v, boolean hasFocus) { 257 258 /* 259 * When focus is lost check that the text field has valid values. 260 */ 261 if (!hasFocus) { 262 validateInput(v); 263 } 264 } 265 266 private void validateInput(View v) { 267 String str = String.valueOf(((TextView) v).getText()); 268 if ("".equals(str)) { 269 270 // Restore to the old value as we don't allow empty values 271 updateView(); 272 } else { 273 274 // Check the new value and ensure it's in range 275 validateCurrentView(str); 276 } 277 } 278 279 /** 280 * We start the long click here but rely on the {@link NumberPickerButton} to inform us when the 281 * long click has ended. 282 */ 283 public boolean onLongClick(View v) { 284 285 /* 286 * The text view may still have focus so clear it's focus which will trigger the on focus 287 * changed and any typed values to be pulled. 288 */ 289 mText.clearFocus(); 290 291 if (R.id.increment == v.getId()) { 292 mIncrement = true; 293 mHandler.post(mRunnable); 294 } else if (R.id.decrement == v.getId()) { 295 mDecrement = true; 296 mHandler.post(mRunnable); 297 } 298 return true; 299 } 300 301 public void cancelIncrement() { 302 mIncrement = false; 303 } 304 305 public void cancelDecrement() { 306 mDecrement = false; 307 } 308 309 private static final char[] DIGIT_CHARACTERS = 310 new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; 311 312 private final NumberPickerButton mIncrementButton; 313 private final NumberPickerButton mDecrementButton; 314 315 private class NumberPickerInputFilter implements InputFilter { 316 public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, 317 int dend) { 318 if (mDisplayedValues == null) { 319 return mNumberInputFilter.filter(source, start, end, dest, dstart, dend); 320 } 321 CharSequence filtered = String.valueOf(source.subSequence(start, end)); 322 String result = 323 String.valueOf(dest.subSequence(0, dstart)) + filtered 324 + dest.subSequence(dend, dest.length()); 325 String str = String.valueOf(result).toLowerCase(); 326 for (String val : mDisplayedValues) { 327 val = val.toLowerCase(); 328 if (val.startsWith(str)) { 329 return filtered; 330 } 331 } 332 return ""; 333 } 334 } 335 336 private class NumberRangeKeyListener extends NumberKeyListener { 337 338 // XXX This doesn't allow for range limits when controlled by a 339 // soft input method! 340 public int getInputType() { 341 return InputType.TYPE_CLASS_NUMBER; 342 } 343 344 @Override 345 protected char[] getAcceptedChars() { 346 return DIGIT_CHARACTERS; 347 } 348 349 @Override 350 public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, 351 int dend) { 352 353 CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); 354 if (filtered == null) { 355 filtered = source.subSequence(start, end); 356 } 357 358 String result = 359 String.valueOf(dest.subSequence(0, dstart)) + filtered 360 + dest.subSequence(dend, dest.length()); 361 362 if ("".equals(result)) { 363 return result; 364 } 365 int val = getSelectedPos(result); 366 367 /* 368 * Ensure the user can't type in a value greater than the max allowed. We have to allow less 369 * than min as the user might want to delete some numbers and then type a new number. 370 */ 371 if (val > mEnd) { 372 return ""; 373 } else { 374 return filtered; 375 } 376 } 377 } 378 379 private int getSelectedPos(String str) { 380 if (mDisplayedValues == null) { 381 return Integer.parseInt(str); 382 } else { 383 for (int i = 0; i < mDisplayedValues.length; i++) { 384 385 /* Don't force the user to type in jan when ja will do */ 386 str = str.toLowerCase(); 387 if (mDisplayedValues[i].toLowerCase().startsWith(str)) { 388 return mStart + i; 389 } 390 } 391 392 /* 393 * The user might have typed in a number into the month field i.e. 10 instead of OCT so 394 * support that too. 395 */ 396 try { 397 return Integer.parseInt(str); 398 } catch (NumberFormatException e) { 399 400 /* Ignore as if it's not a number we don't care */ 401 } 402 } 403 return mStart; 404 } 405 406 /** 407 * @return the current value. 408 */ 409 public int getCurrent() { 410 return mCurrent; 411 } 412}