/* * 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 android.annotation.CallSuper; import android.annotation.IntDef; import android.annotation.TestApi; import android.annotation.Widget; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.InputFilter; import android.text.InputType; import android.text.Spanned; import android.text.TextUtils; import android.text.method.NumberKeyListener; import android.util.AttributeSet; import android.util.SparseArray; import android.util.TypedValue; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.LayoutInflater.Filter; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeProvider; import android.view.animation.DecelerateInterpolator; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import com.android.internal.R; import libcore.icu.LocaleData; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; /** * A widget that enables the user to select a number from a predefined range. * There are two flavors of this widget and which one is presented to the user * depends on the current theme. *
* For an example of using this widget, see {@link android.widget.TimePicker}. *
*/ @Widget public class NumberPicker extends LinearLayout { /** * The number of items show in the selector wheel. */ private static final int SELECTOR_WHEEL_ITEM_COUNT = 3; /** * The default update interval during long press. */ private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300; /** * The index of the middle selector item. */ private static final int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2; /** * The coefficient by which to adjust (divide) the max fling velocity. */ private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8; /** * The the duration for adjusting the selector wheel. */ private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; /** * The duration of scrolling while snapping to a given position. */ private static final int SNAP_SCROLL_DURATION = 300; /** * The strength of fading in the top and bottom while drawing the selector. */ private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f; /** * The default unscaled height of the selection divider. */ private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2; /** * The default unscaled distance between the selection dividers. */ private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48; /** * The resource id for the default layout. */ private static final int DEFAULT_LAYOUT_RESOURCE_ID = R.layout.number_picker; /** * Constant for unspecified size. */ private static final int SIZE_UNSPECIFIED = -1; /** * User choice on whether the selector wheel should be wrapped. */ private boolean mWrapSelectorWheelPreferred = true; /** * 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(). */ private static class TwoDigitFormatter implements NumberPicker.Formatter { final StringBuilder mBuilder = new StringBuilder(); char mZeroDigit; java.util.Formatter mFmt; final Object[] mArgs = new Object[1]; TwoDigitFormatter() { final Locale locale = Locale.getDefault(); init(locale); } private void init(Locale locale) { mFmt = createFormatter(locale); mZeroDigit = getZeroDigit(locale); } public String format(int value) { final Locale currentLocale = Locale.getDefault(); if (mZeroDigit != getZeroDigit(currentLocale)) { init(currentLocale); } mArgs[0] = value; mBuilder.delete(0, mBuilder.length()); mFmt.format("%02d", mArgs); return mFmt.toString(); } private static char getZeroDigit(Locale locale) { return LocaleData.get(locale).zeroDigit; } private java.util.Formatter createFormatter(Locale locale) { return new java.util.Formatter(mBuilder, locale); } } private static final TwoDigitFormatter sTwoDigitFormatter = new TwoDigitFormatter(); /** * @hide */ public static final Formatter getTwoDigitFormatter() { return sTwoDigitFormatter; } /** * The increment button. */ private final ImageButton mIncrementButton; /** * The decrement button. */ private final ImageButton mDecrementButton; /** * The text for showing the current value. */ private final EditText mInputText; /** * The distance between the two selection dividers. */ private final int mSelectionDividersDistance; /** * The min height of this widget. */ private final int mMinHeight; /** * The max height of this widget. */ private final int mMaxHeight; /** * The max width of this widget. */ private final int mMinWidth; /** * The max width of this widget. */ private int mMaxWidth; /** * Flag whether to compute the max width. */ private final boolean mComputeMaxWidth; /** * The height of the text. */ private final int mTextSize; /** * The height of the gap between text elements if the selector wheel. */ private int mSelectorTextGapHeight; /** * The values to be displayed instead the indices. */ private String[] mDisplayedValues; /** * Lower value of the range of numbers allowed for the NumberPicker */ private int mMinValue; /** * Upper value of the range of numbers allowed for the NumberPicker */ private int mMaxValue; /** * Current value of this NumberPicker */ private int mValue; /** * Listener to be notified upon current value change. */ private OnValueChangeListener mOnValueChangeListener; /** * Listener to be notified upon scroll state change. */ private OnScrollListener mOnScrollListener; /** * Formatter for for displaying the current value. */ private Formatter mFormatter; /** * The speed for updating the value form long press. */ private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL; /** * Cache for the string representation of selector indices. */ private final SparseArray* Note: If you have provided alternative values for the values this * formatter is never invoked. *
* * @param formatter The formatter object. If formatter isnull
,
* {@link String#valueOf(int)} will be used.
*@see #setDisplayedValues(String[])
*/
public void setFormatter(Formatter formatter) {
if (formatter == mFormatter) {
return;
}
mFormatter = formatter;
initializeSelectorWheelIndices();
updateInputTextView();
}
/**
* Set the current value for the number picker.
*
* If the argument is less than the {@link NumberPicker#getMinValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is false
the
* current value is set to the {@link NumberPicker#getMinValue()} value.
*
* If the argument is less than the {@link NumberPicker#getMinValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is true
the
* current value is set to the {@link NumberPicker#getMaxValue()} value.
*
* If the argument is less than the {@link NumberPicker#getMaxValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is false
the
* current value is set to the {@link NumberPicker#getMaxValue()} value.
*
* If the argument is less than the {@link NumberPicker#getMaxValue()} and
* {@link NumberPicker#getWrapSelectorWheel()} is true
the
* current value is set to the {@link NumberPicker#getMinValue()} value.
*
* By default if the range (max - min) is more than the number of items shown * on the selector wheel the selector wheel wrapping is enabled. *
** Note: If the number of items, i.e. the range ( * {@link #getMaxValue()} - {@link #getMinValue()}) is less than * the number of items shown on the selector wheel, the selector wheel will * not wrap. Hence, in such a case calling this method is a NOP. *
* * @param wrapSelectorWheel Whether to wrap. */ public void setWrapSelectorWheel(boolean wrapSelectorWheel) { mWrapSelectorWheelPreferred = wrapSelectorWheel; updateWrapSelectorWheel(); } /** * Whether or not the selector wheel should be wrapped is determined by user choice and whether * the choice is allowed. The former comes from {@link #setWrapSelectorWheel(boolean)}, the * latter is calculated based on min & max value set vs selector's visual length. Therefore, * this method should be called any time any of the 3 values (i.e. user choice, min and max * value) gets updated. */ private void updateWrapSelectorWheel() { final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length; mWrapSelectorWheel = wrappingAllowed && mWrapSelectorWheelPreferred; } /** * Sets the speed at which the numbers be incremented and decremented when * the up and down buttons are long pressed respectively. ** The default value is 300 ms. *
* * @param intervalMillis The speed (in milliseconds) at which the numbers * will be incremented and decremented. */ public void setOnLongPressUpdateInterval(long intervalMillis) { mLongPressUpdateInterval = intervalMillis; } /** * Returns the value of the picker. * * @return The value. */ public int getValue() { return mValue; } /** * Returns the min value of the picker. * * @return The min value */ public int getMinValue() { return mMinValue; } /** * Sets the min value of the picker. * * @param minValue The min value inclusive. * * Note: The length of the displayed values array * set via {@link #setDisplayedValues(String[])} must be equal to the * range of selectable numbers which is equal to * {@link #getMaxValue()} - {@link #getMinValue()} + 1. */ public void setMinValue(int minValue) { if (mMinValue == minValue) { return; } if (minValue < 0) { throw new IllegalArgumentException("minValue must be >= 0"); } mMinValue = minValue; if (mMinValue > mValue) { mValue = mMinValue; } updateWrapSelectorWheel(); initializeSelectorWheelIndices(); updateInputTextView(); tryComputeMaxWidth(); invalidate(); } /** * Returns the max value of the picker. * * @return The max value. */ public int getMaxValue() { return mMaxValue; } /** * Sets the max value of the picker. * * @param maxValue The max value inclusive. * * Note: The length of the displayed values array * set via {@link #setDisplayedValues(String[])} must be equal to the * range of selectable numbers which is equal to * {@link #getMaxValue()} - {@link #getMinValue()} + 1. */ public void setMaxValue(int maxValue) { if (mMaxValue == maxValue) { return; } if (maxValue < 0) { throw new IllegalArgumentException("maxValue must be >= 0"); } mMaxValue = maxValue; if (mMaxValue < mValue) { mValue = mMaxValue; } updateWrapSelectorWheel(); initializeSelectorWheelIndices(); updateInputTextView(); tryComputeMaxWidth(); invalidate(); } /** * Gets the values to be displayed instead of string values. * * @return The displayed values. */ public String[] getDisplayedValues() { return mDisplayedValues; } /** * Sets the values to be displayed. * * @param displayedValues The displayed values. * * Note: The length of the displayed values array * must be equal to the range of selectable numbers which is equal to * {@link #getMaxValue()} - {@link #getMinValue()} + 1. */ public void setDisplayedValues(String[] displayedValues) { if (mDisplayedValues == displayedValues) { return; } mDisplayedValues = displayedValues; if (mDisplayedValues != null) { // Allow text entry rather than strictly numeric entry. mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); } else { mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); } updateInputTextView(); initializeSelectorWheelIndices(); tryComputeMaxWidth(); } /** * Retrieves the displayed value for the current selection in this picker. * * @hide */ @TestApi public CharSequence getDisplayedValueForCurrentSelection() { // The cache field itself is initialized at declaration time, and since it's final, it // can't be null here. The cache is updated in ensureCachedScrollSelectorValue which is // called, directly or indirectly, on every call to setDisplayedValues, setFormatter, // setMinValue, setMaxValue and setValue, as well as user-driven interaction with the // picker. As such, the contents of the cache are always synced to the latest state of // the widget. return mSelectorIndexToStringCache.get(getValue()); } @Override protected float getTopFadingEdgeStrength() { return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; } @Override protected float getBottomFadingEdgeStrength() { return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); removeAllCallbacks(); } @CallSuper @Override protected void drawableStateChanged() { super.drawableStateChanged(); final Drawable selectionDivider = mSelectionDivider; if (selectionDivider != null && selectionDivider.isStateful() && selectionDivider.setState(getDrawableState())) { invalidateDrawable(selectionDivider); } } @CallSuper @Override public void jumpDrawablesToCurrentState() { super.jumpDrawablesToCurrentState(); if (mSelectionDivider != null) { mSelectionDivider.jumpToCurrentState(); } } /** @hide */ @Override public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) { super.onResolveDrawables(layoutDirection); if (mSelectionDivider != null) { mSelectionDivider.setLayoutDirection(layoutDirection); } } @Override protected void onDraw(Canvas canvas) { if (!mHasSelectorWheel) { super.onDraw(canvas); return; } final boolean showSelectorWheel = mHideWheelUntilFocused ? hasFocus() : true; float x = (mRight - mLeft) / 2; float y = mCurrentScrollOffset; // draw the virtual buttons pressed state if needed if (showSelectorWheel && mVirtualButtonPressedDrawable != null && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { if (mDecrementVirtualButtonPressed) { mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); mVirtualButtonPressedDrawable.setBounds(0, 0, mRight, mTopSelectionDividerTop); mVirtualButtonPressedDrawable.draw(canvas); } if (mIncrementVirtualButtonPressed) { mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, mRight, mBottom); mVirtualButtonPressedDrawable.draw(canvas); } } // draw the selector wheel int[] selectorIndices = mSelectorIndices; for (int i = 0; i < selectorIndices.length; i++) { int selectorIndex = selectorIndices[i]; String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); // Do not draw the middle item if input is visible since the input // is shown only if the wheel is static and it covers the middle // item. Otherwise, if the user starts editing the text via the // IME he may see a dimmed version of the old value intermixed // with the new one. if ((showSelectorWheel && i != SELECTOR_MIDDLE_ITEM_INDEX) || (i == SELECTOR_MIDDLE_ITEM_INDEX && mInputText.getVisibility() != VISIBLE)) { canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint); } y += mSelectorElementHeight; } // draw the selection dividers if (showSelectorWheel && mSelectionDivider != null) { // draw the top divider int topOfTopDivider = mTopSelectionDividerTop; int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight; mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider); mSelectionDivider.draw(canvas); // draw the bottom divider int bottomOfBottomDivider = mBottomSelectionDividerBottom; int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight; mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider); mSelectionDivider.draw(canvas); } } /** @hide */ @Override public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { super.onInitializeAccessibilityEventInternal(event); event.setClassName(NumberPicker.class.getName()); event.setScrollable(true); event.setScrollY((mMinValue + mValue) * mSelectorElementHeight); event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight); } @Override public AccessibilityNodeProvider getAccessibilityNodeProvider() { if (!mHasSelectorWheel) { return super.getAccessibilityNodeProvider(); } if (mAccessibilityNodeProvider == null) { mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl(); } return mAccessibilityNodeProvider; } /** * Makes a measure spec that tries greedily to use the max value. * * @param measureSpec The measure spec. * @param maxSize The max value for the size. * @return A measure spec greedily imposing the max size. */ private int makeMeasureSpec(int measureSpec, int maxSize) { if (maxSize == SIZE_UNSPECIFIED) { return measureSpec; } final int size = MeasureSpec.getSize(measureSpec); final int mode = MeasureSpec.getMode(measureSpec); switch (mode) { case MeasureSpec.EXACTLY: return measureSpec; case MeasureSpec.AT_MOST: return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY); case MeasureSpec.UNSPECIFIED: return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY); default: throw new IllegalArgumentException("Unknown measure mode: " + mode); } } /** * Utility to reconcile a desired size and state, with constraints imposed * by a MeasureSpec. Tries to respect the min size, unless a different size * is imposed by the constraints. * * @param minSize The minimal desired size. * @param measuredSize The currently measured size. * @param measureSpec The current measure spec. * @return The resolved size and state. */ private int resolveSizeAndStateRespectingMinSize( int minSize, int measuredSize, int measureSpec) { if (minSize != SIZE_UNSPECIFIED) { final int desiredWidth = Math.max(minSize, measuredSize); return resolveSizeAndState(desiredWidth, measureSpec, 0); } else { return measuredSize; } } /** * Resets the selector indices and clear the cached string representation of * these indices. */ private void initializeSelectorWheelIndices() { mSelectorIndexToStringCache.clear(); int[] selectorIndices = mSelectorIndices; int current = getValue(); for (int i = 0; i < mSelectorIndices.length; i++) { int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX); if (mWrapSelectorWheel) { selectorIndex = getWrappedSelectorIndex(selectorIndex); } selectorIndices[i] = selectorIndex; ensureCachedScrollSelectorValue(selectorIndices[i]); } } /** * Sets the current value of this NumberPicker. * * @param current The new value of the NumberPicker. * @param notifyChange Whether to notify if the current value changed. */ private void setValueInternal(int current, boolean notifyChange) { if (mValue == current) { return; } // Wrap around the values if we go past the start or end if (mWrapSelectorWheel) { current = getWrappedSelectorIndex(current); } else { current = Math.max(current, mMinValue); current = Math.min(current, mMaxValue); } int previous = mValue; mValue = current; // If we're flinging, we'll update the text view at the end when it becomes visible if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) { updateInputTextView(); } if (notifyChange) { notifyChange(previous, current); } initializeSelectorWheelIndices(); invalidate(); } /** * Changes the current value by one which is increment or * decrement based on the passes argument. * decrement the current value. * * @param increment True to increment, false to decrement. */ private void changeValueByOne(boolean increment) { if (mHasSelectorWheel) { hideSoftInput(); if (!moveToFinalScrollerPosition(mFlingScroller)) { moveToFinalScrollerPosition(mAdjustScroller); } mPreviousScrollerY = 0; if (increment) { mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, SNAP_SCROLL_DURATION); } else { mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight, SNAP_SCROLL_DURATION); } invalidate(); } else { if (increment) { setValueInternal(mValue + 1, true); } else { setValueInternal(mValue - 1, true); } } } private void initializeSelectorWheel() { initializeSelectorWheelIndices(); int[] selectorIndices = mSelectorIndices; int totalTextHeight = selectorIndices.length * mTextSize; float totalTextGapHeight = (mBottom - mTop) - totalTextHeight; float textGapCount = selectorIndices.length; mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f); mSelectorElementHeight = mTextSize + mSelectorTextGapHeight; // Ensure that the middle item is positioned the same as the text in // mInputText int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop(); mInitialScrollOffset = editTextTextPosition - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX); mCurrentScrollOffset = mInitialScrollOffset; updateInputTextView(); } private void initializeFadingEdges() { setVerticalFadingEdgeEnabled(true); setFadingEdgeLength((mBottom - mTop - mTextSize) / 2); } /** * Callback invoked upon completion of a givenscroller
.
*/
private void onScrollerFinished(Scroller scroller) {
if (scroller == mFlingScroller) {
ensureScrollWheelAdjusted();
updateInputTextView();
onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
} else {
if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
updateInputTextView();
}
}
}
/**
* Handles transition to a given scrollState
*/
private void onScrollStateChange(int scrollState) {
if (mScrollState == scrollState) {
return;
}
mScrollState = scrollState;
if (mOnScrollListener != null) {
mOnScrollListener.onScrollStateChange(this, scrollState);
}
}
/**
* Flings the selector with the given velocityY
.
*/
private void fling(int velocityY) {
mPreviousScrollerY = 0;
if (velocityY > 0) {
mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
} else {
mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
}
invalidate();
}
/**
* @return The wrapped index selectorIndex
value.
*/
private int getWrappedSelectorIndex(int selectorIndex) {
if (selectorIndex > mMaxValue) {
return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1;
} else if (selectorIndex < mMinValue) {
return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1;
}
return selectorIndex;
}
/**
* Increments the selectorIndices
whose string representations
* will be displayed in the selector.
*/
private void incrementSelectorIndices(int[] selectorIndices) {
for (int i = 0; i < selectorIndices.length - 1; i++) {
selectorIndices[i] = selectorIndices[i + 1];
}
int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) {
nextScrollSelectorIndex = mMinValue;
}
selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
}
/**
* Decrements the selectorIndices
whose string representations
* will be displayed in the selector.
*/
private void decrementSelectorIndices(int[] selectorIndices) {
for (int i = selectorIndices.length - 1; i > 0; i--) {
selectorIndices[i] = selectorIndices[i - 1];
}
int nextScrollSelectorIndex = selectorIndices[1] - 1;
if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
nextScrollSelectorIndex = mMaxValue;
}
selectorIndices[0] = nextScrollSelectorIndex;
ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
}
/**
* Ensures we have a cached string representation of the given
* selectorIndex
to avoid multiple instantiations of the same string.
*/
private void ensureCachedScrollSelectorValue(int selectorIndex) {
SparseArrayvalue
.
*/
private int getSelectedPos(String value) {
if (mDisplayedValues == null) {
try {
return Integer.parseInt(value);
} 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
value = value.toLowerCase();
if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
return mMinValue + 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(value);
} catch (NumberFormatException e) {
// Ignore as if it's not a number we don't care
}
}
return mMinValue;
}
/**
* Posts a {@link SetSelectionCommand} from the given
* {@code selectionStart} to {@code selectionEnd}.
*/
private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
if (mSetSelectionCommand == null) {
mSetSelectionCommand = new SetSelectionCommand(mInputText);
}
mSetSelectionCommand.post(selectionStart, selectionEnd);
}
/**
* The numbers accepted by the input text's {@link Filter}
*/
private static final char[] DIGIT_CHARACTERS = new char[] {
// Latin digits are the common case
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
// Arabic-Indic
'\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668'
, '\u0669',
// Extended Arabic-Indic
'\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8'
, '\u06f9',
// Hindi and Marathi (Devanagari script)
'\u0966', '\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e'
, '\u096f',
// Bengali
'\u09e6', '\u09e7', '\u09e8', '\u09e9', '\u09ea', '\u09eb', '\u09ec', '\u09ed', '\u09ee'
, '\u09ef',
// Kannada
'\u0ce6', '\u0ce7', '\u0ce8', '\u0ce9', '\u0cea', '\u0ceb', '\u0cec', '\u0ced', '\u0cee'
, '\u0cef'
};
/**
* Filter for accepting only valid indices or prefixes of the string
* representation of valid indices.
*/
class InputTextFilter extends NumberKeyListener {
// XXX This doesn't allow for range limits when controlled by a
// soft input method!
public int getInputType() {
return InputType.TYPE_CLASS_TEXT;
}
@Override
protected char[] getAcceptedChars() {
return DIGIT_CHARACTERS;
}
@Override
public CharSequence filter(
CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
// We don't know what the output will be, so always cancel any
// pending set selection command.
if (mSetSelectionCommand != null) {
mSetSelectionCommand.cancel();
}
if (mDisplayedValues == null) {
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.
* And prevent multiple-"0" that exceeds the length of upper
* bound number.
*/
if (val > mMaxValue || result.length() > String.valueOf(mMaxValue).length()) {
return "";
} else {
return filtered;
}
} else {
CharSequence filtered = String.valueOf(source.subSequence(start, end));
if (TextUtils.isEmpty(filtered)) {
return "";
}
String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
+ dest.subSequence(dend, dest.length());
String str = String.valueOf(result).toLowerCase();
for (String val : mDisplayedValues) {
String valLowerCase = val.toLowerCase();
if (valLowerCase.startsWith(str)) {
postSetSelectionCommand(result.length(), val.length());
return val.subSequence(dstart, val.length());
}
}
return "";
}
}
}
/**
* Ensures that the scroll wheel is adjusted i.e. there is no offset and the
* middle element is in the middle of the widget.
*
* @return Whether an adjustment has been made.
*/
private boolean ensureScrollWheelAdjusted() {
// adjust to the closest value
int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
if (deltaY != 0) {
mPreviousScrollerY = 0;
if (Math.abs(deltaY) > mSelectorElementHeight / 2) {
deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;
}
mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
invalidate();
return true;
}
return false;
}
class PressedStateHelper implements Runnable {
public static final int BUTTON_INCREMENT = 1;
public static final int BUTTON_DECREMENT = 2;
private final int MODE_PRESS = 1;
private final int MODE_TAPPED = 2;
private int mManagedButton;
private int mMode;
public void cancel() {
mMode = 0;
mManagedButton = 0;
NumberPicker.this.removeCallbacks(this);
if (mIncrementVirtualButtonPressed) {
mIncrementVirtualButtonPressed = false;
invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
}
mDecrementVirtualButtonPressed = false;
if (mDecrementVirtualButtonPressed) {
invalidate(0, 0, mRight, mTopSelectionDividerTop);
}
}
public void buttonPressDelayed(int button) {
cancel();
mMode = MODE_PRESS;
mManagedButton = button;
NumberPicker.this.postDelayed(this, ViewConfiguration.getTapTimeout());
}
public void buttonTapped(int button) {
cancel();
mMode = MODE_TAPPED;
mManagedButton = button;
NumberPicker.this.post(this);
}
@Override
public void run() {
switch (mMode) {
case MODE_PRESS: {
switch (mManagedButton) {
case BUTTON_INCREMENT: {
mIncrementVirtualButtonPressed = true;
invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
} break;
case BUTTON_DECREMENT: {
mDecrementVirtualButtonPressed = true;
invalidate(0, 0, mRight, mTopSelectionDividerTop);
}
}
} break;
case MODE_TAPPED: {
switch (mManagedButton) {
case BUTTON_INCREMENT: {
if (!mIncrementVirtualButtonPressed) {
NumberPicker.this.postDelayed(this,
ViewConfiguration.getPressedStateDuration());
}
mIncrementVirtualButtonPressed ^= true;
invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
} break;
case BUTTON_DECREMENT: {
if (!mDecrementVirtualButtonPressed) {
NumberPicker.this.postDelayed(this,
ViewConfiguration.getPressedStateDuration());
}
mDecrementVirtualButtonPressed ^= true;
invalidate(0, 0, mRight, mTopSelectionDividerTop);
}
}
} break;
}
}
}
/**
* Command for setting the input text selection.
*/
private static class SetSelectionCommand implements Runnable {
private final EditText mInputText;
private int mSelectionStart;
private int mSelectionEnd;
/** Whether this runnable is currently posted. */
private boolean mPosted;
public SetSelectionCommand(EditText inputText) {
mInputText = inputText;
}
public void post(int selectionStart, int selectionEnd) {
mSelectionStart = selectionStart;
mSelectionEnd = selectionEnd;
if (!mPosted) {
mInputText.post(this);
mPosted = true;
}
}
public void cancel() {
if (mPosted) {
mInputText.removeCallbacks(this);
mPosted = false;
}
}
@Override
public void run() {
mPosted = false;
mInputText.setSelection(mSelectionStart, mSelectionEnd);
}
}
/**
* Command for changing the current value from a long press by one.
*/
class ChangeCurrentByOneFromLongPressCommand implements Runnable {
private boolean mIncrement;
private void setStep(boolean increment) {
mIncrement = increment;
}
@Override
public void run() {
changeValueByOne(mIncrement);
postDelayed(this, mLongPressUpdateInterval);
}
}
/**
* @hide
*/
public static class CustomEditText extends EditText {
public CustomEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onEditorAction(int actionCode) {
super.onEditorAction(actionCode);
if (actionCode == EditorInfo.IME_ACTION_DONE) {
clearFocus();
}
}
}
/**
* Command for beginning soft input on long press.
*/
class BeginSoftInputOnLongPressCommand implements Runnable {
@Override
public void run() {
performLongClick();
}
}
/**
* Class for managing virtual view tree rooted at this picker.
*/
class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider {
private static final int UNDEFINED = Integer.MIN_VALUE;
private static final int VIRTUAL_VIEW_ID_INCREMENT = 1;
private static final int VIRTUAL_VIEW_ID_INPUT = 2;
private static final int VIRTUAL_VIEW_ID_DECREMENT = 3;
private final Rect mTempRect = new Rect();
private final int[] mTempArray = new int[2];
private int mAccessibilityFocusedView = UNDEFINED;
@Override
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
switch (virtualViewId) {
case View.NO_ID:
return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY,
mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
case VIRTUAL_VIEW_ID_DECREMENT:
return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT,
getVirtualDecrementButtonText(), mScrollX, mScrollY,
mScrollX + (mRight - mLeft),
mTopSelectionDividerTop + mSelectionDividerHeight);
case VIRTUAL_VIEW_ID_INPUT:
return createAccessibiltyNodeInfoForInputText(mScrollX,
mTopSelectionDividerTop + mSelectionDividerHeight,
mScrollX + (mRight - mLeft),
mBottomSelectionDividerBottom - mSelectionDividerHeight);
case VIRTUAL_VIEW_ID_INCREMENT:
return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT,
getVirtualIncrementButtonText(), mScrollX,
mBottomSelectionDividerBottom - mSelectionDividerHeight,
mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
}
return super.createAccessibilityNodeInfo(virtualViewId);
}
@Override
public List