--- /dev/null
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import com.android.internal.R;
+
+import android.annotation.Widget;
+import android.content.Context;
+import android.os.Handler;
+import android.text.InputFilter;
+import android.text.InputType;
+import android.text.Spanned;
+import android.text.method.NumberKeyListener;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+
+/**
+ * A view for selecting a number
+ *
+ * For a dialog using this view, see {@link android.app.TimePickerDialog}.
+ * @hide
+ */
+@Widget
+public class NumberPicker extends LinearLayout {
+
+ /**
+ * The callback interface used to indicate the number value has been adjusted.
+ */
+ public interface OnChangedListener {
+ /**
+ * @param picker The NumberPicker associated with this listener.
+ * @param oldVal The previous value.
+ * @param newVal The new value.
+ */
+ void onChanged(NumberPicker picker, int oldVal, int newVal);
+ }
+
+ /**
+ * Interface used to format the number into a string for presentation
+ */
+ public interface Formatter {
+ String toString(int value);
+ }
+
+ /*
+ * Use a custom NumberPicker formatting callback to use two-digit
+ * minutes strings like "01". Keeping a static formatter etc. is the
+ * most efficient way to do this; it avoids creating temporary objects
+ * on every call to format().
+ */
+ public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER =
+ new NumberPicker.Formatter() {
+ final StringBuilder mBuilder = new StringBuilder();
+ final java.util.Formatter mFmt = new java.util.Formatter(mBuilder);
+ final Object[] mArgs = new Object[1];
+ public String toString(int value) {
+ mArgs[0] = value;
+ mBuilder.delete(0, mBuilder.length());
+ mFmt.format("%02d", mArgs);
+ return mFmt.toString();
+ }
+ };
+
+ private final Handler mHandler;
+ private final Runnable mRunnable = new Runnable() {
+ public void run() {
+ if (mIncrement) {
+ changeCurrent(mCurrent + 1);
+ mHandler.postDelayed(this, mSpeed);
+ } else if (mDecrement) {
+ changeCurrent(mCurrent - 1);
+ mHandler.postDelayed(this, mSpeed);
+ }
+ }
+ };
+
+ private final EditText mText;
+ private final InputFilter mNumberInputFilter;
+
+ private String[] mDisplayedValues;
+
+ /**
+ * Lower value of the range of numbers allowed for the NumberPicker
+ */
+ private int mStart;
+
+ /**
+ * Upper value of the range of numbers allowed for the NumberPicker
+ */
+ private int mEnd;
+
+ /**
+ * Current value of this NumberPicker
+ */
+ private int mCurrent;
+
+ /**
+ * Previous value of this NumberPicker.
+ */
+ private int mPrevious;
+ private OnChangedListener mListener;
+ private Formatter mFormatter;
+ private long mSpeed = 300;
+
+ private boolean mIncrement;
+ private boolean mDecrement;
+
+ /**
+ * Create a new number picker
+ * @param context the application environment
+ */
+ public NumberPicker(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Create a new number picker
+ * @param context the application environment
+ * @param attrs a collection of attributes
+ */
+ public NumberPicker(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setOrientation(VERTICAL);
+ LayoutInflater inflater =
+ (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.number_picker, this, true);
+ mHandler = new Handler();
+
+ OnClickListener clickListener = new OnClickListener() {
+ public void onClick(View v) {
+ validateInput(mText);
+ if (!mText.hasFocus()) mText.requestFocus();
+
+ // now perform the increment/decrement
+ if (R.id.increment == v.getId()) {
+ changeCurrent(mCurrent + 1);
+ } else if (R.id.decrement == v.getId()) {
+ changeCurrent(mCurrent - 1);
+ }
+ }
+ };
+
+ OnFocusChangeListener focusListener = new OnFocusChangeListener() {
+ public void onFocusChange(View v, boolean hasFocus) {
+
+ /* When focus is lost check that the text field
+ * has valid values.
+ */
+ if (!hasFocus) {
+ validateInput(v);
+ }
+ }
+ };
+
+ OnLongClickListener longClickListener = new OnLongClickListener() {
+ /**
+ * We start the long click here but rely on the {@link NumberPickerButton}
+ * to inform us when the long click has ended.
+ */
+ public boolean onLongClick(View v) {
+ /* The text view may still have focus so clear it's focus which will
+ * trigger the on focus changed and any typed values to be pulled.
+ */
+ mText.clearFocus();
+
+ if (R.id.increment == v.getId()) {
+ mIncrement = true;
+ mHandler.post(mRunnable);
+ } else if (R.id.decrement == v.getId()) {
+ mDecrement = true;
+ mHandler.post(mRunnable);
+ }
+ return true;
+ }
+ };
+
+ InputFilter inputFilter = new NumberPickerInputFilter();
+ mNumberInputFilter = new NumberRangeKeyListener();
+ mIncrementButton = (NumberPickerButton) findViewById(R.id.increment);
+ mIncrementButton.setOnClickListener(clickListener);
+ mIncrementButton.setOnLongClickListener(longClickListener);
+ mIncrementButton.setNumberPicker(this);
+
+ mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement);
+ mDecrementButton.setOnClickListener(clickListener);
+ mDecrementButton.setOnLongClickListener(longClickListener);
+ mDecrementButton.setNumberPicker(this);
+
+ mText = (EditText) findViewById(R.id.timepicker_input);
+ mText.setOnFocusChangeListener(focusListener);
+ mText.setFilters(new InputFilter[] {inputFilter});
+ mText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
+
+ if (!isEnabled()) {
+ setEnabled(false);
+ }
+ }
+
+ /**
+ * Set the enabled state of this view. The interpretation of the enabled
+ * state varies by subclass.
+ *
+ * @param enabled True if this view is enabled, false otherwise.
+ */
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ mIncrementButton.setEnabled(enabled);
+ mDecrementButton.setEnabled(enabled);
+ mText.setEnabled(enabled);
+ }
+
+ /**
+ * Set the callback that indicates the number has been adjusted by the user.
+ * @param listener the callback, should not be null.
+ */
+ public void setOnChangeListener(OnChangedListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Set the formatter that will be used to format the number for presentation
+ * @param formatter the formatter object. If formatter is null, String.valueOf()
+ * will be used
+ */
+ public void setFormatter(Formatter formatter) {
+ mFormatter = formatter;
+ }
+
+ /**
+ * Set the range of numbers allowed for the number picker. The current
+ * value will be automatically set to the start.
+ *
+ * @param start the start of the range (inclusive)
+ * @param end the end of the range (inclusive)
+ */
+ public void setRange(int start, int end) {
+ setRange(start, end, null/*displayedValues*/);
+ }
+
+ /**
+ * Set the range of numbers allowed for the number picker. The current
+ * value will be automatically set to the start. Also provide a mapping
+ * for values used to display to the user.
+ *
+ * @param start the start of the range (inclusive)
+ * @param end the end of the range (inclusive)
+ * @param displayedValues the values displayed to the user.
+ */
+ public void setRange(int start, int end, String[] displayedValues) {
+ mDisplayedValues = displayedValues;
+ mStart = start;
+ mEnd = end;
+ mCurrent = start;
+ updateView();
+
+ if (displayedValues != null) {
+ // Allow text entry rather than strictly numeric entry.
+ mText.setRawInputType(InputType.TYPE_CLASS_TEXT |
+ InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+ }
+ }
+
+ /**
+ * Set the current value for the number picker.
+ *
+ * @param current the current value the start of the range (inclusive)
+ * @throws IllegalArgumentException when current is not within the range
+ * of of the number picker
+ */
+ public void setCurrent(int current) {
+ if (current < mStart || current > mEnd) {
+ throw new IllegalArgumentException(
+ "current should be >= start and <= end");
+ }
+ mCurrent = current;
+ updateView();
+ }
+
+ /**
+ * Sets the speed at which the numbers will scroll when the +/-
+ * buttons are longpressed
+ *
+ * @param speed The speed (in milliseconds) at which the numbers will scroll
+ * default 300ms
+ */
+ public void setSpeed(long speed) {
+ mSpeed = speed;
+ }
+
+ private String formatNumber(int value) {
+ return (mFormatter != null)
+ ? mFormatter.toString(value)
+ : String.valueOf(value);
+ }
+
+ /**
+ * Sets the current value of this NumberPicker, and sets mPrevious to the previous
+ * value. If current is greater than mEnd less than mStart, the value of mCurrent
+ * is wrapped around.
+ *
+ * Subclasses can override this to change the wrapping behavior
+ *
+ * @param current the new value of the NumberPicker
+ */
+ protected void changeCurrent(int current) {
+ // Wrap around the values if we go past the start or end
+ if (current > mEnd) {
+ current = mStart;
+ } else if (current < mStart) {
+ current = mEnd;
+ }
+ mPrevious = mCurrent;
+ mCurrent = current;
+ notifyChange();
+ updateView();
+ }
+
+ /**
+ * Notifies the listener, if registered, of a change of the value of this
+ * NumberPicker.
+ */
+ private void notifyChange() {
+ if (mListener != null) {
+ mListener.onChanged(this, mPrevious, mCurrent);
+ }
+ }
+
+ /**
+ * Updates the view of this NumberPicker. If displayValues were specified
+ * in {@link #setRange}, the string corresponding to the index specified by
+ * the current value will be returned. Otherwise, the formatter specified
+ * in {@link setFormatter} will be used to format the number.
+ */
+ private void updateView() {
+ /* If we don't have displayed values then use the
+ * current number else find the correct value in the
+ * displayed values for the current number.
+ */
+ if (mDisplayedValues == null) {
+ mText.setText(formatNumber(mCurrent));
+ } else {
+ mText.setText(mDisplayedValues[mCurrent - mStart]);
+ }
+ mText.setSelection(mText.getText().length());
+ }
+
+ private void validateCurrentView(CharSequence str) {
+ int val = getSelectedPos(str.toString());
+ if ((val >= mStart) && (val <= mEnd)) {
+ if (mCurrent != val) {
+ mPrevious = mCurrent;
+ mCurrent = val;
+ notifyChange();
+ }
+ }
+ updateView();
+ }
+
+ private void validateInput(View v) {
+ String str = String.valueOf(((TextView) v).getText());
+ if ("".equals(str)) {
+
+ // Restore to the old value as we don't allow empty values
+ updateView();
+ } else {
+
+ // Check the new value and ensure it's in range
+ validateCurrentView(str);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void cancelIncrement() {
+ mIncrement = false;
+ }
+
+ /**
+ * @hide
+ */
+ public void cancelDecrement() {
+ mDecrement = false;
+ }
+
+ private static final char[] DIGIT_CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
+ };
+
+ private NumberPickerButton mIncrementButton;
+ private NumberPickerButton mDecrementButton;
+
+ private class NumberPickerInputFilter implements InputFilter {
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+ if (mDisplayedValues == null) {
+ return mNumberInputFilter.filter(source, start, end, dest, dstart, dend);
+ }
+ CharSequence filtered = String.valueOf(source.subSequence(start, end));
+ String result = String.valueOf(dest.subSequence(0, dstart))
+ + filtered
+ + dest.subSequence(dend, dest.length());
+ String str = String.valueOf(result).toLowerCase();
+ for (String val : mDisplayedValues) {
+ val = val.toLowerCase();
+ if (val.startsWith(str)) {
+ return filtered;
+ }
+ }
+ return "";
+ }
+ }
+
+ private class NumberRangeKeyListener extends NumberKeyListener {
+
+ // XXX This doesn't allow for range limits when controlled by a
+ // soft input method!
+ public int getInputType() {
+ return InputType.TYPE_CLASS_NUMBER;
+ }
+
+ @Override
+ protected char[] getAcceptedChars() {
+ return DIGIT_CHARACTERS;
+ }
+
+ @Override
+ public CharSequence filter(CharSequence source, int start, int end,
+ Spanned dest, int dstart, int dend) {
+
+ CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
+ if (filtered == null) {
+ filtered = source.subSequence(start, end);
+ }
+
+ String result = String.valueOf(dest.subSequence(0, dstart))
+ + filtered
+ + dest.subSequence(dend, dest.length());
+
+ if ("".equals(result)) {
+ return result;
+ }
+ int val = getSelectedPos(result);
+
+ /* Ensure the user can't type in a value greater
+ * than the max allowed. We have to allow less than min
+ * as the user might want to delete some numbers
+ * and then type a new number.
+ */
+ if (val > mEnd) {
+ return "";
+ } else {
+ return filtered;
+ }
+ }
+ }
+
+ private int getSelectedPos(String str) {
+ if (mDisplayedValues == null) {
+ try {
+ return Integer.parseInt(str);
+ } catch (NumberFormatException e) {
+ /* Ignore as if it's not a number we don't care */
+ }
+ } else {
+ for (int i = 0; i < mDisplayedValues.length; i++) {
+ /* Don't force the user to type in jan when ja will do */
+ str = str.toLowerCase();
+ if (mDisplayedValues[i].toLowerCase().startsWith(str)) {
+ return mStart + i;
+ }
+ }
+
+ /* The user might have typed in a number into the month field i.e.
+ * 10 instead of OCT so support that too.
+ */
+ try {
+ return Integer.parseInt(str);
+ } catch (NumberFormatException e) {
+
+ /* Ignore as if it's not a number we don't care */
+ }
+ }
+ return mStart;
+ }
+
+ /**
+ * Returns the current value of the NumberPicker
+ * @return the current value.
+ */
+ public int getCurrent() {
+ return mCurrent;
+ }
+
+ /**
+ * Returns the upper value of the range of the NumberPicker
+ * @return the uppper number of the range.
+ */
+ protected int getEndRange() {
+ return mEnd;
+ }
+
+ /**
+ * Returns the lower value of the range of the NumberPicker
+ * @return the lower number of the range.
+ */
+ protected int getBeginRange() {
+ return mStart;
+ }
+}