OSDN Git Service

original
[gb-231r1-is01/Gingerbread_2.3.3_r1_IS01.git] / frameworks / base / core / java / android / widget / NumberPicker.java
diff --git a/frameworks/base/core/java/android/widget/NumberPicker.java b/frameworks/base/core/java/android/widget/NumberPicker.java
new file mode 100644 (file)
index 0000000..4482b5b
--- /dev/null
@@ -0,0 +1,526 @@
+/*
+ * 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;
+    }
+}